message.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. package mail
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "io"
  6. "mime/multipart"
  7. "mime/quotedprintable"
  8. "net/textproto"
  9. "regexp"
  10. "strconv"
  11. "strings"
  12. "time"
  13. )
  14. type message struct {
  15. headers textproto.MIMEHeader
  16. body *bytes.Buffer
  17. writers []*multipart.Writer
  18. parts uint8
  19. cids map[string]string
  20. charset string
  21. encoding encoding
  22. }
  23. func newMessage(email *Email) *message {
  24. return &message{
  25. headers: email.headers,
  26. body: new(bytes.Buffer),
  27. cids: make(map[string]string),
  28. charset: email.Charset,
  29. encoding: email.Encoding}
  30. }
  31. func encodeHeader(text string, charset string, usedChars int) string {
  32. // create buffer
  33. buf := new(bytes.Buffer)
  34. // encode
  35. encoder := newEncoder(buf, charset, usedChars)
  36. encoder.encode([]byte(text))
  37. return buf.String()
  38. /*
  39. switch encoding {
  40. case EncodingBase64:
  41. return mime.BEncoding.Encode(charset, text)
  42. default:
  43. return mime.QEncoding.Encode(charset, text)
  44. }
  45. */
  46. }
  47. // getHeaders returns the message headers
  48. func (msg *message) getHeaders() (headers string) {
  49. // if the date header isn't set, set it
  50. if date := msg.headers.Get("Date"); date == "" {
  51. msg.headers.Set("Date", time.Now().Format(time.RFC1123Z))
  52. }
  53. // encode and combine the headers
  54. for header, values := range msg.headers {
  55. headers += header + ": " + encodeHeader(strings.Join(values, ", "), msg.charset, len(header)+2) + "\r\n"
  56. }
  57. headers = headers + "\r\n"
  58. return
  59. }
  60. // getCID gets the generated CID for the provided text
  61. func (msg *message) getCID(text string) (cid string) {
  62. // set the date format to use
  63. const dateFormat = "20060102.150405"
  64. // get the cid if we have one
  65. cid, exists := msg.cids[text]
  66. if !exists {
  67. // generate a new cid
  68. cid = time.Now().Format(dateFormat) + "." + strconv.Itoa(len(msg.cids)+1) + "@mail.0"
  69. // save it
  70. msg.cids[text] = cid
  71. }
  72. return
  73. }
  74. // replaceCIDs replaces the CIDs found in a text string
  75. // with generated ones
  76. func (msg *message) replaceCIDs(text string) string {
  77. // regular expression to find cids
  78. re := regexp.MustCompile(`(src|href)="cid:(.*?)"`)
  79. // replace all of the found cids with generated ones
  80. for _, matches := range re.FindAllStringSubmatch(text, -1) {
  81. cid := msg.getCID(matches[2])
  82. text = strings.Replace(text, "cid:"+matches[2], "cid:"+cid, -1)
  83. }
  84. return text
  85. }
  86. // openMultipart creates a new part of a multipart message
  87. func (msg *message) openMultipart(multipartType string) {
  88. // create a new multipart writer
  89. msg.writers = append(msg.writers, multipart.NewWriter(msg.body))
  90. // create the boundary
  91. contentType := "multipart/" + multipartType + ";\n \tboundary=" + msg.writers[msg.parts].Boundary()
  92. // if no existing parts, add header to main header group
  93. if msg.parts == 0 {
  94. msg.headers.Set("Content-Type", contentType)
  95. } else { // add header to multipart section
  96. header := make(textproto.MIMEHeader)
  97. header.Set("Content-Type", contentType)
  98. msg.writers[msg.parts-1].CreatePart(header)
  99. }
  100. msg.parts++
  101. }
  102. // closeMultipart closes a part of a multipart message
  103. func (msg *message) closeMultipart() {
  104. if msg.parts > 0 {
  105. msg.writers[msg.parts-1].Close()
  106. msg.parts--
  107. }
  108. }
  109. // base64Encode base64 encodes the provided text with line wrapping
  110. func base64Encode(text []byte) []byte {
  111. // create buffer
  112. buf := new(bytes.Buffer)
  113. // create base64 encoder that linewraps
  114. encoder := base64.NewEncoder(base64.StdEncoding, &base64LineWrap{writer: buf})
  115. // write the encoded text to buf
  116. encoder.Write(text)
  117. encoder.Close()
  118. return buf.Bytes()
  119. }
  120. // qpEncode uses the quoted-printable encoding to encode the provided text
  121. func qpEncode(text []byte) []byte {
  122. // create buffer
  123. buf := new(bytes.Buffer)
  124. encoder := quotedprintable.NewWriter(buf)
  125. encoder.Write(text)
  126. encoder.Close()
  127. return buf.Bytes()
  128. }
  129. const maxLineChars = 76
  130. type base64LineWrap struct {
  131. writer io.Writer
  132. numLineChars int
  133. }
  134. func (e *base64LineWrap) Write(p []byte) (n int, err error) {
  135. n = 0
  136. // while we have more chars than are allowed
  137. for len(p)+e.numLineChars > maxLineChars {
  138. numCharsToWrite := maxLineChars - e.numLineChars
  139. // write the chars we can
  140. e.writer.Write(p[:numCharsToWrite])
  141. // write a line break
  142. e.writer.Write([]byte("\r\n"))
  143. // reset the line count
  144. e.numLineChars = 0
  145. // remove the chars that have been written
  146. p = p[numCharsToWrite:]
  147. // set the num of chars written
  148. n += numCharsToWrite
  149. }
  150. // write what is left
  151. e.writer.Write(p)
  152. e.numLineChars += len(p)
  153. n += len(p)
  154. return
  155. }
  156. func (msg *message) write(header textproto.MIMEHeader, body []byte, encoding encoding) {
  157. msg.writeHeader(header)
  158. msg.writeBody(body, encoding)
  159. }
  160. func (msg *message) writeHeader(headers textproto.MIMEHeader) {
  161. // if there are no parts add header to main headers
  162. if msg.parts == 0 {
  163. for header, value := range headers {
  164. msg.headers[header] = value
  165. }
  166. } else { // add header to multipart section
  167. msg.writers[msg.parts-1].CreatePart(headers)
  168. }
  169. }
  170. func (msg *message) writeBody(body []byte, encoding encoding) {
  171. // encode and write the body
  172. switch encoding {
  173. case EncodingQuotedPrintable:
  174. msg.body.Write(qpEncode(body))
  175. case EncodingBase64:
  176. msg.body.Write(base64Encode(body))
  177. default:
  178. msg.body.Write(body)
  179. }
  180. }
  181. func (msg *message) addBody(contentType string, body []byte) {
  182. body = []byte(msg.replaceCIDs(string(body)))
  183. header := make(textproto.MIMEHeader)
  184. header.Set("Content-Type", contentType+"; charset="+msg.charset)
  185. header.Set("Content-Transfer-Encoding", msg.encoding.string())
  186. msg.write(header, body, msg.encoding)
  187. }
  188. var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
  189. func escapeQuotes(s string) string {
  190. return quoteEscaper.Replace(s)
  191. }
  192. func (msg *message) addFiles(files []*file, inline bool) {
  193. encoding := EncodingBase64
  194. for _, file := range files {
  195. header := make(textproto.MIMEHeader)
  196. header.Set("Content-Type", file.mimeType+";\n \tname=\""+encodeHeader(escapeQuotes(file.filename), msg.charset, 6)+`"`)
  197. header.Set("Content-Transfer-Encoding", encoding.string())
  198. if inline {
  199. header.Set("Content-Disposition", "inline;\n \tfilename=\""+encodeHeader(escapeQuotes(file.filename), msg.charset, 10)+`"`)
  200. header.Set("Content-ID", "<"+msg.getCID(file.filename)+">")
  201. } else {
  202. header.Set("Content-Disposition", "attachment;\n \tfilename=\""+encodeHeader(escapeQuotes(file.filename), msg.charset, 10)+`"`)
  203. }
  204. msg.write(header, file.data, encoding)
  205. }
  206. }