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 `))