Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(691)

Unified Diff: impl/memory/mail.go

Issue 1525923002: Implement Mail service. (Closed) Base URL: https://github.com/luci/gae.git@filter_user
Patch Set: add tests Created 5 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: impl/memory/mail.go
diff --git a/impl/memory/mail.go b/impl/memory/mail.go
new file mode 100644
index 0000000000000000000000000000000000000000..2dcea96535a837db3fb24b77049632f4fd4f22af
--- /dev/null
+++ b/impl/memory/mail.go
@@ -0,0 +1,203 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package memory
+
+import (
+ "fmt"
+ net_mail "net/mail"
+ "net/textproto"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/luci/gae/service/mail"
+ "github.com/luci/gae/service/user"
+ "golang.org/x/net/context"
+)
+
+type mailData struct {
+ sync.Mutex
+ queue []*mail.TestMessage
+ admins []string
+ adminsPlain []string
+}
+
+// mailImpl is a contextual pointer to the current mailData.
+type mailImpl struct {
+ data *mailData
+
+ c context.Context
+}
+
+var _ mail.Interface = (*mailImpl)(nil)
+
+// useMail adds a mail.Interface implementation to context, accessible
+// by mail.Get(c)
+func useMail(c context.Context) context.Context {
+ data := &mailData{
+ admins: []string{"admin@example.com"},
+ adminsPlain: []string{"admin@example.com"},
+ }
+
+ return mail.SetFactory(c, func(c context.Context) mail.Interface {
+ return &mailImpl{data, c}
+ })
+}
+
+func parseEmails(emails ...string) error {
+ for _, e := range emails {
+ if _, err := net_mail.ParseAddress(e); err != nil {
+ return fmt.Errorf("invalid email (%q): %s", e, err)
+ }
+ }
+ return nil
+}
+
+func checkMessage(msg *mail.TestMessage, adminsPlain []string, user string) error {
+ sender, err := net_mail.ParseAddress(msg.Sender)
+ if err != nil {
+ return fmt.Errorf("unparsable Sender address: %s: %s", msg.Sender, err)
+ }
+ senderOK := user != "" && sender.Address == user
+ if !senderOK {
+ for _, a := range adminsPlain {
+ if sender.Address == a {
+ senderOK = true
+ break
+ }
+ }
+ }
+ if !senderOK {
+ return fmt.Errorf("invalid Sender: %s", msg.Sender)
+ }
+
+ if len(msg.To) == 0 && len(msg.Cc) == 0 && len(msg.Bcc) == 0 {
+ return fmt.Errorf("one of To, Cc or Bcc must be non-empty")
+ }
+
+ if err := parseEmails(msg.To...); err != nil {
+ return err
+ }
+ if err := parseEmails(msg.Cc...); err != nil {
+ return err
+ }
+ if err := parseEmails(msg.Bcc...); err != nil {
+ return err
+ }
+
+ if len(msg.Body) == 0 && len(msg.HTMLBody) == 0 {
+ return fmt.Errorf("one of Body or HTMLBody must be non-empty")
+ }
+
+ if len(msg.Attachments) > 0 {
+ msg.MIMETypes = make([]string, len(msg.Attachments))
+ for i := range msg.Attachments {
+ n := msg.Attachments[i].Name
+ ext := strings.TrimLeft(strings.ToLower(filepath.Ext(n)), ".")
+ if badExtensions.Has(ext) {
+ return fmt.Errorf("illegal attachment extension for %q", n)
+ }
+ mimetype := extensionMapping[ext]
+ if mimetype == "" {
+ mimetype = "application/octet-stream"
+ }
+ msg.MIMETypes[i] = mimetype
+ }
+ }
+
+ fixKeys := map[string]string{}
+ for k := range msg.Headers {
+ canonK := textproto.CanonicalMIMEHeaderKey(k)
martiniss 2015/12/16 21:54:15 Is this the same way the app engine people do it?
iannucci 2015/12/17 00:01:07 yes, it's the same way that all mime implementatio
+ if !okHeaders.Has(canonK) {
+ return fmt.Errorf("disallowed header: %s", k)
+ }
+ if canonK != k {
+ fixKeys[k] = canonK
+ }
+ }
+ for k, canonK := range fixKeys {
+ vals := msg.Headers[k]
+ delete(msg.Headers, k)
+ msg.Headers[canonK] = vals
+ }
+
+ return nil
+}
+
+func (m *mailImpl) sendImpl(msg *mail.Message) error {
+ email := ""
+ userSvc := user.Get(m.c)
+ if u := userSvc.Current(); u != nil {
+ email = u.Email
+ }
+
+ m.data.Lock()
+ adminsPlain := m.data.adminsPlain[:]
+ m.data.Unlock()
+
+ testMsg := &mail.TestMessage{Message: *msg}
+
+ if err := checkMessage(testMsg, adminsPlain, email); err != nil {
+ return err
+ }
+ m.data.Lock()
+ m.data.queue = append(m.data.queue, testMsg)
+ m.data.Unlock()
+ return nil
+}
+
+func (m *mailImpl) Send(msg *mail.Message) error {
+ return m.sendImpl(msg.Copy())
+}
+
+func (m *mailImpl) SendToAdmins(msg *mail.Message) error {
+ msg = msg.Copy()
+ m.data.Lock()
+ ads := m.data.admins[:]
+ m.data.Unlock()
+
+ msg.To = make([]string, len(ads))
+ copy(msg.To, ads)
+
+ return m.sendImpl(msg)
+}
+
+func (m *mailImpl) Testable() mail.Testable {
+ return m
+}
+
+func (m *mailImpl) SetAdminEmails(emails ...string) {
+ adminsPlain := make([]string, len(emails))
+ for i, e := range emails {
+ adr, err := net_mail.ParseAddress(e)
+ if err != nil {
+ panic(fmt.Errorf("invalid email (%q): %s", e, err))
+ }
+ adminsPlain[i] = adr.Address
+ }
+
+ m.data.Lock()
+ m.data.admins = emails
+ m.data.adminsPlain = adminsPlain
+ m.data.Unlock()
+}
+
+func (m *mailImpl) SentMessages() []*mail.TestMessage {
+ m.data.Lock()
+ msgs := m.data.queue[:]
martiniss 2015/12/16 21:54:15 [:] means a new slice, but it's not a copy, so you
iannucci 2015/12/17 00:01:07 no, the queue is an replaceable slice of immutable
+ m.data.Unlock()
+
+ ret := make([]*mail.TestMessage, len(msgs))
+ for i, m := range msgs {
+ ret[i] = m.Copy()
+ }
+ return ret
+}
+
+func (m *mailImpl) Reset() {
+ m.data.Lock()
+ m.data.queue = nil
+ m.data.Unlock()
+}

Powered by Google App Engine
This is Rietveld 408576698