Chromium Code Reviews| 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() |
| +} |