envelope.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. package envelope
  2. import (
  3. "bufio"
  4. "bytes"
  5. "encoding/base64"
  6. "errors"
  7. "fmt"
  8. "github.com/sloonz/go-qprintable"
  9. "gopkg.in/iconv.v1"
  10. "io/ioutil"
  11. "net/textproto"
  12. "regexp"
  13. "strings"
  14. )
  15. // EmailAddress encodes an email address of the form `<user@host>`
  16. type EmailAddress struct {
  17. User string
  18. Host string
  19. }
  20. func (ep *EmailAddress) String() string {
  21. return fmt.Sprintf("%s@%s", ep.User, ep.Host)
  22. }
  23. func (ep *EmailAddress) IsEmpty() bool {
  24. return ep.User == "" && ep.Host == ""
  25. }
  26. // Email represents a single SMTP message.
  27. type Envelope struct {
  28. // Remote IP address
  29. RemoteAddress string
  30. // Message sent in EHLO command
  31. Helo string
  32. // Sender
  33. MailFrom EmailAddress
  34. // Recipients
  35. RcptTo []EmailAddress
  36. // Data stores the header and message body
  37. Data bytes.Buffer
  38. // Subject stores the subject of the email, extracted and decoded after calling ParseHeaders()
  39. Subject string
  40. // TLS is true if the email was received using a TLS connection
  41. TLS bool
  42. // Header stores the results from ParseHeaders()
  43. Header textproto.MIMEHeader
  44. // Hold the information generated when processing the envelope by the backend
  45. Info map[string]interface{}
  46. // Hashes of each email on the rcpt
  47. Hashes []string
  48. //
  49. DeliveryHeader string
  50. }
  51. func NewEnvelope(remoteAddr string) *Envelope {
  52. return &Envelope{
  53. RemoteAddress: remoteAddr,
  54. Info: make(map[string]interface{}),
  55. }
  56. }
  57. // ParseHeaders parses the headers into Header field of the Envelope struct.
  58. // Data buffer must be full before calling.
  59. // It assumes that at most 30kb of email data can be a header
  60. // Decoding of encoding to UTF is only done on the Subject, where the result is assigned to the Subject field
  61. func (e *Envelope) ParseHeaders() error {
  62. var err error
  63. if e.Header != nil {
  64. return errors.New("Headers already parsed")
  65. }
  66. b2 := bytes.NewBuffer(e.Data.Bytes())
  67. // find where the header ends, assuming that over 30 kb would be max
  68. max := 1024 * 30
  69. if b2.Len() < max {
  70. max = b2.Len()
  71. }
  72. // read in the chunk which we'll scan for the header
  73. chunk := make([]byte, max)
  74. b2.Read(chunk)
  75. headerEnd := strings.Index(string(chunk), "\n\n") // the first two new-lines is the end of header
  76. if headerEnd > -1 {
  77. header := chunk[0:headerEnd]
  78. headerReader := textproto.NewReader(bufio.NewReader(bytes.NewBuffer(header)))
  79. e.Header, err = headerReader.ReadMIMEHeader()
  80. if err != nil {
  81. // decode the subject
  82. if subject, ok := e.Header["Subject"]; ok {
  83. e.Subject = MimeHeaderDecode(subject[0])
  84. }
  85. }
  86. } else {
  87. err = errors.New("header not found")
  88. }
  89. return err
  90. }
  91. // String converts the email to string. Typically, you would want to use the compressor processor for more efficiency
  92. func (e *Envelope) String() string {
  93. return e.DeliveryHeader + e.Data.String()
  94. }
  95. var mimeRegex, _ = regexp.Compile(`=\?(.+?)\?([QBqp])\?(.+?)\?=`)
  96. // Decode strings in Mime header format
  97. // eg. =?ISO-2022-JP?B?GyRCIVo9dztSOWJAOCVBJWMbKEI=?=
  98. func MimeHeaderDecode(str string) string {
  99. matched := mimeRegex.FindAllStringSubmatch(str, -1)
  100. var charset, encoding, payload string
  101. if matched != nil {
  102. for i := 0; i < len(matched); i++ {
  103. if len(matched[i]) > 2 {
  104. charset = matched[i][1]
  105. encoding = strings.ToUpper(matched[i][2])
  106. payload = matched[i][3]
  107. switch encoding {
  108. case "B":
  109. str = strings.Replace(
  110. str,
  111. matched[i][0],
  112. MailTransportDecode(payload, "base64", charset),
  113. 1)
  114. case "Q":
  115. str = strings.Replace(
  116. str,
  117. matched[i][0],
  118. MailTransportDecode(payload, "quoted-printable", charset),
  119. 1)
  120. }
  121. }
  122. }
  123. }
  124. return str
  125. }
  126. // decode from 7bit to 8bit UTF-8
  127. // encodingType can be "base64" or "quoted-printable"
  128. func MailTransportDecode(str string, encodingType string, charset string) string {
  129. if charset == "" {
  130. charset = "UTF-8"
  131. } else {
  132. charset = strings.ToUpper(charset)
  133. }
  134. if encodingType == "base64" {
  135. str = fromBase64(str)
  136. } else if encodingType == "quoted-printable" {
  137. str = fromQuotedP(str)
  138. }
  139. if charset != "UTF-8" {
  140. charset = fixCharset(charset)
  141. // iconv is pretty good at what it does
  142. if cd, err := iconv.Open("UTF-8", charset); err == nil {
  143. defer func() {
  144. cd.Close()
  145. if r := recover(); r != nil {
  146. //logln(1, fmt.Sprintf("Recovered in %v", r))
  147. }
  148. }()
  149. // eg. charset can be "ISO-2022-JP"
  150. return cd.ConvString(str)
  151. }
  152. }
  153. return str
  154. }
  155. func fromBase64(data string) string {
  156. buf := bytes.NewBufferString(data)
  157. decoder := base64.NewDecoder(base64.StdEncoding, buf)
  158. res, _ := ioutil.ReadAll(decoder)
  159. return string(res)
  160. }
  161. func fromQuotedP(data string) string {
  162. buf := bytes.NewBufferString(data)
  163. decoder := qprintable.NewDecoder(qprintable.BinaryEncoding, buf)
  164. res, _ := ioutil.ReadAll(decoder)
  165. return string(res)
  166. }
  167. var charsetRegex, _ = regexp.Compile(`[_:.\/\\]`)
  168. func fixCharset(charset string) string {
  169. fixed_charset := charsetRegex.ReplaceAllString(charset, "-")
  170. // Fix charset
  171. // borrowed from http://squirrelmail.svn.sourceforge.net/viewvc/squirrelmail/trunk/squirrelmail/include/languages.php?revision=13765&view=markup
  172. // OE ks_c_5601_1987 > cp949
  173. fixed_charset = strings.Replace(fixed_charset, "ks-c-5601-1987", "cp949", -1)
  174. // Moz x-euc-tw > euc-tw
  175. fixed_charset = strings.Replace(fixed_charset, "x-euc", "euc", -1)
  176. // Moz x-windows-949 > cp949
  177. fixed_charset = strings.Replace(fixed_charset, "x-windows_", "cp", -1)
  178. // windows-125x and cp125x charsets
  179. fixed_charset = strings.Replace(fixed_charset, "windows-", "cp", -1)
  180. // ibm > cp
  181. fixed_charset = strings.Replace(fixed_charset, "ibm", "cp", -1)
  182. // iso-8859-8-i -> iso-8859-8
  183. fixed_charset = strings.Replace(fixed_charset, "iso-8859-8-i", "iso-8859-8", -1)
  184. if charset != fixed_charset {
  185. return fixed_charset
  186. }
  187. return charset
  188. }