Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
1c143921e6
|
|||
68e63a9cf6
|
|||
c3162fec95
|
|||
1124aa781a
|
|||
eef0e9f2aa
|
|||
af38b06d22
|
|||
2fad6340c7
|
|||
03aa0a2282
|
|||
358c238f3d
|
|||
d65ac8ba2b
|
|||
55d02b8c65
|
|||
8f13eb2f16
|
@@ -37,7 +37,7 @@ var rexEnumPackage = rext.W(regexp.MustCompile(`^package\s+(?P<name>[A-Za-z0-9_]
|
|||||||
|
|
||||||
var rexEnumDef = rext.W(regexp.MustCompile(`^\s*type\s+(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*//\s*(@enum:type).*$`))
|
var rexEnumDef = rext.W(regexp.MustCompile(`^\s*type\s+(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*//\s*(@enum:type).*$`))
|
||||||
|
|
||||||
var rexEnumValueDef = rext.W(regexp.MustCompile(`^\s*(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*=\s*(?P<value>("[A-Za-z0-9_:\s\-]+"|[0-9]+))\s*(//(?P<descr>.*))?.*$`))
|
var rexEnumValueDef = rext.W(regexp.MustCompile(`^\s*(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*=\s*(?P<value>("[A-Za-z0-9_:\s\-.]+"|[0-9]+))\s*(//(?P<descr>.*))?.*$`))
|
||||||
|
|
||||||
var rexEnumChecksumConst = rext.W(regexp.MustCompile(`const ChecksumEnumGenerator = "(?P<cs>[A-Za-z0-9_]*)"`))
|
var rexEnumChecksumConst = rext.W(regexp.MustCompile(`const ChecksumEnumGenerator = "(?P<cs>[A-Za-z0-9_]*)"`))
|
||||||
|
|
||||||
|
@@ -75,6 +75,12 @@ func (e {{.EnumTypeName}}) Meta() enums.EnumMetaValue {
|
|||||||
{{if $hasDescr}} return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: langext.Ptr(e.Description())} {{else}} return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil} {{end}}
|
{{if $hasDescr}} return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: langext.Ptr(e.Description())} {{else}} return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil} {{end}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{{if $hasDescr}}
|
||||||
|
func (e {{.EnumTypeName}}) DescriptionMeta() enums.EnumDescriptionMetaValue {
|
||||||
|
return enums.EnumDescriptionMetaValue{VarName: e.VarName(), Value: e, Description: e.Description()}
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
func Parse{{.EnumTypeName}}(vv string) ({{.EnumTypeName}}, bool) {
|
func Parse{{.EnumTypeName}}(vv string) ({{.EnumTypeName}}, bool) {
|
||||||
for _, ev := range __{{.EnumTypeName}}Values {
|
for _, ev := range __{{.EnumTypeName}}Values {
|
||||||
if string(ev) == vv {
|
if string(ev) == vv {
|
||||||
@@ -94,4 +100,12 @@ func {{.EnumTypeName}}ValuesMeta() []enums.EnumMetaValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{{if $hasDescr}}
|
||||||
|
func {{.EnumTypeName}}ValuesDescriptionMeta() []enums.EnumDescriptionMetaValue {
|
||||||
|
return []enums.EnumDescriptionMetaValue{ {{range .Values}}
|
||||||
|
{{.VarName}}.DescriptionMeta(), {{end}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{end}}
|
{{end}}
|
@@ -15,10 +15,17 @@ type StringEnum interface {
|
|||||||
type DescriptionEnum interface {
|
type DescriptionEnum interface {
|
||||||
Enum
|
Enum
|
||||||
Description() string
|
Description() string
|
||||||
|
DescriptionMeta() EnumDescriptionMetaValue
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnumMetaValue struct {
|
type EnumMetaValue struct {
|
||||||
VarName string `json:"varName"`
|
VarName string `json:"varName"`
|
||||||
Value any `json:"value"`
|
Value Enum `json:"value"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EnumDescriptionMetaValue struct {
|
||||||
|
VarName string `json:"varName"`
|
||||||
|
Value Enum `json:"value"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
@@ -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))
|
||||||
|
|
||||||
|
@@ -6,8 +6,10 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
|
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/rext"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -126,8 +128,8 @@ func (w *GinWrapper) DebugPrintRoutes() {
|
|||||||
line := [4]string{
|
line := [4]string{
|
||||||
spec.Method,
|
spec.Method,
|
||||||
spec.URL,
|
spec.URL,
|
||||||
strings.Join(spec.Middlewares, " -> "),
|
strings.Join(langext.ArrMap(spec.Middlewares, w.cleanMiddlewareName), " -> "),
|
||||||
spec.Handler,
|
w.cleanMiddlewareName(spec.Handler),
|
||||||
}
|
}
|
||||||
|
|
||||||
lines = append(lines, line)
|
lines = append(lines, line)
|
||||||
@@ -138,12 +140,38 @@ func (w *GinWrapper) DebugPrintRoutes() {
|
|||||||
pad[3] = mathext.Max(pad[3], len(line[3]))
|
pad[3] = mathext.Max(pad[3], len(line[3]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Gin-Routes:")
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
|
|
||||||
fmt.Printf("Gin-Route: %s %s --> %s --> %s\n",
|
fmt.Printf(" %s %s --> %s --> %s\n",
|
||||||
langext.StrPadRight("["+line[0]+"]", " ", pad[0]+2),
|
langext.StrPadRight("["+line[0]+"]", " ", pad[0]+2),
|
||||||
langext.StrPadRight(line[1], " ", pad[1]),
|
langext.StrPadRight(line[1], " ", pad[1]),
|
||||||
langext.StrPadRight(line[2], " ", pad[2]),
|
langext.StrPadRight(line[2], " ", pad[2]),
|
||||||
langext.StrPadRight(line[3], " ", pad[3]))
|
langext.StrPadRight(line[3], " ", pad[3]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *GinWrapper) cleanMiddlewareName(fname string) string {
|
||||||
|
|
||||||
|
funcSuffix := rext.W(regexp.MustCompile(`\.func[0-9]+(?:\.[0-9]+)*$`))
|
||||||
|
if match, ok := funcSuffix.MatchFirst(fname); ok {
|
||||||
|
fname = fname[:len(fname)-match.FullMatch().Length()]
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(fname, ".(*GinRoutesWrapper).WithJSONFilter") {
|
||||||
|
fname = "[JSONFilter]"
|
||||||
|
}
|
||||||
|
|
||||||
|
if fname == "ginext.BodyBuffer" {
|
||||||
|
fname = "[BodyBuffer]"
|
||||||
|
}
|
||||||
|
|
||||||
|
skipPrefixes := []string{"api.(*Handler).", "api.", "ginext.", "handler.", "admin-app.", "employee-app.", "employer-app."}
|
||||||
|
for _, pfx := range skipPrefixes {
|
||||||
|
if strings.HasPrefix(fname, pfx) {
|
||||||
|
fname = fname[len(pfx):]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fname
|
||||||
|
}
|
||||||
|
@@ -3,11 +3,9 @@ package ginext
|
|||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/rext"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -196,12 +194,6 @@ func nameOfFunction(f any) string {
|
|||||||
fname = fname[:len(fname)-len("-fm")]
|
fname = fname[:len(fname)-len("-fm")]
|
||||||
}
|
}
|
||||||
|
|
||||||
suffix := rext.W(regexp.MustCompile(`\.func[0-9]+(?:\.[0-9]+)*$`))
|
|
||||||
|
|
||||||
if match, ok := suffix.MatchFirst(fname); ok {
|
|
||||||
fname = fname[:len(fname)-match.FullMatch().Length()]
|
|
||||||
}
|
|
||||||
|
|
||||||
return fname
|
return fname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
go.mod
2
go.mod
@@ -7,7 +7,7 @@ require (
|
|||||||
github.com/jmoiron/sqlx v1.3.5
|
github.com/jmoiron/sqlx v1.3.5
|
||||||
github.com/rs/xid v1.5.0
|
github.com/rs/xid v1.5.0
|
||||||
github.com/rs/zerolog v1.31.0
|
github.com/rs/zerolog v1.31.0
|
||||||
go.mongodb.org/mongo-driver v1.13.0
|
go.mongodb.org/mongo-driver v1.13.1
|
||||||
golang.org/x/crypto v0.16.0
|
golang.org/x/crypto v0.16.0
|
||||||
golang.org/x/sys v0.15.0
|
golang.org/x/sys v0.15.0
|
||||||
golang.org/x/term v0.15.0
|
golang.org/x/term v0.15.0
|
||||||
|
2
go.sum
2
go.sum
@@ -126,6 +126,8 @@ go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecq
|
|||||||
go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
|
go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
|
||||||
go.mongodb.org/mongo-driver v1.13.0 h1:67DgFFjYOCMWdtTEmKFpV3ffWlFnh+CYZ8ZS/tXWUfY=
|
go.mongodb.org/mongo-driver v1.13.0 h1:67DgFFjYOCMWdtTEmKFpV3ffWlFnh+CYZ8ZS/tXWUfY=
|
||||||
go.mongodb.org/mongo-driver v1.13.0/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
|
go.mongodb.org/mongo-driver v1.13.0/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
|
||||||
|
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
|
||||||
|
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
|
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
|
||||||
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
package goext
|
package goext
|
||||||
|
|
||||||
const GoextVersion = "0.0.327"
|
const GoextVersion = "0.0.340"
|
||||||
|
|
||||||
const GoextVersionTimestamp = "2023-12-02T13:15:19+0100"
|
const GoextVersionTimestamp = "2023-12-07T14:42:25+0100"
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
46
googleapi/attachment.go
Normal file
46
googleapi/attachment.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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 != "" {
|
||||||
|
res = append(res, "Content-Type: "+a.ContentType+"; charset=UTF-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, "Content-Transfer-Encoding: base64")
|
||||||
|
|
||||||
|
if a.IsInline {
|
||||||
|
if a.Filename != "" {
|
||||||
|
res = append(res, fmt.Sprintf("Content-Disposition: inline;filename=\"%s\"", a.Filename))
|
||||||
|
} else {
|
||||||
|
res = append(res, "Content-Disposition: inline")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if a.Filename != "" {
|
||||||
|
res = append(res, fmt.Sprintf("Content-Disposition: attachment;filename=\"%s\"", a.Filename))
|
||||||
|
} else {
|
||||||
|
res = append(res, "Content-Disposition: attachment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
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
|
||||||
|
}
|
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)
|
||||||
|
}
|
91
googleapi/oAuth.go
Normal file
91
googleapi/oAuth.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package googleapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/exerr"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GoogleOAuth interface {
|
||||||
|
AccessToken() (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
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{}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.expiryDate = langext.Ptr(reqStartTime.Add(timeext.FromSeconds(r.ExpiresIn - 10)))
|
||||||
|
c.accessToken = langext.Ptr(r.AccessToken)
|
||||||
|
c.lock.Unlock()
|
||||||
|
|
||||||
|
return r.AccessToken, nil
|
||||||
|
}
|
69
googleapi/sendMail.go
Normal file
69
googleapi/sendMail.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package googleapi
|
||||||
|
|
||||||
|
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"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MailRef struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ThreadID string `json:"threadId"`
|
||||||
|
LabelIDs []string `json:"labelIds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) SendMail(ctx context.Context, from string, recipients []string, cc []string, bcc []string, subject string, body MailBody, attachments []MailAttachment) (MailRef, error) {
|
||||||
|
|
||||||
|
mm := encodeMimeMail(from, recipients, cc, bcc, subject, body, attachments)
|
||||||
|
|
||||||
|
tok, err := c.oauth.AccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return MailRef{}, exerr.Wrap(err, "").Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
151
googleapi/sendMail_test.go
Normal file
151
googleapi/sendMail_test.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
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) {
|
||||||
|
t.Skip()
|
||||||
|
return
|
||||||
|
|
||||||
|
auth := NewGoogleOAuth(
|
||||||
|
"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com",
|
||||||
|
"TODO",
|
||||||
|
"TODO")
|
||||||
|
|
||||||
|
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) {
|
||||||
|
t.Skip()
|
||||||
|
return
|
||||||
|
|
||||||
|
auth := NewGoogleOAuth(
|
||||||
|
"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com",
|
||||||
|
"TODO",
|
||||||
|
"TODO")
|
||||||
|
|
||||||
|
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) {
|
||||||
|
t.Skip()
|
||||||
|
return
|
||||||
|
|
||||||
|
auth := NewGoogleOAuth(
|
||||||
|
"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com",
|
||||||
|
"TODO",
|
||||||
|
"TODO")
|
||||||
|
|
||||||
|
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) {
|
||||||
|
t.Skip()
|
||||||
|
return
|
||||||
|
|
||||||
|
auth := NewGoogleOAuth(
|
||||||
|
"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com",
|
||||||
|
"TODO",
|
||||||
|
"TODO")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
22
googleapi/service.go
Normal file
22
googleapi/service.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package googleapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
oauth GoogleOAuth
|
||||||
|
http http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGoogleClient(oauth GoogleOAuth) GoogleClient {
|
||||||
|
return &client{
|
||||||
|
oauth: oauth,
|
||||||
|
http: http.Client{},
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user