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 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!", + ) +} +```