github.com/volatiletech/authboss@v2.4.1+incompatible/defaults/smtp_mailer.go (about)

     1  package defaults
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"math/rand"
     8  	"net/smtp"
     9  	"strings"
    10  	"text/template"
    11  	"time"
    12  
    13  	"github.com/pkg/errors"
    14  	"github.com/volatiletech/authboss"
    15  )
    16  
    17  // NewSMTPMailer creates an SMTP Mailer to send emails with.
    18  // An example usage might be something like:
    19  //
    20  //   NewSMTPMailer("smtp.gmail.com",
    21  //     smtp.PlainAuth("", "admin@yoursite.com", "password", "smtp.gmail.com"))
    22  func NewSMTPMailer(server string, auth smtp.Auth) *SMTPMailer {
    23  	if len(server) == 0 {
    24  		panic("SMTP Mailer must be created with a server string.")
    25  	}
    26  	random := rand.New(rand.NewSource(time.Now().UnixNano()))
    27  	return &SMTPMailer{server, auth, random}
    28  }
    29  
    30  // SMTPMailer uses smtp to actually send e-mails
    31  type SMTPMailer struct {
    32  	Server string
    33  	Auth   smtp.Auth
    34  	rand   *rand.Rand
    35  }
    36  
    37  // Send an e-mail
    38  func (s SMTPMailer) Send(ctx context.Context, mail authboss.Email) error {
    39  	if len(mail.TextBody) == 0 && len(mail.HTMLBody) == 0 {
    40  		return errors.New("refusing to send mail without text or html body")
    41  	}
    42  
    43  	buf := &bytes.Buffer{}
    44  
    45  	data := struct {
    46  		Boundary string
    47  		Mail     authboss.Email
    48  	}{
    49  		Boundary: s.boundary(),
    50  		Mail:     mail,
    51  	}
    52  
    53  	err := emailTmpl.Execute(buf, data)
    54  	if err != nil {
    55  		return err
    56  	}
    57  
    58  	toSend := bytes.Replace(buf.Bytes(), []byte{'\n'}, []byte{'\r', '\n'}, -1)
    59  
    60  	return smtp.SendMail(s.Server, s.Auth, mail.From, mail.To, toSend)
    61  }
    62  
    63  // boundary makes mime boundaries, these are largely useless strings that just
    64  // need to be the same in the mime structure. We choose from the alphabet below
    65  // and create a random string of length 23
    66  // Example:
    67  // 284fad24nao8f4na284f2n4
    68  func (s SMTPMailer) boundary() string {
    69  	const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
    70  	buf := &bytes.Buffer{}
    71  
    72  	for i := 0; i < 23; i++ {
    73  		buf.WriteByte(alphabet[s.rand.Int()%len(alphabet)])
    74  	}
    75  
    76  	return buf.String()
    77  }
    78  
    79  func namedAddress(name, address string) string {
    80  	if len(name) == 0 {
    81  		return address
    82  	}
    83  
    84  	return fmt.Sprintf("%s <%s>", name, address)
    85  }
    86  
    87  func namedAddresses(names, addresses []string) string {
    88  	if len(names) == 0 {
    89  		return strings.Join(addresses, ", ")
    90  	}
    91  
    92  	buf := &bytes.Buffer{}
    93  	first := true
    94  
    95  	for i, address := range addresses {
    96  		if first {
    97  			first = false
    98  		} else {
    99  			buf.WriteString(", ")
   100  		}
   101  
   102  		buf.WriteString(namedAddress(names[i], address))
   103  	}
   104  
   105  	return buf.String()
   106  }
   107  
   108  var emailTmpl = template.Must(template.New("email").Funcs(template.FuncMap{
   109  	"join":           strings.Join,
   110  	"namedAddress":   namedAddress,
   111  	"namedAddresses": namedAddresses,
   112  }).Parse(`To: {{namedAddresses .Mail.ToNames .Mail.To}}{{if .Mail.Cc}}
   113  Cc: {{namedAddresses .Mail.CcNames .Mail.Cc}}{{end}}{{if .Mail.Bcc}}
   114  Bcc: {{namedAddresses .Mail.BccNames .Mail.Bcc}}{{end}}
   115  From: {{namedAddress .Mail.FromName .Mail.From}}
   116  Subject: {{.Mail.Subject}}{{if .Mail.ReplyTo}}
   117  Reply-To: {{namedAddress .Mail.ReplyToName .Mail.ReplyTo}}{{end}}
   118  MIME-Version: 1.0
   119  Content-Type: multipart/alternative; boundary="==============={{.Boundary}}=="
   120  Content-Transfer-Encoding: 7bit
   121  
   122  {{if .Mail.TextBody -}}
   123  --==============={{.Boundary}}==
   124  Content-Type: text/plain; charset=UTF-8
   125  Content-Transfer-Encoding: 7bit
   126  
   127  {{.Mail.TextBody}}
   128  {{end -}}
   129  {{if .Mail.HTMLBody -}}
   130  --==============={{.Boundary}}==
   131  Content-Type: text/html; charset=UTF-8
   132  Content-Transfer-Encoding: 7bit
   133  
   134  {{.Mail.HTMLBody}}
   135  {{end -}}
   136  --==============={{.Boundary}}==--
   137  `))