mail.go 11 KB

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