blob: 3a4c035bd120be6df650b259b2c7d2081e95a3ee [file] [log] [blame]
package email
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
"io"
"io/ioutil"
"strings"
ttemplate "text/template"
"go.skia.org/infra/go/auth"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
gmail "google.golang.org/api/gmail/v1"
)
var (
viewActionMarkupTemplate string = `
<div itemscope itemtype="http://schema.org/EmailMessage">
<div itemprop="potentialAction" itemscope itemtype="http://schema.org/ViewAction">
<link itemprop="target" href="{{.Link}}"/>
<meta itemprop="name" content="{{.Name}}"/>
</div>
<meta itemprop="description" content="{{.Description}}"/>
</div>
`
emailTemplate string = `From: {{.From}}
To: {{.To}}
Subject: {{.Subject}}
Content-Type: text/html; charset=UTF-8
References: {{.ThreadingReference}}
In-Reply-To: {{.ThreadingReference}}
<html>
<body>
{{.Markup}}
{{.Body}}
</body>
</html>
`
viewActionMarkupTemplateParsed *template.Template = nil
emailTemplateParsed *ttemplate.Template = nil
)
func init() {
viewActionMarkupTemplateParsed = template.Must(template.New("view_action").Parse(viewActionMarkupTemplate))
emailTemplateParsed = ttemplate.Must(ttemplate.New("email").Parse(emailTemplate))
}
// GMail is an object used for authenticating to the GMail API server.
type GMail struct {
service *gmail.Service
}
// Message represents a single email message.
type Message struct {
SenderDisplayName string
To []string
Subject string
Body string
ThreadingReference string
}
// GetViewActionMarkup returns a string that contains the required markup.
func GetViewActionMarkup(link, name, description string) (string, error) {
markupBytes := new(bytes.Buffer)
if err := viewActionMarkupTemplateParsed.Execute(markupBytes, struct {
Link string
Name string
Description string
}{
Link: link,
Name: name,
Description: description,
}); err != nil {
return "", fmt.Errorf("Could not execute template: %v", err)
}
return markupBytes.String(), nil
}
// NewGMail returns a new GMail object which is authorized to send email.
func NewGMail(clientId, clientSecret, tokenCacheFile string) (*GMail, error) {
ts, err := auth.NewTokenSourceFromIdAndSecret(clientId, clientSecret, tokenCacheFile, gmail.GmailComposeScope)
if err != nil {
return nil, err
}
client := httputils.DefaultClientConfig().WithTokenSource(ts).With2xxOnly().Client()
service, err := gmail.New(client)
if err != nil {
return nil, err
}
return &GMail{
service: service,
}, nil
}
// ClientSecrets is the structure of a client_secrets.json file that contains info on an installed client.
type ClientSecrets struct {
Installed ClientConfig `json:"installed"`
}
type ClientConfig struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
}
// NewFromFiles creates a new GMail object authorized from the given files.
//
// Creates a copy of the token cache file in /tmp since mounted secrets are read-only.
func NewFromFiles(emailTokenCacheFile, emailClientSecretsFile string) (*GMail, error) {
var clientSecrets ClientSecrets
err := util.WithReadFile(emailClientSecretsFile, func(f io.Reader) error {
return json.NewDecoder(f).Decode(&clientSecrets)
})
if err != nil {
sklog.Fatalf("Failed to read client secrets from %q: %s", emailClientSecretsFile, err)
}
// Create a copy of the token cache file since mounted secrets are read-only.
fout, err := ioutil.TempFile("", "")
if err != nil {
sklog.Fatalf("Unable to create temp file %q: %s", fout.Name(), err)
}
err = util.WithReadFile(emailTokenCacheFile, func(fin io.Reader) error {
_, err := io.Copy(fout, fin)
if err != nil {
err = fout.Close()
}
return err
})
if err != nil {
sklog.Fatalf("Failed to write token cache file from %q to %q: %s", emailTokenCacheFile, fout.Name(), err)
}
emailTokenCacheFile = fout.Name()
return NewGMail(clientSecrets.Installed.ClientID, clientSecrets.Installed.ClientSecret, emailTokenCacheFile)
}
// Send an email. Returns the messageId of the sent email.
func (a *GMail) Send(senderDisplayName string, to []string, subject, body, threadingReference string) (string, error) {
return a.SendWithMarkup(senderDisplayName, to, subject, body, "", threadingReference)
}
// Send an email with gmail markup. Returns the messageId of the sent email.
// Documentation about markups supported in gmail are here: https://developers.google.com/gmail/markup/
// A go-to action example is here: https://developers.google.com/gmail/markup/reference/go-to-action
func (a *GMail) SendWithMarkup(senderDisplayName string, to []string, subject, body, markup, threadingReference string) (string, error) {
sender := "me"
// Get email address to use in the from section.
profile, err := a.service.Users.GetProfile(sender).Do()
if err != nil {
return "", fmt.Errorf("Failed to get profile for %s: %v", sender, err)
}
fromWithName := fmt.Sprintf("%s <%s>", senderDisplayName, profile.EmailAddress)
msgBytes := new(bytes.Buffer)
if err := emailTemplateParsed.Execute(msgBytes, struct {
From template.HTML
To string
Subject string
ThreadingReference string
Body template.HTML
Markup template.HTML
}{
From: template.HTML(fromWithName),
To: strings.Join(to, ","),
Subject: subject,
ThreadingReference: threadingReference,
Body: template.HTML(body),
Markup: template.HTML(markup),
}); err != nil {
return "", fmt.Errorf("Failed to send email; could not execute template: %v", err)
}
sklog.Infof("Message to send: %q", msgBytes.String())
msg := gmail.Message{}
msg.SizeEstimate = int64(msgBytes.Len())
msg.Snippet = subject
msg.Raw = base64.URLEncoding.EncodeToString(msgBytes.Bytes())
m, err := a.service.Users.Messages.Send(sender, &msg).Do()
if err != nil {
return "", fmt.Errorf("Failed to send email: %v", err)
}
return m.Id, nil
}
// GetThreadingReference returns the reference string that can be used to thread emails.
func (a *GMail) GetThreadingReference(messageId string) (string, error) {
// Get the reference from the response headers of messages.get call.
m, err := a.service.Users.Messages.Get("me", messageId).Do()
if err != nil {
return "", fmt.Errorf("Failed to get message data: %v", err)
}
reference := ""
for _, h := range m.Payload.Headers {
if h.Name == "Message-Id" {
reference = h.Value
break
}
}
if reference == "" {
return "", fmt.Errorf("Could not find \"Message-Id\" header for message Id %s", messageId)
}
return reference, nil
}
// SendMessage sends the given Message. Returns the messageId of the sent email.
func (a *GMail) SendMessage(msg *Message) (string, error) {
return a.Send(msg.SenderDisplayName, msg.To, msg.Subject, msg.Body, msg.ThreadingReference)
}