| 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)
|
| + 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[:]
|
| + 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()
|
| +}
|
|
|