| package email |
| |
| import ( |
| "bytes" |
| "encoding/base64" |
| "fmt" |
| "html/template" |
| "regexp" |
| "strings" |
| ttemplate "text/template" |
| |
| "go.skia.org/infra/go/auth" |
| "go.skia.org/infra/go/httputils" |
| "go.skia.org/infra/go/skerr" |
| "go.skia.org/infra/go/sklog" |
| gmail "google.golang.org/api/gmail/v1" |
| ) |
| |
| const ( |
| viewActionMarkupTemplate = ` |
| <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 = `From: {{.From}} |
| To: {{.To}} |
| Subject: {{.Subject}} |
| Content-Type: text/html; charset=UTF-8 |
| {{if .ThreadingReference}}References: {{.ThreadingReference}} |
| {{end}}{{if .ThreadingReference}}In-Reply-To: {{.ThreadingReference}} |
| {{end}}{{if .MessageID -}}Message-ID: {{.MessageID}} |
| {{end}} |
| <html> |
| <body> |
| {{.Markup}} |
| {{.Body}} |
| </body> |
| </html> |
| ` |
| ) |
| |
| var ( |
| 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 |
| |
| // From is the email address of the authenticated account. |
| from 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 "", skerr.Wrapf(err, "Could not execute template %s", name) |
| } |
| 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 |
| } |
| ret := &GMail{ |
| service: service, |
| from: "me", |
| } |
| if err := ret.populateFromAddress(); err != nil { |
| sklog.Errorf("Failed to determine sending accounts email address: %s", err) |
| } |
| return ret, nil |
| } |
| |
| // populateFromAddress fills in a.from with the email address for the |
| // authenticated account. |
| func (a *GMail) populateFromAddress() error { |
| profile, err := a.service.Users.GetProfile("me").Do() |
| if err != nil { |
| return skerr.Wrapf(err, "Failed to get profile.") |
| } |
| a.from = profile.EmailAddress |
| return nil |
| } |
| |
| // 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) |
| } |
| |
| // FormatAsRFC2822 returns a *bytes.Buffer that contains the email message |
| // formatted in RFC 2822 format. |
| func FormatAsRFC2822(fromDisplayName string, from string, to []string, subject, body, markup, threadingReference string, messageID string) (*bytes.Buffer, error) { |
| fromWithName := fmt.Sprintf("%s <%s>", fromDisplayName, from) |
| var msgBytes bytes.Buffer |
| if err := emailTemplateParsed.Execute(&msgBytes, struct { |
| From template.HTML |
| To string |
| Subject string |
| ThreadingReference string |
| Body template.HTML |
| Markup template.HTML |
| MessageID string |
| }{ |
| From: template.HTML(fromWithName), |
| To: strings.Join(to, ","), |
| Subject: subject, |
| ThreadingReference: threadingReference, |
| Body: template.HTML(body), |
| Markup: template.HTML(markup), |
| MessageID: messageID, |
| }); err != nil { |
| return nil, skerr.Wrapf(err, "Failed to format email.") |
| } |
| return &msgBytes, nil |
| } |
| |
| // SendWithMarkup sends 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(fromDisplayName string, to []string, subject, body, markup, threadingReference string) (string, error) { |
| msgBytes, err := FormatAsRFC2822(fromDisplayName, a.from, to, subject, body, markup, threadingReference, "") |
| if err != nil { |
| return "", skerr.Wrap(err) |
| } |
| sklog.Infof("Message to send: %q", msgBytes.String()) |
| return a.SendRFC2822Message(subject, msgBytes.Bytes()) |
| } |
| |
| // SendRFC2822Message sends the RFC2822 formatted email message in body with the |
| // given subject. |
| func (a *GMail) SendRFC2822Message(subject string, body []byte) (string, error) { |
| msg := gmail.Message{} |
| msg.SizeEstimate = int64(len(body)) |
| msg.Snippet = subject |
| msg.Raw = base64.URLEncoding.EncodeToString(body) |
| |
| m, err := a.service.Users.Messages.Send(a.from, &msg).Do() |
| if err != nil { |
| return "", skerr.Wrapf(err, "Failed to send email: %s", subject) |
| } |
| return m.Id, nil |
| } |
| |
| var fromRegex = regexp.MustCompile(`(?m)^From: (.*)$`) |
| var toRegex = regexp.MustCompile(`(?m)^To: (.*)$`) |
| var subjectRegex = regexp.MustCompile(`(?m)^Subject:(.*)$`) |
| var doubleNewLine = regexp.MustCompile(`\n\n`) |
| |
| const defaultSubject = "(no subject)" |
| |
| // ParseRFC2822Message returns the email address in the From:, To: and Subject: |
| // lines, and also returns the body of the message, which is presumed to be an |
| // HTML formatted email. |
| func ParseRFC2822Message(body []byte) (string, []string, string, string, error) { |
| // From: senderDisplayName <sender email> |
| // Subject: subject |
| // To: A Display Name <a@example.com>, B <b@example.org> |
| match := fromRegex.FindSubmatch(body) |
| if match == nil || len(match) < 2 { |
| return "", nil, "", "", skerr.Fmt("Failed to find a From: line in message.") |
| } |
| from := string(match[1]) |
| |
| match = subjectRegex.FindSubmatch(body) |
| subject := defaultSubject |
| if len(match) >= 2 { |
| subject = string(bytes.TrimSpace(match[1])) |
| } |
| |
| match = toRegex.FindSubmatch(body) |
| if match == nil || len(match) < 2 { |
| return "", nil, "", "", skerr.Fmt("Failed to find a To: line in message.") |
| } |
| to := []string{} |
| for _, addr := range bytes.Split(match[1], []byte(",")) { |
| toAsString := string(bytes.TrimSpace(addr)) |
| if toAsString != "" { |
| to = append(to, toAsString) |
| } |
| } |
| if len(to) < 1 { |
| return "", nil, "", "", skerr.Fmt("Failed to find any To: addresses.") |
| } |
| |
| parts := doubleNewLine.Split(string(body), 2) |
| if len(parts) != 2 { |
| return "", nil, "", "", skerr.Fmt("Failed to find the body of the message.") |
| } |
| messageBody := parts[1] |
| |
| return from, to, subject, messageBody, 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 "", skerr.Wrapf(err, "Failed to get message data for id %s", messageID) |
| } |
| reference := "" |
| for _, h := range m.Payload.Headers { |
| if h.Name == "Message-Id" { |
| reference = h.Value |
| break |
| } |
| } |
| if reference == "" { |
| return "", skerr.Wrapf(err, "Could not find \"Message-Id\" header for Message-Id %s", messageID) |
| } |
| return reference, nil |
| } |