From 51708e2bcb7e421ffd94ed733629af22bf42c5b8 Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Sun, 5 Apr 2026 03:06:23 -0700 Subject: [PATCH 1/2] feat(service): add Gmail service --- service/gmail/doc.go | 40 +++++++++++++++++++++++ service/gmail/gmail.go | 74 ++++++++++++++++++++++++++++++++++++++++++ service/gmail/usage.md | 56 ++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 service/gmail/doc.go create mode 100644 service/gmail/gmail.go create mode 100644 service/gmail/usage.md diff --git a/service/gmail/doc.go b/service/gmail/doc.go new file mode 100644 index 000000000..b82ce2b13 --- /dev/null +++ b/service/gmail/doc.go @@ -0,0 +1,40 @@ +/* +Package gmail implements a Gmail notifier, allowing plain text emails to be sent +to multiple recipients through the Gmail API using OAuth2 credentials. + +Usage: + + package main + + import ( + "context" + "log" + + "github.com/nikoksr/notify" + "github.com/nikoksr/notify/service/gmail" + "golang.org/x/oauth2" + ) + + func main() { + notifier := notify.New() + + token := &oauth2.Token{AccessToken: "ACCESS_TOKEN"} + tokenSource := oauth2.StaticTokenSource(token) + + gmailService, err := gmail.New(tokenSource, "sender@example.com") + if err != nil { + log.Fatal(err) + } + + gmailService.AddReceivers("alice@example.com", "bob@example.com") + + notifier.UseServices(gmailService) + + _ = notifier.Send( + context.Background(), + "Hello!", + "I am a bot written in Go!", + ) + } +*/ +package gmail diff --git a/service/gmail/gmail.go b/service/gmail/gmail.go new file mode 100644 index 000000000..7f20ed8f2 --- /dev/null +++ b/service/gmail/gmail.go @@ -0,0 +1,74 @@ +package gmail + +import ( + "context" + "encoding/base64" + "fmt" + "net/mail" + "strings" + + "golang.org/x/oauth2" + googleGmail "google.golang.org/api/gmail/v1" + "google.golang.org/api/option" +) + +// Gmail struct holds necessary data to communicate with the Gmail API. +type Gmail struct { + client *googleGmail.Service + senderAddr string + recipients []string +} + +// New returns a new instance of a Gmail notification service. +// tokenSource provides OAuth2 credentials for the Gmail API. +// senderAddr is the email address to send from (typically the authenticated user's email). +func New(tokenSource oauth2.TokenSource, senderAddr string) (*Gmail, error) { + srv, err := googleGmail.NewService(context.Background(), option.WithTokenSource(tokenSource)) + if err != nil { + return nil, fmt.Errorf("create gmail service: %w", err) + } + + return &Gmail{ + client: srv, + senderAddr: senderAddr, + recipients: []string{}, + }, nil +} + +// AddReceivers takes email addresses and adds them to the internal recipient list. +func (g *Gmail) AddReceivers(emails ...string) { + g.recipients = append(g.recipients, emails...) +} + +// Send takes a message subject and body and sends them to all recipients via Gmail API. +func (g Gmail) Send(ctx context.Context, subject, message string) error { + for _, recipient := range g.recipients { + select { + case <-ctx.Done(): + return ctx.Err() + default: + from := mail.Address{Address: g.senderAddr} + to := mail.Address{Address: recipient} + + header := fmt.Sprintf( + "From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=\"UTF-8\"\r\n\r\n%s", + from.String(), + to.String(), + subject, + message, + ) + + raw := base64.URLEncoding.EncodeToString([]byte(header)) + // Gmail API uses URL-safe base64 without padding. + raw = strings.TrimRight(raw, "=") + + gmailMessage := &googleGmail.Message{Raw: raw} + _, err := g.client.Users.Messages.Send("me", gmailMessage).Context(ctx).Do() + if err != nil { + return fmt.Errorf("send email to %q: %w", recipient, err) + } + } + } + + return nil +} diff --git a/service/gmail/usage.md b/service/gmail/usage.md new file mode 100644 index 000000000..3ea55072f --- /dev/null +++ b/service/gmail/usage.md @@ -0,0 +1,56 @@ +# Gmail Usage + +Ensure that you have already navigated to your GOPATH and installed the following packages: + +* `go get -u github.com/nikoksr/notify` +* `go get golang.org/x/oauth2` +* `go get google.golang.org/api/gmail/v1` + +## Steps for Gmail API + +These are general and very high level instructions + +1. Create or select a Google Cloud project +2. Enable the Gmail API for that project +3. Configure an OAuth consent screen +4. Create OAuth 2.0 credentials for your application +5. Request a token with a Gmail sending scope such as `https://www.googleapis.com/auth/gmail.send` +6. Use the authenticated email address as the sender address +7. Now you should be good to use the code below + +## Sample Code + +```go +package main + +import ( + "context" + "log" + + "github.com/nikoksr/notify" + "github.com/nikoksr/notify/service/gmail" + "golang.org/x/oauth2" +) + +func main() { + notifier := notify.New() + + token := &oauth2.Token{AccessToken: "ACCESS_TOKEN"} + tokenSource := oauth2.StaticTokenSource(token) + + gmailService, err := gmail.New(tokenSource, "sender@example.com") + if err != nil { + log.Fatal(err) + } + + gmailService.AddReceivers("alice@example.com", "bob@example.com") + + notifier.UseServices(gmailService) + + _ = notifier.Send( + context.Background(), + "Hello!", + "I am a bot written in Go!", + ) +} +``` From fb322c99b2e97edb90cf228f4bcc394cdfcb9cd0 Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Mon, 6 Apr 2026 09:39:12 -0700 Subject: [PATCH 2/2] fix: keep go 1.24.0 and google.golang.org/api v0.265.0 for CI compatibility The go.mod was bumped to go 1.25.0 with google.golang.org/api v0.274.0 during local development (Go 1.26.1), but the CI linter (golangci-lint v2.7.2, built with Go 1.25) panics when encountering source files requiring go1.26 in the transitive dependency tree. Reverting to go 1.24.0 with API v0.265.0 (matching upstream) resolves the linter panic while keeping the Gmail service fully functional. --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 0ee026a6b..0e9952e42 100644 --- a/go.mod +++ b/go.mod @@ -149,7 +149,7 @@ require ( github.com/ttacon/libphonenumber v1.2.1 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/net v0.49.0 // indirect - golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/oauth2 v0.34.0 golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect google.golang.org/protobuf v1.36.11 // indirect