go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/impl/memory/mail.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package memory
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	net_mail "net/mail"
    21  	"net/textproto"
    22  	"path/filepath"
    23  	"strings"
    24  	"sync"
    25  
    26  	"go.chromium.org/luci/gae/service/mail"
    27  	"go.chromium.org/luci/gae/service/user"
    28  )
    29  
    30  type mailData struct {
    31  	sync.Mutex
    32  	queue       []*mail.TestMessage
    33  	admins      []string
    34  	adminsPlain []string
    35  }
    36  
    37  // mailImpl is a contextual pointer to the current mailData.
    38  type mailImpl struct {
    39  	context.Context
    40  
    41  	data *mailData
    42  }
    43  
    44  var _ mail.RawInterface = (*mailImpl)(nil)
    45  
    46  // useMail adds a mail.RawInterface implementation to context, accessible
    47  // by mail.Raw(c) or the exported mail methods.
    48  func useMail(c context.Context) context.Context {
    49  	data := &mailData{
    50  		admins:      []string{"admin@example.com"},
    51  		adminsPlain: []string{"admin@example.com"},
    52  	}
    53  
    54  	return mail.SetFactory(c, func(ic context.Context) mail.RawInterface {
    55  		return &mailImpl{ic, data}
    56  	})
    57  }
    58  
    59  func parseEmails(emails ...string) error {
    60  	for _, e := range emails {
    61  		if _, err := net_mail.ParseAddress(e); err != nil {
    62  			return fmt.Errorf("invalid email (%q): %s", e, err)
    63  		}
    64  	}
    65  	return nil
    66  }
    67  
    68  func checkMessage(msg *mail.TestMessage, adminsPlain []string, user string) error {
    69  	sender, err := net_mail.ParseAddress(msg.Sender)
    70  	if err != nil {
    71  		return fmt.Errorf("unparsable Sender address: %s: %s", msg.Sender, err)
    72  	}
    73  	senderOK := user != "" && sender.Address == user
    74  	if !senderOK {
    75  		for _, a := range adminsPlain {
    76  			if sender.Address == a {
    77  				senderOK = true
    78  				break
    79  			}
    80  		}
    81  	}
    82  	if !senderOK {
    83  		return fmt.Errorf("invalid Sender: %s", msg.Sender)
    84  	}
    85  
    86  	if len(msg.To) == 0 && len(msg.Cc) == 0 && len(msg.Bcc) == 0 {
    87  		return fmt.Errorf("one of To, Cc or Bcc must be non-empty")
    88  	}
    89  
    90  	if err := parseEmails(msg.To...); err != nil {
    91  		return err
    92  	}
    93  	if err := parseEmails(msg.Cc...); err != nil {
    94  		return err
    95  	}
    96  	if err := parseEmails(msg.Bcc...); err != nil {
    97  		return err
    98  	}
    99  
   100  	if len(msg.Body) == 0 && len(msg.HTMLBody) == 0 {
   101  		return fmt.Errorf("one of Body or HTMLBody must be non-empty")
   102  	}
   103  
   104  	if len(msg.Attachments) > 0 {
   105  		msg.MIMETypes = make([]string, len(msg.Attachments))
   106  		for i := range msg.Attachments {
   107  			n := msg.Attachments[i].Name
   108  			ext := strings.TrimLeft(strings.ToLower(filepath.Ext(n)), ".")
   109  			if badExtensions.Has(ext) {
   110  				return fmt.Errorf("illegal attachment extension for %q", n)
   111  			}
   112  			mimetype := extensionMapping[ext]
   113  			if mimetype == "" {
   114  				mimetype = "application/octet-stream"
   115  			}
   116  			msg.MIMETypes[i] = mimetype
   117  		}
   118  	}
   119  
   120  	fixKeys := map[string]string{}
   121  	for k := range msg.Headers {
   122  		canonK := textproto.CanonicalMIMEHeaderKey(k)
   123  		if !okHeaders.Has(canonK) {
   124  			return fmt.Errorf("disallowed header: %s", k)
   125  		}
   126  		if canonK != k {
   127  			fixKeys[k] = canonK
   128  		}
   129  	}
   130  	for k, canonK := range fixKeys {
   131  		vals := msg.Headers[k]
   132  		delete(msg.Headers, k)
   133  		msg.Headers[canonK] = vals
   134  	}
   135  
   136  	return nil
   137  }
   138  
   139  func (m *mailImpl) sendImpl(msg *mail.Message) error {
   140  	email := ""
   141  	if u := user.Current(m); u != nil {
   142  		email = u.Email
   143  	}
   144  
   145  	m.data.Lock()
   146  	adminsPlain := m.data.adminsPlain[:]
   147  	m.data.Unlock()
   148  
   149  	testMsg := &mail.TestMessage{Message: *msg}
   150  
   151  	if err := checkMessage(testMsg, adminsPlain, email); err != nil {
   152  		return err
   153  	}
   154  	m.data.Lock()
   155  	m.data.queue = append(m.data.queue, testMsg)
   156  	m.data.Unlock()
   157  	return nil
   158  }
   159  
   160  func (m *mailImpl) Send(msg *mail.Message) error {
   161  	return m.sendImpl(msg.Copy())
   162  }
   163  
   164  func (m *mailImpl) SendToAdmins(msg *mail.Message) error {
   165  	msg = msg.Copy()
   166  	m.data.Lock()
   167  	ads := m.data.admins[:]
   168  	m.data.Unlock()
   169  
   170  	msg.To = make([]string, len(ads))
   171  	copy(msg.To, ads)
   172  
   173  	return m.sendImpl(msg)
   174  }
   175  
   176  func (m *mailImpl) GetTestable() mail.Testable { return m }
   177  
   178  func (m *mailImpl) SetAdminEmails(emails ...string) {
   179  	adminsPlain := make([]string, len(emails))
   180  	for i, e := range emails {
   181  		adr, err := net_mail.ParseAddress(e)
   182  		if err != nil {
   183  			panic(fmt.Errorf("invalid email (%q): %s", e, err))
   184  		}
   185  		adminsPlain[i] = adr.Address
   186  	}
   187  
   188  	m.data.Lock()
   189  	m.data.admins = emails
   190  	m.data.adminsPlain = adminsPlain
   191  	m.data.Unlock()
   192  }
   193  
   194  func (m *mailImpl) SentMessages() []*mail.TestMessage {
   195  	m.data.Lock()
   196  	msgs := m.data.queue[:]
   197  	m.data.Unlock()
   198  
   199  	ret := make([]*mail.TestMessage, len(msgs))
   200  	for i, m := range msgs {
   201  		ret[i] = m.Copy()
   202  	}
   203  	return ret
   204  }
   205  
   206  func (m *mailImpl) Reset() {
   207  	m.data.Lock()
   208  	m.data.queue = nil
   209  	m.data.Unlock()
   210  }