github.com/astaxie/beego@v1.12.3/utils/mail.go (about) 1 // Copyright 2014 beego Author. All Rights Reserved. 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 utils 16 17 import ( 18 "bytes" 19 "encoding/base64" 20 "encoding/json" 21 "errors" 22 "fmt" 23 "io" 24 "mime" 25 "mime/multipart" 26 "net/mail" 27 "net/smtp" 28 "net/textproto" 29 "os" 30 "path" 31 "path/filepath" 32 "strconv" 33 "strings" 34 "sync" 35 ) 36 37 const ( 38 maxLineLength = 76 39 40 upperhex = "0123456789ABCDEF" 41 ) 42 43 // Email is the type used for email messages 44 type Email struct { 45 Auth smtp.Auth 46 Identity string `json:"identity"` 47 Username string `json:"username"` 48 Password string `json:"password"` 49 Host string `json:"host"` 50 Port int `json:"port"` 51 From string `json:"from"` 52 To []string 53 Bcc []string 54 Cc []string 55 Subject string 56 Text string // Plaintext message (optional) 57 HTML string // Html message (optional) 58 Headers textproto.MIMEHeader 59 Attachments []*Attachment 60 ReadReceipt []string 61 } 62 63 // Attachment is a struct representing an email attachment. 64 // Based on the mime/multipart.FileHeader struct, Attachment contains the name, MIMEHeader, and content of the attachment in question 65 type Attachment struct { 66 Filename string 67 Header textproto.MIMEHeader 68 Content []byte 69 } 70 71 // NewEMail create new Email struct with config json. 72 // config json is followed from Email struct fields. 73 func NewEMail(config string) *Email { 74 e := new(Email) 75 e.Headers = textproto.MIMEHeader{} 76 err := json.Unmarshal([]byte(config), e) 77 if err != nil { 78 return nil 79 } 80 return e 81 } 82 83 // Bytes Make all send information to byte 84 func (e *Email) Bytes() ([]byte, error) { 85 buff := &bytes.Buffer{} 86 w := multipart.NewWriter(buff) 87 // Set the appropriate headers (overwriting any conflicts) 88 // Leave out Bcc (only included in envelope headers) 89 e.Headers.Set("To", strings.Join(e.To, ",")) 90 if e.Cc != nil { 91 e.Headers.Set("Cc", strings.Join(e.Cc, ",")) 92 } 93 e.Headers.Set("From", e.From) 94 e.Headers.Set("Subject", e.Subject) 95 if len(e.ReadReceipt) != 0 { 96 e.Headers.Set("Disposition-Notification-To", strings.Join(e.ReadReceipt, ",")) 97 } 98 e.Headers.Set("MIME-Version", "1.0") 99 100 // Write the envelope headers (including any custom headers) 101 if err := headerToBytes(buff, e.Headers); err != nil { 102 return nil, fmt.Errorf("Failed to render message headers: %s", err) 103 } 104 105 e.Headers.Set("Content-Type", fmt.Sprintf("multipart/mixed;\r\n boundary=%s\r\n", w.Boundary())) 106 fmt.Fprintf(buff, "%s:", "Content-Type") 107 fmt.Fprintf(buff, " %s\r\n", fmt.Sprintf("multipart/mixed;\r\n boundary=%s\r\n", w.Boundary())) 108 109 // Start the multipart/mixed part 110 fmt.Fprintf(buff, "--%s\r\n", w.Boundary()) 111 header := textproto.MIMEHeader{} 112 // Check to see if there is a Text or HTML field 113 if e.Text != "" || e.HTML != "" { 114 subWriter := multipart.NewWriter(buff) 115 // Create the multipart alternative part 116 header.Set("Content-Type", fmt.Sprintf("multipart/alternative;\r\n boundary=%s\r\n", subWriter.Boundary())) 117 // Write the header 118 if err := headerToBytes(buff, header); err != nil { 119 return nil, fmt.Errorf("Failed to render multipart message headers: %s", err) 120 } 121 // Create the body sections 122 if e.Text != "" { 123 header.Set("Content-Type", fmt.Sprintf("text/plain; charset=UTF-8")) 124 header.Set("Content-Transfer-Encoding", "quoted-printable") 125 if _, err := subWriter.CreatePart(header); err != nil { 126 return nil, err 127 } 128 // Write the text 129 if err := quotePrintEncode(buff, e.Text); err != nil { 130 return nil, err 131 } 132 } 133 if e.HTML != "" { 134 header.Set("Content-Type", fmt.Sprintf("text/html; charset=UTF-8")) 135 header.Set("Content-Transfer-Encoding", "quoted-printable") 136 if _, err := subWriter.CreatePart(header); err != nil { 137 return nil, err 138 } 139 // Write the text 140 if err := quotePrintEncode(buff, e.HTML); err != nil { 141 return nil, err 142 } 143 } 144 if err := subWriter.Close(); err != nil { 145 return nil, err 146 } 147 } 148 // Create attachment part, if necessary 149 for _, a := range e.Attachments { 150 ap, err := w.CreatePart(a.Header) 151 if err != nil { 152 return nil, err 153 } 154 // Write the base64Wrapped content to the part 155 base64Wrap(ap, a.Content) 156 } 157 if err := w.Close(); err != nil { 158 return nil, err 159 } 160 return buff.Bytes(), nil 161 } 162 163 // AttachFile Add attach file to the send mail 164 func (e *Email) AttachFile(args ...string) (a *Attachment, err error) { 165 if len(args) < 1 || len(args) > 2 { // change && to || 166 err = errors.New("Must specify a file name and number of parameters can not exceed at least two") 167 return 168 } 169 filename := args[0] 170 id := "" 171 if len(args) > 1 { 172 id = args[1] 173 } 174 f, err := os.Open(filename) 175 if err != nil { 176 return 177 } 178 defer f.Close() 179 ct := mime.TypeByExtension(filepath.Ext(filename)) 180 basename := path.Base(filename) 181 return e.Attach(f, basename, ct, id) 182 } 183 184 // Attach is used to attach content from an io.Reader to the email. 185 // Parameters include an io.Reader, the desired filename for the attachment, and the Content-Type. 186 func (e *Email) Attach(r io.Reader, filename string, args ...string) (a *Attachment, err error) { 187 if len(args) < 1 || len(args) > 2 { // change && to || 188 err = errors.New("Must specify the file type and number of parameters can not exceed at least two") 189 return 190 } 191 c := args[0] //Content-Type 192 id := "" 193 if len(args) > 1 { 194 id = args[1] //Content-ID 195 } 196 var buffer bytes.Buffer 197 if _, err = io.Copy(&buffer, r); err != nil { 198 return 199 } 200 at := &Attachment{ 201 Filename: filename, 202 Header: textproto.MIMEHeader{}, 203 Content: buffer.Bytes(), 204 } 205 // Get the Content-Type to be used in the MIMEHeader 206 if c != "" { 207 at.Header.Set("Content-Type", c) 208 } else { 209 // If the Content-Type is blank, set the Content-Type to "application/octet-stream" 210 at.Header.Set("Content-Type", "application/octet-stream") 211 } 212 if id != "" { 213 at.Header.Set("Content-Disposition", fmt.Sprintf("inline;\r\n filename=\"%s\"", filename)) 214 at.Header.Set("Content-ID", fmt.Sprintf("<%s>", id)) 215 } else { 216 at.Header.Set("Content-Disposition", fmt.Sprintf("attachment;\r\n filename=\"%s\"", filename)) 217 } 218 at.Header.Set("Content-Transfer-Encoding", "base64") 219 e.Attachments = append(e.Attachments, at) 220 return at, nil 221 } 222 223 // Send will send out the mail 224 func (e *Email) Send() error { 225 if e.Auth == nil { 226 e.Auth = smtp.PlainAuth(e.Identity, e.Username, e.Password, e.Host) 227 } 228 // Merge the To, Cc, and Bcc fields 229 to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc)) 230 to = append(append(append(to, e.To...), e.Cc...), e.Bcc...) 231 // Check to make sure there is at least one recipient and one "From" address 232 if len(to) == 0 { 233 return errors.New("Must specify at least one To address") 234 } 235 236 // Use the username if no From is provided 237 if len(e.From) == 0 { 238 e.From = e.Username 239 } 240 241 from, err := mail.ParseAddress(e.From) 242 if err != nil { 243 return err 244 } 245 246 // use mail's RFC 2047 to encode any string 247 e.Subject = qEncode("utf-8", e.Subject) 248 249 raw, err := e.Bytes() 250 if err != nil { 251 return err 252 } 253 return smtp.SendMail(e.Host+":"+strconv.Itoa(e.Port), e.Auth, from.Address, to, raw) 254 } 255 256 // quotePrintEncode writes the quoted-printable text to the IO Writer (according to RFC 2045) 257 func quotePrintEncode(w io.Writer, s string) error { 258 var buf [3]byte 259 mc := 0 260 for i := 0; i < len(s); i++ { 261 c := s[i] 262 // We're assuming Unix style text formats as input (LF line break), and 263 // quoted-printble uses CRLF line breaks. (Literal CRs will become 264 // "=0D", but probably shouldn't be there to begin with!) 265 if c == '\n' { 266 io.WriteString(w, "\r\n") 267 mc = 0 268 continue 269 } 270 271 var nextOut []byte 272 if isPrintable(c) { 273 nextOut = append(buf[:0], c) 274 } else { 275 nextOut = buf[:] 276 qpEscape(nextOut, c) 277 } 278 279 // Add a soft line break if the next (encoded) byte would push this line 280 // to or past the limit. 281 if mc+len(nextOut) >= maxLineLength { 282 if _, err := io.WriteString(w, "=\r\n"); err != nil { 283 return err 284 } 285 mc = 0 286 } 287 288 if _, err := w.Write(nextOut); err != nil { 289 return err 290 } 291 mc += len(nextOut) 292 } 293 // No trailing end-of-line?? Soft line break, then. TODO: is this sane? 294 if mc > 0 { 295 io.WriteString(w, "=\r\n") 296 } 297 return nil 298 } 299 300 // isPrintable returns true if the rune given is "printable" according to RFC 2045, false otherwise 301 func isPrintable(c byte) bool { 302 return (c >= '!' && c <= '<') || (c >= '>' && c <= '~') || (c == ' ' || c == '\n' || c == '\t') 303 } 304 305 // qpEscape is a helper function for quotePrintEncode which escapes a 306 // non-printable byte. Expects len(dest) == 3. 307 func qpEscape(dest []byte, c byte) { 308 const nums = "0123456789ABCDEF" 309 dest[0] = '=' 310 dest[1] = nums[(c&0xf0)>>4] 311 dest[2] = nums[(c & 0xf)] 312 } 313 314 // headerToBytes enumerates the key and values in the header, and writes the results to the IO Writer 315 func headerToBytes(w io.Writer, t textproto.MIMEHeader) error { 316 for k, v := range t { 317 // Write the header key 318 _, err := fmt.Fprintf(w, "%s:", k) 319 if err != nil { 320 return err 321 } 322 // Write each value in the header 323 for _, c := range v { 324 _, err := fmt.Fprintf(w, " %s\r\n", c) 325 if err != nil { 326 return err 327 } 328 } 329 } 330 return nil 331 } 332 333 // base64Wrap encodes the attachment content, and wraps it according to RFC 2045 standards (every 76 chars) 334 // The output is then written to the specified io.Writer 335 func base64Wrap(w io.Writer, b []byte) { 336 // 57 raw bytes per 76-byte base64 line. 337 const maxRaw = 57 338 // Buffer for each line, including trailing CRLF. 339 var buffer [maxLineLength + len("\r\n")]byte 340 copy(buffer[maxLineLength:], "\r\n") 341 // Process raw chunks until there's no longer enough to fill a line. 342 for len(b) >= maxRaw { 343 base64.StdEncoding.Encode(buffer[:], b[:maxRaw]) 344 w.Write(buffer[:]) 345 b = b[maxRaw:] 346 } 347 // Handle the last chunk of bytes. 348 if len(b) > 0 { 349 out := buffer[:base64.StdEncoding.EncodedLen(len(b))] 350 base64.StdEncoding.Encode(out, b) 351 out = append(out, "\r\n"...) 352 w.Write(out) 353 } 354 } 355 356 // Encode returns the encoded-word form of s. If s is ASCII without special 357 // characters, it is returned unchanged. The provided charset is the IANA 358 // charset name of s. It is case insensitive. 359 // RFC 2047 encoded-word 360 func qEncode(charset, s string) string { 361 if !needsEncoding(s) { 362 return s 363 } 364 return encodeWord(charset, s) 365 } 366 367 func needsEncoding(s string) bool { 368 for _, b := range s { 369 if (b < ' ' || b > '~') && b != '\t' { 370 return true 371 } 372 } 373 return false 374 } 375 376 // encodeWord encodes a string into an encoded-word. 377 func encodeWord(charset, s string) string { 378 buf := getBuffer() 379 380 buf.WriteString("=?") 381 buf.WriteString(charset) 382 buf.WriteByte('?') 383 buf.WriteByte('q') 384 buf.WriteByte('?') 385 386 enc := make([]byte, 3) 387 for i := 0; i < len(s); i++ { 388 b := s[i] 389 switch { 390 case b == ' ': 391 buf.WriteByte('_') 392 case b <= '~' && b >= '!' && b != '=' && b != '?' && b != '_': 393 buf.WriteByte(b) 394 default: 395 enc[0] = '=' 396 enc[1] = upperhex[b>>4] 397 enc[2] = upperhex[b&0x0f] 398 buf.Write(enc) 399 } 400 } 401 buf.WriteString("?=") 402 403 es := buf.String() 404 putBuffer(buf) 405 return es 406 } 407 408 var bufPool = sync.Pool{ 409 New: func() interface{} { 410 return new(bytes.Buffer) 411 }, 412 } 413 414 func getBuffer() *bytes.Buffer { 415 return bufPool.Get().(*bytes.Buffer) 416 } 417 418 func putBuffer(buf *bytes.Buffer) { 419 if buf.Len() > 1024 { 420 return 421 } 422 buf.Reset() 423 bufPool.Put(buf) 424 }