google mail API [[[WIP]]]
This commit is contained in:
parent
8f15d42173
commit
8f13eb2f16
54
googleapi/README.md
Normal file
54
googleapi/README.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
Google OAuth Setup (to send mails)
|
||||||
|
==================================
|
||||||
|
|
||||||
|
|
||||||
|
- Login @ https://console.cloud.google.com
|
||||||
|
|
||||||
|
- GMail API akivieren: https://console.cloud.google.com/apis/library/gmail.googleapis.com?
|
||||||
|
|
||||||
|
- Create new Project (aka 'BackendMailAPI') @ https://console.cloud.google.com/projectcreate
|
||||||
|
User Type: Intern
|
||||||
|
Anwendungsname: 'BackendMailAPI'
|
||||||
|
Support-Email: ...
|
||||||
|
Authorisierte Domains: 'heydyno.de' (or project domain)
|
||||||
|
Kontakt-Email: ...
|
||||||
|
|
||||||
|
|
||||||
|
- Unter "Anmeldedaten" neuer OAuth Client erstellen @ https://console.cloud.google.com/apis/credentials
|
||||||
|
Anwendungstyp: Web
|
||||||
|
Name: 'BackendMailOAuth'
|
||||||
|
Redirect-Uri: 'http://localhost/oauth'
|
||||||
|
Client-ID und Client-Key merken
|
||||||
|
|
||||||
|
- Open in Browser:
|
||||||
|
https://accounts.google.com/o/oauth2/v2/auth?redirect_uri=http://localhost/oauth&prompt=consent&response_type=code&client_id={...}&scope=https://www.googleapis.com/auth/gmail.send&access_type=offline
|
||||||
|
Code aus redirected URI merken
|
||||||
|
|
||||||
|
- Code via request einlösen (und refresh_roken merken):
|
||||||
|
|
||||||
|
```
|
||||||
|
curl --request POST \
|
||||||
|
--url https://oauth2.googleapis.com/token \
|
||||||
|
--data code={...} \
|
||||||
|
--data redirect_uri=http://localhost/oauth \
|
||||||
|
--data client_id={...} \
|
||||||
|
--data client_secret={...} \
|
||||||
|
--data grant_type=authorization_code \
|
||||||
|
--data scope=https://www.googleapis.com/auth/gmail.send
|
||||||
|
```
|
||||||
|
|
||||||
|
- Fertig, mit `client_id`, `client_secret` und `refresh_token` kann das package benutzt werden
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
41
googleapi/attachment.go
Normal file
41
googleapi/attachment.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package googleapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MailAttachment struct {
|
||||||
|
IsInline bool
|
||||||
|
ContentType *string
|
||||||
|
Filename *string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a MailAttachment) dump() []string {
|
||||||
|
res := make([]string, 0, 4)
|
||||||
|
|
||||||
|
if a.ContentType != nil {
|
||||||
|
res = append(res, "Content-Type: "+*a.ContentType+"; charset=UTF-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, "Content-Transfer-Encoding: base64")
|
||||||
|
|
||||||
|
if a.IsInline {
|
||||||
|
if a.Filename != nil {
|
||||||
|
res = append(res, fmt.Sprintf("Content-Disposition: inline;filename=\"%s\"", *a.Filename))
|
||||||
|
} else {
|
||||||
|
res = append(res, "Content-Disposition: inline")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if a.Filename != nil {
|
||||||
|
res = append(res, fmt.Sprintf("Content-Disposition: attachment;filename=\"%s\"", *a.Filename))
|
||||||
|
} else {
|
||||||
|
res = append(res, "Content-Disposition: attachment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, base64.URLEncoding.EncodeToString(a.Data))
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
6
googleapi/body.go
Normal file
6
googleapi/body.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package googleapi
|
||||||
|
|
||||||
|
type MailBody struct {
|
||||||
|
Plain *string
|
||||||
|
HTML *string
|
||||||
|
}
|
83
googleapi/oAuth.go
Normal file
83
googleapi/oAuth.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package googleapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GoogleOAuth interface {
|
||||||
|
}
|
||||||
|
|
||||||
|
type oauth struct {
|
||||||
|
clientID string
|
||||||
|
clientSecret string
|
||||||
|
refreshToken string
|
||||||
|
|
||||||
|
lock sync.RWMutex
|
||||||
|
accessToken *string
|
||||||
|
expiryDate *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGoogleOAuth(clientid string, clientsecret, refreshtoken string) GoogleOAuth {
|
||||||
|
return &oauth{
|
||||||
|
clientID: clientid,
|
||||||
|
clientSecret: clientsecret,
|
||||||
|
refreshToken: refreshtoken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *oauth) AccessToken() (string, error) {
|
||||||
|
c.lock.RLock()
|
||||||
|
if c.accessToken != nil && c.expiryDate != nil && (*c.expiryDate).After(time.Now()) {
|
||||||
|
c.lock.RUnlock()
|
||||||
|
return *c.accessToken, nil // still valid
|
||||||
|
}
|
||||||
|
c.lock.RUnlock()
|
||||||
|
|
||||||
|
httpclient := http.Client{}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, "https://oauth2.googleapis.com/token", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.URL.Query().Add("client_id", c.clientID)
|
||||||
|
req.URL.Query().Add("client_secret", c.clientSecret)
|
||||||
|
req.URL.Query().Add("grant_type", "refresh_token")
|
||||||
|
req.URL.Query().Add("refresh_token", c.refreshToken)
|
||||||
|
|
||||||
|
reqStartTime := time.Now()
|
||||||
|
|
||||||
|
res, err := httpclient.Do(req)
|
||||||
|
|
||||||
|
type response struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var r response
|
||||||
|
|
||||||
|
data, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, &r)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.lock.Lock()
|
||||||
|
c.expiryDate = langext.Ptr(reqStartTime.Add(timeext.FromSeconds(r.ExpiresIn - 10)))
|
||||||
|
c.accessToken = langext.Ptr(r.AccessToken)
|
||||||
|
c.lock.Unlock()
|
||||||
|
|
||||||
|
return r.AccessToken, nil
|
||||||
|
}
|
127
googleapi/sendMail.go
Normal file
127
googleapi/sendMail.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package googleapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
|
"mime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *client) SendMail(from string, recipients []string, cc []string, bcc []string, subject string, body MailBody, attachments []MailAttachment) error {
|
||||||
|
|
||||||
|
data := make([]string, 0, 32)
|
||||||
|
|
||||||
|
data = append(data, "Date: "+time.Now().Format(time.RFC1123Z))
|
||||||
|
data = append(data, "From: "+mime.QEncoding.Encode("UTF-8", from))
|
||||||
|
data = append(data, "To: "+strings.Join(langext.ArrMap(recipients, func(v string) string { return mime.QEncoding.Encode("UTF-8", v) }), ", "))
|
||||||
|
if len(cc) > 0 {
|
||||||
|
data = append(data, "To: "+strings.Join(langext.ArrMap(cc, func(v string) string { return mime.QEncoding.Encode("UTF-8", v) }), ", "))
|
||||||
|
}
|
||||||
|
if len(bcc) > 0 {
|
||||||
|
data = append(data, "Bcc: "+strings.Join(langext.ArrMap(bcc, func(v string) string { return mime.QEncoding.Encode("UTF-8", v) }), ", "))
|
||||||
|
}
|
||||||
|
data = append(data, "Subject: "+mime.QEncoding.Encode("UTF-8", subject))
|
||||||
|
|
||||||
|
hasAttachments := len(attachments) > 0
|
||||||
|
hasInlineAttachments := langext.ArrAny(attachments, func(v MailAttachment) bool { return v.IsInline })
|
||||||
|
hasPlain := body.Plain != nil
|
||||||
|
hasHTML := body.HTML != nil
|
||||||
|
|
||||||
|
mixedBoundary := "--------------" + langext.MustRawHexUUID()
|
||||||
|
relatedBoundary := "--------------" + langext.MustRawHexUUID()
|
||||||
|
altBoundary := "--------------" + langext.MustRawHexUUID()
|
||||||
|
|
||||||
|
inlineAttachments := langext.ArrFilter(attachments, func(v MailAttachment) bool { return v.IsInline })
|
||||||
|
normalAttachments := langext.ArrFilter(attachments, func(v MailAttachment) bool { return !v.IsInline })
|
||||||
|
|
||||||
|
if hasInlineAttachments {
|
||||||
|
// "mixed+related"
|
||||||
|
|
||||||
|
data = append(data, "Content-Type: multipart/mixed; boundary="+mixedBoundary)
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, mixedBoundary)
|
||||||
|
|
||||||
|
data = append(data, "Content-Type: multipart/related; boundary="+relatedBoundary)
|
||||||
|
data = append(data, "")
|
||||||
|
|
||||||
|
data = append(data, c.dumpHTMLBody(body, hasInlineAttachments, hasAttachments, relatedBoundary, altBoundary)...)
|
||||||
|
data = append(data, "")
|
||||||
|
|
||||||
|
for i, attachment := range inlineAttachments {
|
||||||
|
data = append(data, relatedBoundary)
|
||||||
|
data = append(data, attachment.dump()...)
|
||||||
|
|
||||||
|
if i < len(inlineAttachments)-1 {
|
||||||
|
data = append(data, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, relatedBoundary)
|
||||||
|
|
||||||
|
for i, attachment := range normalAttachments {
|
||||||
|
data = append(data, mixedBoundary)
|
||||||
|
data = append(data, attachment.dump()...)
|
||||||
|
|
||||||
|
if i < len(inlineAttachments)-1 {
|
||||||
|
data = append(data, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, mixedBoundary)
|
||||||
|
|
||||||
|
} else if hasAttachments {
|
||||||
|
// "mixed"
|
||||||
|
|
||||||
|
//TODO https://github.dev/muratgozel/MIMEText/blob/master/src/MIMEMessage.ts
|
||||||
|
|
||||||
|
} else if hasPlain && hasHTML {
|
||||||
|
// "alternative"
|
||||||
|
|
||||||
|
//TODO https://github.dev/muratgozel/MIMEText/blob/master/src/MIMEMessage.ts
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// "plain"
|
||||||
|
|
||||||
|
//TODO https://github.dev/muratgozel/MIMEText/blob/master/src/MIMEMessage.ts
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) dumpHTMLBody(body MailBody, hasInlineAttachments bool, hasAttachments bool, boundary string, boundaryAlt string) []string {
|
||||||
|
|
||||||
|
data := make([]string, 0, 16)
|
||||||
|
|
||||||
|
if body.HTML != nil && body.Plain != nil && hasInlineAttachments {
|
||||||
|
data = append(data, boundary)
|
||||||
|
data = append(data, *body.HTML)
|
||||||
|
} else if body.HTML != nil && body.Plain != nil && hasAttachments {
|
||||||
|
data = append(data, boundary)
|
||||||
|
data = append(data, "Content-Type: multipart/alternative; boundary="+boundaryAlt)
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, boundaryAlt)
|
||||||
|
data = append(data, *body.Plain)
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, boundaryAlt)
|
||||||
|
data = append(data, *body.HTML)
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, boundaryAlt)
|
||||||
|
} else if body.HTML != nil && body.Plain != nil {
|
||||||
|
data = append(data, boundary)
|
||||||
|
data = append(data, *body.Plain)
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, boundary)
|
||||||
|
data = append(data, *body.HTML)
|
||||||
|
} else if body.HTML != nil {
|
||||||
|
data = append(data, boundary)
|
||||||
|
data = append(data, *body.HTML)
|
||||||
|
} else if body.Plain != nil {
|
||||||
|
data = append(data, boundary)
|
||||||
|
data = append(data, *body.Plain)
|
||||||
|
} else {
|
||||||
|
data = append(data, boundary)
|
||||||
|
data = append(data, "") // no content ?!?
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
14
googleapi/service.go
Normal file
14
googleapi/service.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package googleapi
|
||||||
|
|
||||||
|
type GoogleClient interface {
|
||||||
|
}
|
||||||
|
|
||||||
|
type client struct {
|
||||||
|
oauth GoogleOAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGoogleClient(oauth GoogleOAuth) GoogleClient {
|
||||||
|
return &client{
|
||||||
|
oauth: oauth,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user