github.com/elliott5/community@v0.14.1-0.20160709191136-823126fb026a/documize/api/mail/smtp.go (about) 1 // Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved. 2 // 3 // This software (Documize Community Edition) is licensed under 4 // GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html 5 // 6 // You can operate outside the AGPL restrictions by purchasing 7 // Documize Enterprise Edition and obtaining a commercial license 8 // by contacting <sales@documize.com>. 9 // 10 // https://documize.com 11 12 /* 13 Elements of the software in this file were modified from github.com/jordan-wright/email and 14 are subject to the licence below: 15 16 The MIT License (MIT) 17 18 Copyright (c) 2013 Jordan Wright 19 20 Permission is hereby granted, free of charge, to any person obtaining a copy of 21 this software and associated documentation files (the "Software"), to deal in 22 the Software without restriction, including without limitation the rights to 23 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 24 the Software, and to permit persons to whom the Software is furnished to do so, 25 subject to the following conditions: 26 27 The above copyright notice and this permission notice shall be included in all 28 copies or substantial portions of the Software. 29 30 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 32 FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 33 COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 34 IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 35 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 36 */ 37 38 // Package mail sends transactional emails. 39 // The code in smtp.go is designed to provide an "email interface for humans". 40 // Designed to be robust and flexible, the email package aims to make sending email easy without getting in the way. 41 package mail 42 43 import ( 44 "bytes" 45 "encoding/base64" 46 "errors" 47 "fmt" 48 "github.com/documize/community/wordsmith/log" 49 "io" 50 "mime" 51 "mime/multipart" 52 "net/mail" 53 "net/smtp" 54 "net/textproto" 55 "os" 56 "path/filepath" 57 "strings" 58 "time" 59 ) 60 61 const ( 62 // MaxLineLength is the maximum line length per RFC 2045 63 MaxLineLength = 76 64 ) 65 66 // Email is the type used for email messages 67 type Email struct { 68 From string 69 To []string 70 Bcc []string 71 Cc []string 72 Subject string 73 Text []byte // Plaintext message (optional) 74 HTML []byte // Html message (optional) 75 Headers textproto.MIMEHeader 76 Attachments []*Attachment 77 ReadReceipt []string 78 } 79 80 // newEmail creates an Email, and returns the pointer to it. 81 func newEmail() *Email { 82 return &Email{Headers: textproto.MIMEHeader{}} 83 } 84 85 // Attach is used to attach content from an io.Reader to the email. 86 // Required parameters include an io.Reader, the desired filename for the attachment, and the Content-Type 87 // The function will return the created Attachment for reference, as well as nil for the error, if successful. 88 func (e *Email) Attach(r io.Reader, filename string, c string) (a *Attachment, err error) { 89 var buffer bytes.Buffer 90 if _, err = io.Copy(&buffer, r); err != nil { 91 return 92 } 93 at := &Attachment{ 94 Filename: filename, 95 Header: textproto.MIMEHeader{}, 96 Content: buffer.Bytes(), 97 } 98 // Get the Content-Type to be used in the MIMEHeader 99 if c != "" { 100 at.Header.Set("Content-Type", c) 101 } else { 102 // If the Content-Type is blank, set the Content-Type to "application/octet-stream" 103 at.Header.Set("Content-Type", "application/octet-stream") 104 } 105 at.Header.Set("Content-Disposition", fmt.Sprintf("attachment;\r\n filename=\"%s\"", filename)) 106 at.Header.Set("Content-ID", fmt.Sprintf("<%s>", filename)) 107 at.Header.Set("Content-Transfer-Encoding", "base64") 108 e.Attachments = append(e.Attachments, at) 109 return at, nil 110 } 111 112 // AttachFile is used to attach content to the email. 113 // It attempts to open the file referenced by filename and, if successful, creates an Attachment. 114 // This Attachment is then appended to the slice of Email.Attachments. 115 // The function will then return the Attachment for reference, as well as nil for the error, if successful. 116 func (e *Email) AttachFile(filename string) (a *Attachment, err error) { 117 f, err := os.Open(filename) 118 if err != nil { 119 return 120 } 121 ct := mime.TypeByExtension(filepath.Ext(filename)) 122 basename := filepath.Base(filename) 123 return e.Attach(f, basename, ct) 124 } 125 126 // msgHeaders merges the Email's various fields and custom headers together in a 127 // standards compliant way to create a MIMEHeader to be used in the resulting 128 // message. It does not alter e.Headers. 129 // 130 // "e"'s fields To, Cc, From, Subject will be used unless they are present in 131 // e.Headers. Unless set in e.Headers, "Date" will filled with the current time. 132 func (e *Email) msgHeaders() textproto.MIMEHeader { 133 res := make(textproto.MIMEHeader, len(e.Headers)+4) 134 if e.Headers != nil { 135 for _, h := range []string{"To", "Cc", "From", "Subject", "Date"} { 136 if v, ok := e.Headers[h]; ok { 137 res[h] = v 138 } 139 } 140 } 141 // Set headers if there are values. 142 if _, ok := res["To"]; !ok && len(e.To) > 0 { 143 res.Set("To", strings.Join(e.To, ", ")) 144 } 145 if _, ok := res["Cc"]; !ok && len(e.Cc) > 0 { 146 res.Set("Cc", strings.Join(e.Cc, ", ")) 147 } 148 if _, ok := res["Subject"]; !ok && e.Subject != "" { 149 res.Set("Subject", e.Subject) 150 } 151 // Date and From are required headers. 152 if _, ok := res["From"]; !ok { 153 res.Set("From", e.From) 154 } 155 if _, ok := res["Date"]; !ok { 156 res.Set("Date", time.Now().Format(time.RFC1123Z)) 157 } 158 if _, ok := res["Mime-Version"]; !ok { 159 res.Set("Mime-Version", "1.0") 160 } 161 for field, vals := range e.Headers { 162 if _, ok := res[field]; !ok { 163 res[field] = vals 164 } 165 } 166 return res 167 } 168 169 // Bytes converts the Email object to a []byte representation, including all needed MIMEHeaders, boundaries, etc. 170 func (e *Email) Bytes() ([]byte, error) { 171 // TODO: better guess buffer size 172 buff := bytes.NewBuffer(make([]byte, 0, 4096)) 173 174 headers := e.msgHeaders() 175 w := multipart.NewWriter(buff) 176 // TODO: determine the content type based on message/attachment mix. 177 headers.Set("Content-Type", "multipart/mixed;\r\n boundary="+w.Boundary()) 178 headerToBytes(buff, headers) 179 _, err := io.WriteString(buff, "\r\n") 180 log.IfErr(err) 181 // Start the multipart/mixed part 182 fmt.Fprintf(buff, "--%s\r\n", w.Boundary()) 183 header := textproto.MIMEHeader{} 184 // Check to see if there is a Text or HTML field 185 if len(e.Text) > 0 || len(e.HTML) > 0 { 186 subWriter := multipart.NewWriter(buff) 187 // Create the multipart alternative part 188 header.Set("Content-Type", fmt.Sprintf("multipart/alternative;\r\n boundary=%s\r\n", subWriter.Boundary())) 189 // Write the header 190 headerToBytes(buff, header) 191 // Create the body sections 192 if len(e.Text) > 0 { 193 header.Set("Content-Type", fmt.Sprintf("text/plain; charset=UTF-8")) 194 header.Set("Content-Transfer-Encoding", "quoted-printable") 195 if _, err := subWriter.CreatePart(header); err != nil { 196 return nil, err 197 } 198 // Write the text 199 if err := quotePrintEncode(buff, e.Text); err != nil { 200 return nil, err 201 } 202 } 203 if len(e.HTML) > 0 { 204 header.Set("Content-Type", fmt.Sprintf("text/html; charset=UTF-8")) 205 header.Set("Content-Transfer-Encoding", "quoted-printable") 206 if _, err := subWriter.CreatePart(header); err != nil { 207 return nil, err 208 } 209 // Write the text 210 if err := quotePrintEncode(buff, e.HTML); err != nil { 211 return nil, err 212 } 213 } 214 if err := subWriter.Close(); err != nil { 215 return nil, err 216 } 217 } 218 // Create attachment part, if necessary 219 for _, a := range e.Attachments { 220 ap, err := w.CreatePart(a.Header) 221 if err != nil { 222 return nil, err 223 } 224 // Write the base64Wrapped content to the part 225 base64Wrap(ap, a.Content) 226 } 227 if err := w.Close(); err != nil { 228 return nil, err 229 } 230 return buff.Bytes(), nil 231 } 232 233 // Send an email using the given host and SMTP auth (optional), returns any error thrown by smtp.SendMail 234 // This function merges the To, Cc, and Bcc fields and calls the smtp.SendMail function using the Email.Bytes() output as the message 235 func (e *Email) Send(addr string, a smtp.Auth) error { 236 // Merge the To, Cc, and Bcc fields 237 to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc)) 238 to = append(append(append(to, e.To...), e.Cc...), e.Bcc...) 239 for i := 0; i < len(to); i++ { 240 addr, err := mail.ParseAddress(to[i]) 241 if err != nil { 242 return err 243 } 244 to[i] = addr.Address 245 } 246 // Check to make sure there is at least one recipient and one "From" address 247 if e.From == "" || len(to) == 0 { 248 return errors.New("Must specify at least one From address and one To address") 249 } 250 from, err := mail.ParseAddress(e.From) 251 if err != nil { 252 return err 253 } 254 raw, err := e.Bytes() 255 if err != nil { 256 return err 257 } 258 return smtpSendMail(addr, a, from.Address, to, raw) 259 } 260 261 var smtpSendMail = smtp.SendMail // so that it can be overloaded for testing 262 263 // Attachment is a struct representing an email attachment. 264 // Based on the mime/multipart.FileHeader struct, Attachment contains the name, MIMEHeader, and content of the attachment in question 265 type Attachment struct { 266 Filename string 267 Header textproto.MIMEHeader 268 Content []byte 269 } 270 271 // quotePrintEncode writes the quoted-printable text to the IO Writer (according to RFC 2045) 272 func quotePrintEncode(w io.Writer, body []byte) error { 273 var buf [3]byte 274 mc := 0 275 for _, c := range body { 276 // We're assuming Unix style text formats as input (LF line break), and 277 // quoted-printable uses CRLF line breaks. (Literal CRs will become 278 // "=0D", but probably shouldn't be there to begin with!) 279 if c == '\n' { 280 _, err := io.WriteString(w, "\r\n") 281 if err != nil { 282 return err 283 } 284 mc = 0 285 continue 286 } 287 288 var nextOut []byte 289 if isPrintable[c] { 290 buf[0] = c 291 nextOut = buf[:1] 292 } else { 293 nextOut = buf[:] 294 qpEscape(nextOut, c) 295 } 296 297 // Add a soft line break if the next (encoded) byte would push this line 298 // to or past the limit. 299 if mc+len(nextOut) >= MaxLineLength { 300 if _, err := io.WriteString(w, "=\r\n"); err != nil { 301 return err 302 } 303 mc = 0 304 } 305 306 if _, err := w.Write(nextOut); err != nil { 307 return err 308 } 309 mc += len(nextOut) 310 } 311 // No trailing end-of-line?? Soft line break, then. TODO: is this sane? 312 if mc > 0 { 313 _, err := io.WriteString(w, "=\r\n") 314 if err != nil { 315 return err 316 } 317 } 318 return nil 319 } 320 321 // isPrintable holds true if the byte given is "printable" according to RFC 2045, false otherwise 322 var isPrintable [256]bool 323 324 func init() { 325 for c := '!'; c <= '<'; c++ { 326 isPrintable[c] = true 327 } 328 for c := '>'; c <= '~'; c++ { 329 isPrintable[c] = true 330 } 331 isPrintable[' '] = true 332 isPrintable['\n'] = true 333 isPrintable['\t'] = true 334 } 335 336 // qpEscape is a helper function for quotePrintEncode which escapes a 337 // non-printable byte. Expects len(dest) == 3. 338 func qpEscape(dest []byte, c byte) { 339 const nums = "0123456789ABCDEF" 340 dest[0] = '=' 341 dest[1] = nums[(c&0xf0)>>4] 342 dest[2] = nums[(c & 0xf)] 343 } 344 345 // base64Wrap encodes the attachment content, and wraps it according to RFC 2045 standards (every 76 chars) 346 // The output is then written to the specified io.Writer 347 func base64Wrap(w io.Writer, b []byte) { 348 // 57 raw bytes per 76-byte base64 line. 349 const maxRaw = 57 350 // Buffer for each line, including trailing CRLF. 351 buffer := make([]byte, MaxLineLength+len("\r\n")) 352 copy(buffer[MaxLineLength:], "\r\n") 353 // Process raw chunks until there's no longer enough to fill a line. 354 for len(b) >= maxRaw { 355 base64.StdEncoding.Encode(buffer, b[:maxRaw]) 356 _, err := w.Write(buffer) 357 log.IfErr(err) 358 b = b[maxRaw:] 359 } 360 // Handle the last chunk of bytes. 361 if len(b) > 0 { 362 out := buffer[:base64.StdEncoding.EncodedLen(len(b))] 363 base64.StdEncoding.Encode(out, b) 364 out = append(out, "\r\n"...) 365 _, err := w.Write(out) 366 log.IfErr(err) 367 } 368 } 369 370 // headerToBytes renders "header" to "buff". If there are multiple values for a 371 // field, multiple "Field: value\r\n" lines will be emitted. 372 func headerToBytes(buff *bytes.Buffer, header textproto.MIMEHeader) { 373 for field, vals := range header { 374 for _, subval := range vals { 375 _, err := io.WriteString(buff, field+": "+subval+"\r\n") 376 log.IfErr(err) 377 } 378 } 379 }