google mail API [[FIN]]
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m53s
Some checks failed
Build Docker and Deploy / Run goext test-suite (push) Failing after 2m53s
This commit is contained in:
parent
8f13eb2f16
commit
68f4261fcd
@ -59,6 +59,9 @@ var (
|
|||||||
TypeMarshalEntityID = NewType("MARSHAL_ENTITY_ID", langext.Ptr(400))
|
TypeMarshalEntityID = NewType("MARSHAL_ENTITY_ID", langext.Ptr(400))
|
||||||
TypeInvalidCSID = NewType("INVALID_CSID", langext.Ptr(400))
|
TypeInvalidCSID = NewType("INVALID_CSID", langext.Ptr(400))
|
||||||
|
|
||||||
|
TypeGoogleStatuscode = NewType("GOOGLE_STATUSCODE", langext.Ptr(400))
|
||||||
|
TypeGoogleResponse = NewType("GOOGLE_RESPONSE", langext.Ptr(400))
|
||||||
|
|
||||||
TypeUnauthorized = NewType("UNAUTHORIZED", langext.Ptr(401))
|
TypeUnauthorized = NewType("UNAUTHORIZED", langext.Ptr(401))
|
||||||
TypeAuthFailed = NewType("AUTH_FAILED", langext.Ptr(401))
|
TypeAuthFailed = NewType("AUTH_FAILED", langext.Ptr(401))
|
||||||
|
|
||||||
|
@ -7,35 +7,40 @@ import (
|
|||||||
|
|
||||||
type MailAttachment struct {
|
type MailAttachment struct {
|
||||||
IsInline bool
|
IsInline bool
|
||||||
ContentType *string
|
ContentType string
|
||||||
Filename *string
|
Filename string
|
||||||
Data []byte
|
Data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MailAttachment) dump() []string {
|
func (a MailAttachment) dump() []string {
|
||||||
res := make([]string, 0, 4)
|
res := make([]string, 0, 4)
|
||||||
|
|
||||||
if a.ContentType != nil {
|
if a.ContentType != "" {
|
||||||
res = append(res, "Content-Type: "+*a.ContentType+"; charset=UTF-8")
|
res = append(res, "Content-Type: "+a.ContentType+"; charset=UTF-8")
|
||||||
}
|
}
|
||||||
|
|
||||||
res = append(res, "Content-Transfer-Encoding: base64")
|
res = append(res, "Content-Transfer-Encoding: base64")
|
||||||
|
|
||||||
if a.IsInline {
|
if a.IsInline {
|
||||||
if a.Filename != nil {
|
if a.Filename != "" {
|
||||||
res = append(res, fmt.Sprintf("Content-Disposition: inline;filename=\"%s\"", *a.Filename))
|
res = append(res, fmt.Sprintf("Content-Disposition: inline;filename=\"%s\"", a.Filename))
|
||||||
} else {
|
} else {
|
||||||
res = append(res, "Content-Disposition: inline")
|
res = append(res, "Content-Disposition: inline")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if a.Filename != nil {
|
if a.Filename != "" {
|
||||||
res = append(res, fmt.Sprintf("Content-Disposition: attachment;filename=\"%s\"", *a.Filename))
|
res = append(res, fmt.Sprintf("Content-Disposition: attachment;filename=\"%s\"", a.Filename))
|
||||||
} else {
|
} else {
|
||||||
res = append(res, "Content-Disposition: attachment")
|
res = append(res, "Content-Disposition: attachment")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res = append(res, base64.URLEncoding.EncodeToString(a.Data))
|
b64 := base64.StdEncoding.EncodeToString(a.Data)
|
||||||
|
for i := 0; i < len(b64); i += 80 {
|
||||||
|
res = append(res, b64[i:min(i+80, len(b64))])
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package googleapi
|
package googleapi
|
||||||
|
|
||||||
type MailBody struct {
|
type MailBody struct {
|
||||||
Plain *string
|
Plain string
|
||||||
HTML *string
|
HTML string
|
||||||
}
|
}
|
||||||
|
224
googleapi/mimeMessage.go
Normal file
224
googleapi/mimeMessage.go
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
package googleapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
|
"mime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822
|
||||||
|
func encodeMimeMail(from string, recipients []string, cc []string, bcc []string, subject string, body MailBody, attachments []MailAttachment) string {
|
||||||
|
|
||||||
|
data := make([]string, 0, 32)
|
||||||
|
|
||||||
|
data = append(data, "Date: "+time.Now().Format(time.RFC1123Z))
|
||||||
|
data = append(data, "MIME-Version: 1.0")
|
||||||
|
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))
|
||||||
|
|
||||||
|
hasInlineAttachments := langext.ArrAny(attachments, func(v MailAttachment) bool { return v.IsInline })
|
||||||
|
hasNormalAttachments := langext.ArrAny(attachments, func(v MailAttachment) bool { return !v.IsInline })
|
||||||
|
hasPlain := body.Plain != ""
|
||||||
|
hasHTML := body.HTML != ""
|
||||||
|
|
||||||
|
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 && hasNormalAttachments {
|
||||||
|
// "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, dumpMailBody(body, hasInlineAttachments, hasNormalAttachments, 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(normalAttachments)-1 {
|
||||||
|
data = append(data, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, "--"+mixedBoundary+"--")
|
||||||
|
|
||||||
|
} else if hasNormalAttachments {
|
||||||
|
// "mixed"
|
||||||
|
|
||||||
|
data = append(data, "Content-Type: multipart/mixed; boundary="+mixedBoundary)
|
||||||
|
data = append(data, "")
|
||||||
|
|
||||||
|
data = append(data, dumpMailBody(body, hasInlineAttachments, hasNormalAttachments, mixedBoundary, altBoundary)...)
|
||||||
|
if hasPlain && hasHTML {
|
||||||
|
data = append(data, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, attachment := range normalAttachments {
|
||||||
|
data = append(data, "--"+mixedBoundary)
|
||||||
|
data = append(data, attachment.dump()...)
|
||||||
|
|
||||||
|
if i < len(normalAttachments)-1 {
|
||||||
|
data = append(data, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, "--"+mixedBoundary+"--")
|
||||||
|
|
||||||
|
} else if hasInlineAttachments {
|
||||||
|
// "related"
|
||||||
|
|
||||||
|
data = append(data, "Content-Type: multipart/related; boundary="+relatedBoundary)
|
||||||
|
data = append(data, "")
|
||||||
|
|
||||||
|
data = append(data, dumpMailBody(body, hasInlineAttachments, hasNormalAttachments, 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+"--")
|
||||||
|
|
||||||
|
} else if hasPlain && hasHTML {
|
||||||
|
// "alternative"
|
||||||
|
|
||||||
|
data = append(data, "Content-Type: multipart/alternative; boundary="+altBoundary)
|
||||||
|
data = append(data, "")
|
||||||
|
|
||||||
|
data = append(data, dumpMailBody(body, hasInlineAttachments, hasNormalAttachments, altBoundary, altBoundary)...)
|
||||||
|
data = append(data, "")
|
||||||
|
|
||||||
|
data = append(data, "--"+altBoundary+"--")
|
||||||
|
|
||||||
|
} else if hasPlain {
|
||||||
|
// "plain"
|
||||||
|
|
||||||
|
data = append(data, "Content-Type: text/plain; charset=UTF-8")
|
||||||
|
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, body.Plain)
|
||||||
|
|
||||||
|
} else if hasHTML {
|
||||||
|
// "plain"
|
||||||
|
|
||||||
|
data = append(data, "Content-Type: text/html; charset=UTF-8")
|
||||||
|
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, body.HTML)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// "empty??"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(data, "\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func dumpMailBody(body MailBody, hasInlineAttachments bool, hasNormalAttachments bool, boundary string, boundaryAlt string) []string {
|
||||||
|
|
||||||
|
if body.HTML != "" && body.Plain != "" && !hasInlineAttachments && hasNormalAttachments {
|
||||||
|
data := make([]string, 0, 16)
|
||||||
|
data = append(data, "--"+boundary)
|
||||||
|
data = append(data, "Content-Type: multipart/alternative; boundary="+boundaryAlt)
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, "--"+boundaryAlt)
|
||||||
|
data = append(data, "Content-Type: text/plain; charset=UTF-8")
|
||||||
|
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, body.Plain)
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, "--"+boundaryAlt)
|
||||||
|
data = append(data, "Content-Type: text/html; charset=UTF-8")
|
||||||
|
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, body.HTML)
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, "--"+boundaryAlt+"--")
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.HTML != "" && body.Plain != "" && hasInlineAttachments {
|
||||||
|
data := make([]string, 0, 2)
|
||||||
|
data = append(data, "--"+boundary)
|
||||||
|
data = append(data, body.HTML)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.HTML != "" && body.Plain != "" {
|
||||||
|
data := make([]string, 0, 8)
|
||||||
|
data = append(data, "--"+boundary)
|
||||||
|
data = append(data, "Content-Type: text/plain; charset=UTF-8")
|
||||||
|
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, body.Plain)
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, "--"+boundary)
|
||||||
|
data = append(data, "Content-Type: text/html; charset=UTF-8")
|
||||||
|
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, body.HTML)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.HTML != "" {
|
||||||
|
data := make([]string, 0, 2)
|
||||||
|
data = append(data, "--"+boundary)
|
||||||
|
data = append(data, "Content-Type: text/html; charset=UTF-8")
|
||||||
|
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, body.HTML)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.Plain != "" {
|
||||||
|
data := make([]string, 0, 2)
|
||||||
|
data = append(data, "--"+boundary)
|
||||||
|
data = append(data, "Content-Type: text/plain; charset=UTF-8")
|
||||||
|
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, body.Plain)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]string, 0, 16)
|
||||||
|
data = append(data, "--"+boundary)
|
||||||
|
data = append(data, "Content-Type: text/plain; charset=UTF-8")
|
||||||
|
data = append(data, "Content-Transfer-Encoding: 7bit")
|
||||||
|
data = append(data, "")
|
||||||
|
data = append(data, "") // no content ?!?
|
||||||
|
return data
|
||||||
|
}
|
77
googleapi/mimeMessage_test.go
Normal file
77
googleapi/mimeMessage_test.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package googleapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/tst"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncodeMimeMail(t *testing.T) {
|
||||||
|
|
||||||
|
mail := encodeMimeMail(
|
||||||
|
"noreply@heydyno.de",
|
||||||
|
[]string{"trash@mikescher.de"},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
"Hello Test Mail",
|
||||||
|
MailBody{Plain: "Plain Text"},
|
||||||
|
nil)
|
||||||
|
|
||||||
|
fmt.Printf("%s\n\n", mail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeMimeMail2(t *testing.T) {
|
||||||
|
|
||||||
|
mail := encodeMimeMail(
|
||||||
|
"noreply@heydyno.de",
|
||||||
|
[]string{"trash@mikescher.de"},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
"Hello Test Mail (alternative)",
|
||||||
|
MailBody{
|
||||||
|
Plain: "Plain Text",
|
||||||
|
HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>",
|
||||||
|
},
|
||||||
|
nil)
|
||||||
|
|
||||||
|
fmt.Printf("%s\n\n", mail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeMimeMail3(t *testing.T) {
|
||||||
|
|
||||||
|
mail := encodeMimeMail(
|
||||||
|
"noreply@heydyno.de",
|
||||||
|
[]string{"trash@mikescher.de"},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
"Hello Test Mail (alternative)",
|
||||||
|
MailBody{
|
||||||
|
HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>",
|
||||||
|
},
|
||||||
|
[]MailAttachment{
|
||||||
|
{Data: []byte("HelloWorld"), Filename: "test.txt", IsInline: false, ContentType: "text/plain"},
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Printf("%s\n\n", mail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeMimeMail4(t *testing.T) {
|
||||||
|
|
||||||
|
b := tst.Must(os.ReadFile("/home/mike/Pictures/Screenshot_20220706_190205.png"))(t)
|
||||||
|
|
||||||
|
mail := encodeMimeMail(
|
||||||
|
"noreply@heydyno.de",
|
||||||
|
[]string{"trash@mikescher.de"},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
"Hello Test Mail (inline)",
|
||||||
|
MailBody{
|
||||||
|
HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>",
|
||||||
|
},
|
||||||
|
[]MailAttachment{
|
||||||
|
{Data: b, Filename: "img.png", IsInline: true, ContentType: "image/png"},
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Printf("%s\n\n", mail)
|
||||||
|
}
|
@ -2,6 +2,8 @@ package googleapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
|
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
|
||||||
"io"
|
"io"
|
||||||
@ -11,6 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type GoogleOAuth interface {
|
type GoogleOAuth interface {
|
||||||
|
AccessToken() (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type oauth struct {
|
type oauth struct {
|
||||||
@ -41,16 +44,17 @@ func (c *oauth) AccessToken() (string, error) {
|
|||||||
|
|
||||||
httpclient := http.Client{}
|
httpclient := http.Client{}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, "https://oauth2.googleapis.com/token", nil)
|
url := fmt.Sprintf("https://oauth2.googleapis.com/token?client_id=%s&client_secret=%s&grant_type=%s&refresh_token=%s",
|
||||||
|
c.clientID,
|
||||||
|
c.clientSecret,
|
||||||
|
"refresh_token",
|
||||||
|
c.refreshToken)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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()
|
reqStartTime := time.Now()
|
||||||
|
|
||||||
res, err := httpclient.Do(req)
|
res, err := httpclient.Do(req)
|
||||||
@ -74,6 +78,10 @@ func (c *oauth) AccessToken() (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.ExpiresIn == 0 || r.AccessToken == "" {
|
||||||
|
return "", exerr.New(exerr.TypeGoogleResponse, "google oauth returned no response").Str("body", string(data)).Build()
|
||||||
|
}
|
||||||
|
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
c.expiryDate = langext.Ptr(reqStartTime.Add(timeext.FromSeconds(r.ExpiresIn - 10)))
|
c.expiryDate = langext.Ptr(reqStartTime.Add(timeext.FromSeconds(r.ExpiresIn - 10)))
|
||||||
c.accessToken = langext.Ptr(r.AccessToken)
|
c.accessToken = langext.Ptr(r.AccessToken)
|
||||||
|
@ -1,127 +1,69 @@
|
|||||||
package googleapi
|
package googleapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
"mime"
|
"io"
|
||||||
"strings"
|
"net/http"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *client) SendMail(from string, recipients []string, cc []string, bcc []string, subject string, body MailBody, attachments []MailAttachment) error {
|
type MailRef struct {
|
||||||
|
ID string `json:"id"`
|
||||||
data := make([]string, 0, 32)
|
ThreadID string `json:"threadId"`
|
||||||
|
LabelIDs []string `json:"labelIds"`
|
||||||
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 {
|
func (c *client) SendMail(ctx context.Context, from string, recipients []string, cc []string, bcc []string, subject string, body MailBody, attachments []MailAttachment) (MailRef, error) {
|
||||||
|
|
||||||
data := make([]string, 0, 16)
|
mm := encodeMimeMail(from, recipients, cc, bcc, subject, body, attachments)
|
||||||
|
|
||||||
if body.HTML != nil && body.Plain != nil && hasInlineAttachments {
|
tok, err := c.oauth.AccessToken()
|
||||||
data = append(data, boundary)
|
if err != nil {
|
||||||
data = append(data, *body.HTML)
|
return MailRef{}, exerr.Wrap(err, "").Build()
|
||||||
} 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
|
url := fmt.Sprintf("https://gmail.googleapis.com/gmail/v1/users/%s/messages/send?alt=json&prettyPrint=false", "me")
|
||||||
|
|
||||||
|
msgbody, err := json.Marshal(langext.H{"raw": base64.URLEncoding.EncodeToString([]byte(mm))})
|
||||||
|
if err != nil {
|
||||||
|
return MailRef{}, exerr.Wrap(err, "").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(msgbody))
|
||||||
|
if err != nil {
|
||||||
|
return MailRef{}, exerr.Wrap(err, "").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Authorization", "Bearer "+tok)
|
||||||
|
req.Header.Add("X-Goog-Api-Client", "blackforestbytes-goext/"+goext.GoextVersion)
|
||||||
|
req.Header.Add("User-Agent", "blackforestbytes-goext/"+goext.GoextVersion)
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.http.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return MailRef{}, exerr.Wrap(err, "").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return MailRef{}, exerr.Wrap(err, "").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return MailRef{}, exerr.New(exerr.TypeGoogleStatuscode, "gmail returned non-200 statuscode").Int("sc", resp.StatusCode).Str("body", string(respBody)).Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
var respObj MailRef
|
||||||
|
err = json.Unmarshal(respBody, &respObj)
|
||||||
|
if err != nil {
|
||||||
|
return MailRef{}, exerr.Wrap(err, "").Str("body", string(respBody)).Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
return respObj, nil
|
||||||
}
|
}
|
||||||
|
139
googleapi/sendMail_test.go
Normal file
139
googleapi/sendMail_test.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package googleapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/tst"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
if !exerr.Initialized() {
|
||||||
|
exerr.Init(exerr.ErrorPackageConfigInit{ZeroLogErrTraces: langext.PFalse, ZeroLogAllTraces: langext.PFalse})
|
||||||
|
}
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendMail1(t *testing.T) {
|
||||||
|
auth := NewGoogleOAuth(
|
||||||
|
"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com",
|
||||||
|
"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",
|
||||||
|
"1//09rEjBe2Ua7SuCgYIARAAGAkSNwF-L9IrIRPdlM4Zb8w8gN4ccZX4srDZmiWtxxDH1kC5c_ApdaM3jK0fJ4eZFRhRt2vvHqiNs4g")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
gclient := NewGoogleClient(auth)
|
||||||
|
|
||||||
|
mail, err := gclient.SendMail(
|
||||||
|
ctx,
|
||||||
|
"noreply@heydyno.de",
|
||||||
|
[]string{"trash@mikescher.de"},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
"Hello Test Mail",
|
||||||
|
MailBody{Plain: "Plain Text"},
|
||||||
|
nil)
|
||||||
|
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
fmt.Printf("mail.ID := %s\n", mail.ID)
|
||||||
|
fmt.Printf("mail.ThreadID := %s\n", mail.ThreadID)
|
||||||
|
fmt.Printf("mail.LabelIDs := %v\n", mail.LabelIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendMail2(t *testing.T) {
|
||||||
|
auth := NewGoogleOAuth(
|
||||||
|
"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com",
|
||||||
|
"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",
|
||||||
|
"1//09rEjBe2Ua7SuCgYIARAAGAkSNwF-L9IrIRPdlM4Zb8w8gN4ccZX4srDZmiWtxxDH1kC5c_ApdaM3jK0fJ4eZFRhRt2vvHqiNs4g")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
gclient := NewGoogleClient(auth)
|
||||||
|
|
||||||
|
mail, err := gclient.SendMail(
|
||||||
|
ctx,
|
||||||
|
"noreply@heydyno.de",
|
||||||
|
[]string{"trash@mikescher.de"},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
"Hello Test Mail (alternative)",
|
||||||
|
MailBody{
|
||||||
|
Plain: "Plain Text",
|
||||||
|
HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>",
|
||||||
|
},
|
||||||
|
nil)
|
||||||
|
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
fmt.Printf("mail.ID := %s\n", mail.ID)
|
||||||
|
fmt.Printf("mail.ThreadID := %s\n", mail.ThreadID)
|
||||||
|
fmt.Printf("mail.LabelIDs := %v\n", mail.LabelIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendMail3(t *testing.T) {
|
||||||
|
auth := NewGoogleOAuth(
|
||||||
|
"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com",
|
||||||
|
"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",
|
||||||
|
"1//09rEjBe2Ua7SuCgYIARAAGAkSNwF-L9IrIRPdlM4Zb8w8gN4ccZX4srDZmiWtxxDH1kC5c_ApdaM3jK0fJ4eZFRhRt2vvHqiNs4g")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
gclient := NewGoogleClient(auth)
|
||||||
|
|
||||||
|
mail, err := gclient.SendMail(
|
||||||
|
ctx,
|
||||||
|
"noreply@heydyno.de",
|
||||||
|
[]string{"trash@mikescher.de"},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
"Hello Test Mail (attach)",
|
||||||
|
MailBody{
|
||||||
|
HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>",
|
||||||
|
},
|
||||||
|
[]MailAttachment{
|
||||||
|
{Data: []byte("HelloWorld"), Filename: "test.txt", IsInline: false, ContentType: "text/plain"},
|
||||||
|
})
|
||||||
|
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
fmt.Printf("mail.ID := %s\n", mail.ID)
|
||||||
|
fmt.Printf("mail.ThreadID := %s\n", mail.ThreadID)
|
||||||
|
fmt.Printf("mail.LabelIDs := %v\n", mail.LabelIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendMail4(t *testing.T) {
|
||||||
|
auth := NewGoogleOAuth(
|
||||||
|
"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com",
|
||||||
|
"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",
|
||||||
|
"1//09rEjBe2Ua7SuCgYIARAAGAkSNwF-L9IrIRPdlM4Zb8w8gN4ccZX4srDZmiWtxxDH1kC5c_ApdaM3jK0fJ4eZFRhRt2vvHqiNs4g")
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
gclient := NewGoogleClient(auth)
|
||||||
|
|
||||||
|
b := tst.Must(os.ReadFile("/home/mike/Pictures/Screenshot_20220706_190205.png"))(t)
|
||||||
|
|
||||||
|
mail, err := gclient.SendMail(
|
||||||
|
ctx,
|
||||||
|
"noreply@heydyno.de",
|
||||||
|
[]string{"trash@mikescher.de"},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
"Hello Test Mail (inline)",
|
||||||
|
MailBody{
|
||||||
|
HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>",
|
||||||
|
},
|
||||||
|
[]MailAttachment{
|
||||||
|
{Data: b, Filename: "img.png", IsInline: true, ContentType: "image/png"},
|
||||||
|
})
|
||||||
|
|
||||||
|
tst.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
fmt.Printf("mail.ID := %s\n", mail.ID)
|
||||||
|
fmt.Printf("mail.ThreadID := %s\n", mail.ThreadID)
|
||||||
|
fmt.Printf("mail.LabelIDs := %v\n", mail.LabelIDs)
|
||||||
|
}
|
@ -1,14 +1,22 @@
|
|||||||
package googleapi
|
package googleapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
type GoogleClient interface {
|
type GoogleClient interface {
|
||||||
|
SendMail(ctx context.Context, from string, recipients []string, cc []string, bcc []string, subject string, body MailBody, attachments []MailAttachment) (MailRef, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
oauth GoogleOAuth
|
oauth GoogleOAuth
|
||||||
|
http http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGoogleClient(oauth GoogleOAuth) GoogleClient {
|
func NewGoogleClient(oauth GoogleOAuth) GoogleClient {
|
||||||
return &client{
|
return &client{
|
||||||
oauth: oauth,
|
oauth: oauth,
|
||||||
|
http: http.Client{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user