email_test.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  1. package email
  2. import (
  3. "fmt"
  4. "strings"
  5. "testing"
  6. "bufio"
  7. "bytes"
  8. "crypto/rand"
  9. "io"
  10. "io/ioutil"
  11. "mime"
  12. "mime/multipart"
  13. "mime/quotedprintable"
  14. "net/mail"
  15. "net/smtp"
  16. "net/textproto"
  17. )
  18. func prepareEmail() *Email {
  19. e := NewEmail()
  20. e.From = "Jordan Wright <test@example.com>"
  21. e.To = []string{"test@example.com"}
  22. e.Bcc = []string{"test_bcc@example.com"}
  23. e.Cc = []string{"test_cc@example.com"}
  24. e.Subject = "Awesome Subject"
  25. return e
  26. }
  27. func basicTests(t *testing.T, e *Email) *mail.Message {
  28. raw, err := e.Bytes()
  29. if err != nil {
  30. t.Fatal("Failed to render message: ", e)
  31. }
  32. msg, err := mail.ReadMessage(bytes.NewBuffer(raw))
  33. if err != nil {
  34. t.Fatal("Could not parse rendered message: ", err)
  35. }
  36. expectedHeaders := map[string]string{
  37. "To": "<test@example.com>",
  38. "From": "\"Jordan Wright\" <test@example.com>",
  39. "Cc": "<test_cc@example.com>",
  40. "Subject": "Awesome Subject",
  41. }
  42. for header, expected := range expectedHeaders {
  43. if val := msg.Header.Get(header); val != expected {
  44. t.Errorf("Wrong value for message header %s: %v != %v", header, expected, val)
  45. }
  46. }
  47. return msg
  48. }
  49. func TestEmailText(t *testing.T) {
  50. e := prepareEmail()
  51. e.Text = []byte("Text Body is, of course, supported!\n")
  52. msg := basicTests(t, e)
  53. // Were the right headers set?
  54. ct := msg.Header.Get("Content-type")
  55. mt, _, err := mime.ParseMediaType(ct)
  56. if err != nil {
  57. t.Fatal("Content-type header is invalid: ", ct)
  58. } else if mt != "text/plain" {
  59. t.Fatalf("Content-type expected \"text/plain\", not %v", mt)
  60. }
  61. }
  62. func TestEmailWithHTMLAttachments(t *testing.T) {
  63. e := prepareEmail()
  64. // Set plain text to exercise "mime/alternative"
  65. e.Text = []byte("Text Body is, of course, supported!\n")
  66. e.HTML = []byte("<html><body>This is a text.</body></html>")
  67. // Set HTML attachment to exercise "mime/related".
  68. attachment, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "image/png; charset=utf-8")
  69. if err != nil {
  70. t.Fatal("Could not add an attachment to the message: ", err)
  71. }
  72. attachment.HTMLRelated = true
  73. b, err := e.Bytes()
  74. if err != nil {
  75. t.Fatal("Could not serialize e-mail:", err)
  76. }
  77. // Print the bytes for ocular validation and make sure no errors.
  78. //fmt.Println(string(b))
  79. // TODO: Verify the attachments.
  80. s := &trimReader{rd: bytes.NewBuffer(b)}
  81. tp := textproto.NewReader(bufio.NewReader(s))
  82. // Parse the main headers
  83. hdrs, err := tp.ReadMIMEHeader()
  84. if err != nil {
  85. t.Fatal("Could not parse the headers:", err)
  86. }
  87. // Recursively parse the MIME parts
  88. ps, err := parseMIMEParts(hdrs, tp.R)
  89. if err != nil {
  90. t.Fatal("Could not parse the MIME parts recursively:", err)
  91. }
  92. plainTextFound := false
  93. htmlFound := false
  94. imageFound := false
  95. if expected, actual := 3, len(ps); actual != expected {
  96. t.Error("Unexpected number of parts. Expected:", expected, "Was:", actual)
  97. }
  98. for _, part := range ps {
  99. // part has "header" and "body []byte"
  100. cd := part.header.Get("Content-Disposition")
  101. ct := part.header.Get("Content-Type")
  102. if strings.Contains(ct, "image/png") && strings.HasPrefix(cd, "inline") {
  103. imageFound = true
  104. }
  105. if strings.Contains(ct, "text/html") {
  106. htmlFound = true
  107. }
  108. if strings.Contains(ct, "text/plain") {
  109. plainTextFound = true
  110. }
  111. }
  112. if !plainTextFound {
  113. t.Error("Did not find plain text part.")
  114. }
  115. if !htmlFound {
  116. t.Error("Did not find HTML part.")
  117. }
  118. if !imageFound {
  119. t.Error("Did not find image part.")
  120. }
  121. }
  122. func TestEmailWithHTMLAttachmentsHTMLOnly(t *testing.T) {
  123. e := prepareEmail()
  124. e.HTML = []byte("<html><body>This is a text.</body></html>")
  125. // Set HTML attachment to exercise "mime/related".
  126. attachment, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "image/png; charset=utf-8")
  127. if err != nil {
  128. t.Fatal("Could not add an attachment to the message: ", err)
  129. }
  130. attachment.HTMLRelated = true
  131. b, err := e.Bytes()
  132. if err != nil {
  133. t.Fatal("Could not serialize e-mail:", err)
  134. }
  135. // Print the bytes for ocular validation and make sure no errors.
  136. //fmt.Println(string(b))
  137. // TODO: Verify the attachments.
  138. s := &trimReader{rd: bytes.NewBuffer(b)}
  139. tp := textproto.NewReader(bufio.NewReader(s))
  140. // Parse the main headers
  141. hdrs, err := tp.ReadMIMEHeader()
  142. if err != nil {
  143. t.Fatal("Could not parse the headers:", err)
  144. }
  145. if !strings.HasPrefix(hdrs.Get("Content-Type"), "multipart/related") {
  146. t.Error("Envelope Content-Type is not multipart/related: ", hdrs["Content-Type"])
  147. }
  148. // Recursively parse the MIME parts
  149. ps, err := parseMIMEParts(hdrs, tp.R)
  150. if err != nil {
  151. t.Fatal("Could not parse the MIME parts recursively:", err)
  152. }
  153. htmlFound := false
  154. imageFound := false
  155. if expected, actual := 2, len(ps); actual != expected {
  156. t.Error("Unexpected number of parts. Expected:", expected, "Was:", actual)
  157. }
  158. for _, part := range ps {
  159. // part has "header" and "body []byte"
  160. ct := part.header.Get("Content-Type")
  161. if strings.Contains(ct, "image/png") {
  162. imageFound = true
  163. }
  164. if strings.Contains(ct, "text/html") {
  165. htmlFound = true
  166. }
  167. }
  168. if !htmlFound {
  169. t.Error("Did not find HTML part.")
  170. }
  171. if !imageFound {
  172. t.Error("Did not find image part.")
  173. }
  174. }
  175. func TestEmailHTML(t *testing.T) {
  176. e := prepareEmail()
  177. e.HTML = []byte("<h1>Fancy Html is supported, too!</h1>\n")
  178. msg := basicTests(t, e)
  179. // Were the right headers set?
  180. ct := msg.Header.Get("Content-type")
  181. mt, _, err := mime.ParseMediaType(ct)
  182. if err != nil {
  183. t.Fatalf("Content-type header is invalid: %#v", ct)
  184. } else if mt != "text/html" {
  185. t.Fatalf("Content-type expected \"text/html\", not %v", mt)
  186. }
  187. }
  188. func TestEmailTextAttachment(t *testing.T) {
  189. e := prepareEmail()
  190. e.Text = []byte("Text Body is, of course, supported!\n")
  191. _, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "text/plain; charset=utf-8")
  192. if err != nil {
  193. t.Fatal("Could not add an attachment to the message: ", err)
  194. }
  195. msg := basicTests(t, e)
  196. // Were the right headers set?
  197. ct := msg.Header.Get("Content-type")
  198. mt, params, err := mime.ParseMediaType(ct)
  199. if err != nil {
  200. t.Fatal("Content-type header is invalid: ", ct)
  201. } else if mt != "multipart/mixed" {
  202. t.Fatalf("Content-type expected \"multipart/mixed\", not %v", mt)
  203. }
  204. b := params["boundary"]
  205. if b == "" {
  206. t.Fatalf("Invalid or missing boundary parameter: %#v", b)
  207. }
  208. if len(params) != 1 {
  209. t.Fatal("Unexpected content-type parameters")
  210. }
  211. // Is the generated message parsable?
  212. mixed := multipart.NewReader(msg.Body, params["boundary"])
  213. text, err := mixed.NextPart()
  214. if err != nil {
  215. t.Fatalf("Could not find text component of email: %s", err)
  216. }
  217. // Does the text portion match what we expect?
  218. mt, _, err = mime.ParseMediaType(text.Header.Get("Content-type"))
  219. if err != nil {
  220. t.Fatal("Could not parse message's Content-Type")
  221. } else if mt != "text/plain" {
  222. t.Fatal("Message missing text/plain")
  223. }
  224. plainText, err := ioutil.ReadAll(text)
  225. if err != nil {
  226. t.Fatal("Could not read plain text component of message: ", err)
  227. }
  228. if !bytes.Equal(plainText, []byte("Text Body is, of course, supported!\r\n")) {
  229. t.Fatalf("Plain text is broken: %#q", plainText)
  230. }
  231. // Check attachments.
  232. _, err = mixed.NextPart()
  233. if err != nil {
  234. t.Fatalf("Could not find attachment component of email: %s", err)
  235. }
  236. if _, err = mixed.NextPart(); err != io.EOF {
  237. t.Error("Expected only text and one attachment!")
  238. }
  239. }
  240. func TestEmailTextHtmlAttachment(t *testing.T) {
  241. e := prepareEmail()
  242. e.Text = []byte("Text Body is, of course, supported!\n")
  243. e.HTML = []byte("<h1>Fancy Html is supported, too!</h1>\n")
  244. _, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "text/plain; charset=utf-8")
  245. if err != nil {
  246. t.Fatal("Could not add an attachment to the message: ", err)
  247. }
  248. msg := basicTests(t, e)
  249. // Were the right headers set?
  250. ct := msg.Header.Get("Content-type")
  251. mt, params, err := mime.ParseMediaType(ct)
  252. if err != nil {
  253. t.Fatal("Content-type header is invalid: ", ct)
  254. } else if mt != "multipart/mixed" {
  255. t.Fatalf("Content-type expected \"multipart/mixed\", not %v", mt)
  256. }
  257. b := params["boundary"]
  258. if b == "" {
  259. t.Fatal("Unexpected empty boundary parameter")
  260. }
  261. if len(params) != 1 {
  262. t.Fatal("Unexpected content-type parameters")
  263. }
  264. // Is the generated message parsable?
  265. mixed := multipart.NewReader(msg.Body, params["boundary"])
  266. text, err := mixed.NextPart()
  267. if err != nil {
  268. t.Fatalf("Could not find text component of email: %s", err)
  269. }
  270. // Does the text portion match what we expect?
  271. mt, params, err = mime.ParseMediaType(text.Header.Get("Content-type"))
  272. if err != nil {
  273. t.Fatal("Could not parse message's Content-Type")
  274. } else if mt != "multipart/alternative" {
  275. t.Fatal("Message missing multipart/alternative")
  276. }
  277. mpReader := multipart.NewReader(text, params["boundary"])
  278. part, err := mpReader.NextPart()
  279. if err != nil {
  280. t.Fatal("Could not read plain text component of message: ", err)
  281. }
  282. plainText, err := ioutil.ReadAll(part)
  283. if err != nil {
  284. t.Fatal("Could not read plain text component of message: ", err)
  285. }
  286. if !bytes.Equal(plainText, []byte("Text Body is, of course, supported!\r\n")) {
  287. t.Fatalf("Plain text is broken: %#q", plainText)
  288. }
  289. // Check attachments.
  290. _, err = mixed.NextPart()
  291. if err != nil {
  292. t.Fatalf("Could not find attachment component of email: %s", err)
  293. }
  294. if _, err = mixed.NextPart(); err != io.EOF {
  295. t.Error("Expected only text and one attachment!")
  296. }
  297. }
  298. func TestEmailAttachment(t *testing.T) {
  299. e := prepareEmail()
  300. _, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "text/plain; charset=utf-8")
  301. if err != nil {
  302. t.Fatal("Could not add an attachment to the message: ", err)
  303. }
  304. msg := basicTests(t, e)
  305. // Were the right headers set?
  306. ct := msg.Header.Get("Content-type")
  307. mt, params, err := mime.ParseMediaType(ct)
  308. if err != nil {
  309. t.Fatal("Content-type header is invalid: ", ct)
  310. } else if mt != "multipart/mixed" {
  311. t.Fatalf("Content-type expected \"multipart/mixed\", not %v", mt)
  312. }
  313. b := params["boundary"]
  314. if b == "" {
  315. t.Fatal("Unexpected empty boundary parameter")
  316. }
  317. if len(params) != 1 {
  318. t.Fatal("Unexpected content-type parameters")
  319. }
  320. // Is the generated message parsable?
  321. mixed := multipart.NewReader(msg.Body, params["boundary"])
  322. // Check attachments.
  323. a, err := mixed.NextPart()
  324. if err != nil {
  325. t.Fatalf("Could not find attachment component of email: %s", err)
  326. }
  327. if !strings.HasPrefix(a.Header.Get("Content-Disposition"), "attachment") {
  328. t.Fatalf("Content disposition is not attachment: %s", a.Header.Get("Content-Disposition"))
  329. }
  330. if _, err = mixed.NextPart(); err != io.EOF {
  331. t.Error("Expected only one attachment!")
  332. }
  333. }
  334. func TestHeaderEncoding(t *testing.T) {
  335. cases := []struct {
  336. field string
  337. have string
  338. want string
  339. }{
  340. {
  341. field: "From",
  342. have: "Needs Encóding <encoding@example.com>, Only ASCII <foo@example.com>",
  343. want: "=?utf-8?q?Needs_Enc=C3=B3ding?= <encoding@example.com>, \"Only ASCII\" <foo@example.com>\r\n",
  344. },
  345. {
  346. field: "To",
  347. have: "Keith Moore <moore@cs.utk.edu>, Keld Jørn Simonsen <keld@dkuug.dk>",
  348. want: "\"Keith Moore\" <moore@cs.utk.edu>, =?utf-8?q?Keld_J=C3=B8rn_Simonsen?= <keld@dkuug.dk>\r\n",
  349. },
  350. {
  351. field: "Cc",
  352. have: "Needs Encóding <encoding@example.com>, \"Test :)\" <test@localhost>",
  353. want: "=?utf-8?q?Needs_Enc=C3=B3ding?= <encoding@example.com>, \"Test :)\" <test@localhost>\r\n",
  354. },
  355. {
  356. field: "Subject",
  357. have: "Subject with a 🐟",
  358. want: "=?UTF-8?q?Subject_with_a_=F0=9F=90=9F?=\r\n",
  359. },
  360. {
  361. field: "Subject",
  362. have: "Subject with only ASCII",
  363. want: "Subject with only ASCII\r\n",
  364. },
  365. }
  366. buff := &bytes.Buffer{}
  367. for _, c := range cases {
  368. header := make(textproto.MIMEHeader)
  369. header.Add(c.field, c.have)
  370. buff.Reset()
  371. headerToBytes(buff, header)
  372. want := fmt.Sprintf("%s: %s", c.field, c.want)
  373. got := buff.String()
  374. if got != want {
  375. t.Errorf("invalid utf-8 header encoding. \nwant:%#v\ngot: %#v", want, got)
  376. }
  377. }
  378. }
  379. func TestEmailFromReader(t *testing.T) {
  380. ex := &Email{
  381. Subject: "Test Subject",
  382. To: []string{"Jordan Wright <jmwright798@gmail.com>", "also@example.com"},
  383. From: "Jordan Wright <jmwright798@gmail.com>",
  384. ReplyTo: []string{"Jordan Wright <jmwright798@gmail.com>"},
  385. Cc: []string{"one@example.com", "Two <two@example.com>"},
  386. Bcc: []string{"three@example.com", "Four <four@example.com>"},
  387. Text: []byte("This is a test email with HTML Formatting. It also has very long lines so\nthat the content must be wrapped if using quoted-printable decoding.\n"),
  388. HTML: []byte("<div dir=\"ltr\">This is a test email with <b>HTML Formatting.</b>\u00a0It also has very long lines so that the content must be wrapped if using quoted-printable decoding.</div>\n"),
  389. }
  390. raw := []byte(`
  391. MIME-Version: 1.0
  392. Subject: Test Subject
  393. From: Jordan Wright <jmwright798@gmail.com>
  394. Reply-To: Jordan Wright <jmwright798@gmail.com>
  395. To: Jordan Wright <jmwright798@gmail.com>, also@example.com
  396. Cc: one@example.com, Two <two@example.com>
  397. Bcc: three@example.com, Four <four@example.com>
  398. Content-Type: multipart/alternative; boundary=001a114fb3fc42fd6b051f834280
  399. --001a114fb3fc42fd6b051f834280
  400. Content-Type: text/plain; charset=UTF-8
  401. This is a test email with HTML Formatting. It also has very long lines so
  402. that the content must be wrapped if using quoted-printable decoding.
  403. --001a114fb3fc42fd6b051f834280
  404. Content-Type: text/html; charset=UTF-8
  405. Content-Transfer-Encoding: quoted-printable
  406. <div dir=3D"ltr">This is a test email with <b>HTML Formatting.</b>=C2=A0It =
  407. also has very long lines so that the content must be wrapped if using quote=
  408. d-printable decoding.</div>
  409. --001a114fb3fc42fd6b051f834280--`)
  410. e, err := NewEmailFromReader(bytes.NewReader(raw))
  411. if err != nil {
  412. t.Fatalf("Error creating email %s", err.Error())
  413. }
  414. if e.Subject != ex.Subject {
  415. t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject)
  416. }
  417. if !bytes.Equal(e.Text, ex.Text) {
  418. t.Fatalf("Incorrect text: %#q != %#q", e.Text, ex.Text)
  419. }
  420. if !bytes.Equal(e.HTML, ex.HTML) {
  421. t.Fatalf("Incorrect HTML: %#q != %#q", e.HTML, ex.HTML)
  422. }
  423. if e.From != ex.From {
  424. t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From)
  425. }
  426. if len(e.To) != len(ex.To) {
  427. t.Fatalf("Incorrect number of \"To\" addresses: %v != %v", len(e.To), len(ex.To))
  428. }
  429. if e.To[0] != ex.To[0] {
  430. t.Fatalf("Incorrect \"To[0]\": %#q != %#q", e.To[0], ex.To[0])
  431. }
  432. if e.To[1] != ex.To[1] {
  433. t.Fatalf("Incorrect \"To[1]\": %#q != %#q", e.To[1], ex.To[1])
  434. }
  435. if len(e.Cc) != len(ex.Cc) {
  436. t.Fatalf("Incorrect number of \"Cc\" addresses: %v != %v", len(e.Cc), len(ex.Cc))
  437. }
  438. if e.Cc[0] != ex.Cc[0] {
  439. t.Fatalf("Incorrect \"Cc[0]\": %#q != %#q", e.Cc[0], ex.Cc[0])
  440. }
  441. if e.Cc[1] != ex.Cc[1] {
  442. t.Fatalf("Incorrect \"Cc[1]\": %#q != %#q", e.Cc[1], ex.Cc[1])
  443. }
  444. if len(e.Bcc) != len(ex.Bcc) {
  445. t.Fatalf("Incorrect number of \"Bcc\" addresses: %v != %v", len(e.Bcc), len(ex.Bcc))
  446. }
  447. if e.Bcc[0] != ex.Bcc[0] {
  448. t.Fatalf("Incorrect \"Bcc[0]\": %#q != %#q", e.Cc[0], ex.Cc[0])
  449. }
  450. if e.Bcc[1] != ex.Bcc[1] {
  451. t.Fatalf("Incorrect \"Bcc[1]\": %#q != %#q", e.Bcc[1], ex.Bcc[1])
  452. }
  453. if len(e.ReplyTo) != len(ex.ReplyTo) {
  454. t.Fatalf("Incorrect number of \"Reply-To\" addresses: %v != %v", len(e.ReplyTo), len(ex.ReplyTo))
  455. }
  456. if e.ReplyTo[0] != ex.ReplyTo[0] {
  457. t.Fatalf("Incorrect \"ReplyTo\": %#q != %#q", e.ReplyTo[0], ex.ReplyTo[0])
  458. }
  459. }
  460. func TestNonAsciiEmailFromReader(t *testing.T) {
  461. ex := &Email{
  462. Subject: "Test Subject",
  463. To: []string{"Anaïs <anais@example.org>"},
  464. Cc: []string{"Patrik Fältström <paf@example.com>"},
  465. From: "Mrs Valérie Dupont <valerie.dupont@example.com>",
  466. Text: []byte("This is a test message!"),
  467. }
  468. raw := []byte(`
  469. MIME-Version: 1.0
  470. Subject: =?UTF-8?Q?Test Subject?=
  471. From: Mrs =?ISO-8859-1?Q?Val=C3=A9rie=20Dupont?= <valerie.dupont@example.com>
  472. To: =?utf-8?q?Ana=C3=AFs?= <anais@example.org>
  473. Cc: =?ISO-8859-1?Q?Patrik_F=E4ltstr=F6m?= <paf@example.com>
  474. Content-type: text/plain; charset=ISO-8859-1
  475. This is a test message!`)
  476. e, err := NewEmailFromReader(bytes.NewReader(raw))
  477. if err != nil {
  478. t.Fatalf("Error creating email %s", err.Error())
  479. }
  480. if e.Subject != ex.Subject {
  481. t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject)
  482. }
  483. if e.From != ex.From {
  484. t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From)
  485. }
  486. if e.To[0] != ex.To[0] {
  487. t.Fatalf("Incorrect \"To\": %#q != %#q", e.To, ex.To)
  488. }
  489. if e.Cc[0] != ex.Cc[0] {
  490. t.Fatalf("Incorrect \"Cc\": %#q != %#q", e.Cc, ex.Cc)
  491. }
  492. }
  493. func TestNonMultipartEmailFromReader(t *testing.T) {
  494. ex := &Email{
  495. Text: []byte("This is a test message!"),
  496. Subject: "Example Subject (no MIME Type)",
  497. Headers: textproto.MIMEHeader{},
  498. }
  499. ex.Headers.Add("Content-Type", "text/plain; charset=us-ascii")
  500. ex.Headers.Add("Message-ID", "<foobar@example.com>")
  501. raw := []byte(`From: "Foo Bar" <foobar@example.com>
  502. Content-Type: text/plain
  503. To: foobar@example.com
  504. Subject: Example Subject (no MIME Type)
  505. Message-ID: <foobar@example.com>
  506. This is a test message!`)
  507. e, err := NewEmailFromReader(bytes.NewReader(raw))
  508. if err != nil {
  509. t.Fatalf("Error creating email %s", err.Error())
  510. }
  511. if ex.Subject != e.Subject {
  512. t.Errorf("Incorrect subject. %#q != %#q\n", ex.Subject, e.Subject)
  513. }
  514. if !bytes.Equal(ex.Text, e.Text) {
  515. t.Errorf("Incorrect body. %#q != %#q\n", ex.Text, e.Text)
  516. }
  517. if ex.Headers.Get("Message-ID") != e.Headers.Get("Message-ID") {
  518. t.Errorf("Incorrect message ID. %#q != %#q\n", ex.Headers.Get("Message-ID"), e.Headers.Get("Message-ID"))
  519. }
  520. }
  521. func TestBase64EmailFromReader(t *testing.T) {
  522. ex := &Email{
  523. Subject: "Test Subject",
  524. To: []string{"Jordan Wright <jmwright798@gmail.com>"},
  525. From: "Jordan Wright <jmwright798@gmail.com>",
  526. Text: []byte("This is a test email with HTML Formatting. It also has very long lines so that the content must be wrapped if using quoted-printable decoding."),
  527. HTML: []byte("<div dir=\"ltr\">This is a test email with <b>HTML Formatting.</b>\u00a0It also has very long lines so that the content must be wrapped if using quoted-printable decoding.</div>\n"),
  528. }
  529. raw := []byte(`
  530. MIME-Version: 1.0
  531. Subject: Test Subject
  532. From: Jordan Wright <jmwright798@gmail.com>
  533. To: Jordan Wright <jmwright798@gmail.com>
  534. Content-Type: multipart/alternative; boundary=001a114fb3fc42fd6b051f834280
  535. --001a114fb3fc42fd6b051f834280
  536. Content-Type: text/plain; charset=UTF-8
  537. Content-Transfer-Encoding: base64
  538. VGhpcyBpcyBhIHRlc3QgZW1haWwgd2l0aCBIVE1MIEZvcm1hdHRpbmcuIEl0IGFsc28gaGFzIHZl
  539. cnkgbG9uZyBsaW5lcyBzbyB0aGF0IHRoZSBjb250ZW50IG11c3QgYmUgd3JhcHBlZCBpZiB1c2lu
  540. ZyBxdW90ZWQtcHJpbnRhYmxlIGRlY29kaW5nLg==
  541. --001a114fb3fc42fd6b051f834280
  542. Content-Type: text/html; charset=UTF-8
  543. Content-Transfer-Encoding: quoted-printable
  544. <div dir=3D"ltr">This is a test email with <b>HTML Formatting.</b>=C2=A0It =
  545. also has very long lines so that the content must be wrapped if using quote=
  546. d-printable decoding.</div>
  547. --001a114fb3fc42fd6b051f834280--`)
  548. e, err := NewEmailFromReader(bytes.NewReader(raw))
  549. if err != nil {
  550. t.Fatalf("Error creating email %s", err.Error())
  551. }
  552. if e.Subject != ex.Subject {
  553. t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject)
  554. }
  555. if !bytes.Equal(e.Text, ex.Text) {
  556. t.Fatalf("Incorrect text: %#q != %#q", e.Text, ex.Text)
  557. }
  558. if !bytes.Equal(e.HTML, ex.HTML) {
  559. t.Fatalf("Incorrect HTML: %#q != %#q", e.HTML, ex.HTML)
  560. }
  561. if e.From != ex.From {
  562. t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From)
  563. }
  564. }
  565. func TestAttachmentEmailFromReader(t *testing.T) {
  566. ex := &Email{
  567. Subject: "Test Subject",
  568. To: []string{"Jordan Wright <jmwright798@gmail.com>"},
  569. From: "Jordan Wright <jmwright798@gmail.com>",
  570. Text: []byte("Simple text body"),
  571. HTML: []byte("<div dir=\"ltr\">Simple HTML body</div>\n"),
  572. }
  573. a, err := ex.Attach(bytes.NewReader([]byte("Let's just pretend this is raw JPEG data.")), "cat.jpeg", "image/jpeg")
  574. if err != nil {
  575. t.Fatalf("Error attaching image %s", err.Error())
  576. }
  577. b, err := ex.Attach(bytes.NewReader([]byte("Let's just pretend this is raw JPEG data.")), "cat-inline.jpeg", "image/jpeg")
  578. if err != nil {
  579. t.Fatalf("Error attaching inline image %s", err.Error())
  580. }
  581. raw := []byte(`
  582. From: Jordan Wright <jmwright798@gmail.com>
  583. Date: Thu, 17 Oct 2019 08:55:37 +0100
  584. Mime-Version: 1.0
  585. Content-Type: multipart/mixed;
  586. boundary=35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7
  587. To: Jordan Wright <jmwright798@gmail.com>
  588. Subject: Test Subject
  589. --35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7
  590. Content-Type: multipart/alternative;
  591. boundary=b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c
  592. --b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c
  593. Content-Transfer-Encoding: quoted-printable
  594. Content-Type: text/plain; charset=UTF-8
  595. Simple text body
  596. --b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c
  597. Content-Transfer-Encoding: quoted-printable
  598. Content-Type: text/html; charset=UTF-8
  599. <div dir=3D"ltr">Simple HTML body</div>
  600. --b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c--
  601. --35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7
  602. Content-Disposition: attachment;
  603. filename="cat.jpeg"
  604. Content-Id: <cat.jpeg>
  605. Content-Transfer-Encoding: base64
  606. Content-Type: image/jpeg
  607. TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4=
  608. --35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7
  609. Content-Disposition: inline;
  610. filename="cat-inline.jpeg"
  611. Content-Id: <cat-inline.jpeg>
  612. Content-Transfer-Encoding: base64
  613. Content-Type: image/jpeg
  614. TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4=
  615. --35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7--`)
  616. e, err := NewEmailFromReader(bytes.NewReader(raw))
  617. if err != nil {
  618. t.Fatalf("Error creating email %s", err.Error())
  619. }
  620. if e.Subject != ex.Subject {
  621. t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject)
  622. }
  623. if !bytes.Equal(e.Text, ex.Text) {
  624. t.Fatalf("Incorrect text: %#q != %#q", e.Text, ex.Text)
  625. }
  626. if !bytes.Equal(e.HTML, ex.HTML) {
  627. t.Fatalf("Incorrect HTML: %#q != %#q", e.HTML, ex.HTML)
  628. }
  629. if e.From != ex.From {
  630. t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From)
  631. }
  632. if len(e.Attachments) != 2 {
  633. t.Fatalf("Incorrect number of attachments %d != %d", len(e.Attachments), 1)
  634. }
  635. if e.Attachments[0].Filename != a.Filename {
  636. t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[0].Filename, a.Filename)
  637. }
  638. if !bytes.Equal(e.Attachments[0].Content, a.Content) {
  639. t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[0].Content, a.Content)
  640. }
  641. if e.Attachments[1].Filename != b.Filename {
  642. t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[1].Filename, b.Filename)
  643. }
  644. if !bytes.Equal(e.Attachments[1].Content, b.Content) {
  645. t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[1].Content, b.Content)
  646. }
  647. }
  648. func ExampleGmail() {
  649. e := NewEmail()
  650. e.From = "Jordan Wright <test@gmail.com>"
  651. e.To = []string{"test@example.com"}
  652. e.Bcc = []string{"test_bcc@example.com"}
  653. e.Cc = []string{"test_cc@example.com"}
  654. e.Subject = "Awesome Subject"
  655. e.Text = []byte("Text Body is, of course, supported!\n")
  656. e.HTML = []byte("<h1>Fancy Html is supported, too!</h1>\n")
  657. e.Send("smtp.gmail.com:587", smtp.PlainAuth("", e.From, "password123", "smtp.gmail.com"))
  658. }
  659. func ExampleAttach() {
  660. e := NewEmail()
  661. e.AttachFile("test.txt")
  662. }
  663. func Test_base64Wrap(t *testing.T) {
  664. file := "I'm a file long enough to force the function to wrap a\n" +
  665. "couple of lines, but I stop short of the end of one line and\n" +
  666. "have some padding dangling at the end."
  667. encoded := "SSdtIGEgZmlsZSBsb25nIGVub3VnaCB0byBmb3JjZSB0aGUgZnVuY3Rpb24gdG8gd3JhcCBhCmNv\r\n" +
  668. "dXBsZSBvZiBsaW5lcywgYnV0IEkgc3RvcCBzaG9ydCBvZiB0aGUgZW5kIG9mIG9uZSBsaW5lIGFu\r\n" +
  669. "ZApoYXZlIHNvbWUgcGFkZGluZyBkYW5nbGluZyBhdCB0aGUgZW5kLg==\r\n"
  670. var buf bytes.Buffer
  671. base64Wrap(&buf, []byte(file))
  672. if !bytes.Equal(buf.Bytes(), []byte(encoded)) {
  673. t.Fatalf("Encoded file does not match expected: %#q != %#q", string(buf.Bytes()), encoded)
  674. }
  675. }
  676. // *Since the mime library in use by ```email``` is now in the stdlib, this test is deprecated
  677. func Test_quotedPrintEncode(t *testing.T) {
  678. var buf bytes.Buffer
  679. text := []byte("Dear reader!\n\n" +
  680. "This is a test email to try and capture some of the corner cases that exist within\n" +
  681. "the quoted-printable encoding.\n" +
  682. "There are some wacky parts like =, and this input assumes UNIX line breaks so\r\n" +
  683. "it can come out a little weird. Also, we need to support unicode so here's a fish: 🐟\n")
  684. expected := []byte("Dear reader!\r\n\r\n" +
  685. "This is a test email to try and capture some of the corner cases that exist=\r\n" +
  686. " within\r\n" +
  687. "the quoted-printable encoding.\r\n" +
  688. "There are some wacky parts like =3D, and this input assumes UNIX line break=\r\n" +
  689. "s so\r\n" +
  690. "it can come out a little weird. Also, we need to support unicode so here's=\r\n" +
  691. " a fish: =F0=9F=90=9F\r\n")
  692. qp := quotedprintable.NewWriter(&buf)
  693. if _, err := qp.Write(text); err != nil {
  694. t.Fatal("quotePrintEncode: ", err)
  695. }
  696. if err := qp.Close(); err != nil {
  697. t.Fatal("Error closing writer", err)
  698. }
  699. if b := buf.Bytes(); !bytes.Equal(b, expected) {
  700. t.Errorf("quotedPrintEncode generated incorrect results: %#q != %#q", b, expected)
  701. }
  702. }
  703. func TestMultipartNoContentType(t *testing.T) {
  704. raw := []byte(`From: Mikhail Gusarov <dottedmag@dottedmag.net>
  705. To: notmuch@notmuchmail.org
  706. References: <20091117190054.GU3165@dottiness.seas.harvard.edu>
  707. Date: Wed, 18 Nov 2009 01:02:38 +0600
  708. Message-ID: <87iqd9rn3l.fsf@vertex.dottedmag>
  709. MIME-Version: 1.0
  710. Subject: Re: [notmuch] Working with Maildir storage?
  711. Content-Type: multipart/mixed; boundary="===============1958295626=="
  712. --===============1958295626==
  713. Content-Type: multipart/signed; boundary="=-=-=";
  714. micalg=pgp-sha1; protocol="application/pgp-signature"
  715. --=-=-=
  716. Content-Transfer-Encoding: quoted-printable
  717. Twas brillig at 14:00:54 17.11.2009 UTC-05 when lars@seas.harvard.edu did g=
  718. yre and gimble:
  719. --=-=-=
  720. Content-Type: application/pgp-signature
  721. -----BEGIN PGP SIGNATURE-----
  722. Version: GnuPG v1.4.9 (GNU/Linux)
  723. iQIcBAEBAgAGBQJLAvNOAAoJEJ0g9lA+M4iIjLYQAKp0PXEgl3JMOEBisH52AsIK
  724. =/ksP
  725. -----END PGP SIGNATURE-----
  726. --=-=-=--
  727. --===============1958295626==
  728. Content-Type: text/plain; charset="us-ascii"
  729. MIME-Version: 1.0
  730. Content-Transfer-Encoding: 7bit
  731. Content-Disposition: inline
  732. Testing!
  733. --===============1958295626==--
  734. `)
  735. e, err := NewEmailFromReader(bytes.NewReader(raw))
  736. if err != nil {
  737. t.Fatalf("Error when parsing email %s", err.Error())
  738. }
  739. if !bytes.Equal(e.Text, []byte("Testing!")) {
  740. t.Fatalf("Error incorrect text: %#q != %#q\n", e.Text, "Testing!")
  741. }
  742. }
  743. func TestNoMultipartHTMLContentTypeBase64Encoding(t *testing.T) {
  744. raw := []byte(`MIME-Version: 1.0
  745. From: no-reply@example.com
  746. To: tester@example.org
  747. Date: 7 Jan 2021 03:07:44 -0800
  748. Subject: Hello
  749. Content-Type: text/html; charset=utf-8
  750. Content-Transfer-Encoding: base64
  751. Message-Id: <20210107110744.547DD70532@example.com>
  752. PGh0bWw+PGhlYWQ+PHRpdGxlPnRlc3Q8L3RpdGxlPjwvaGVhZD48Ym9keT5IZWxsbyB3
  753. b3JsZCE8L2JvZHk+PC9odG1sPg==
  754. `)
  755. e, err := NewEmailFromReader(bytes.NewReader(raw))
  756. if err != nil {
  757. t.Fatalf("Error when parsing email %s", err.Error())
  758. }
  759. if !bytes.Equal(e.HTML, []byte("<html><head><title>test</title></head><body>Hello world!</body></html>")) {
  760. t.Fatalf("Error incorrect text: %#q != %#q\n", e.Text, "<html>...</html>")
  761. }
  762. }
  763. // *Since the mime library in use by ```email``` is now in the stdlib, this test is deprecated
  764. func Test_quotedPrintDecode(t *testing.T) {
  765. text := []byte("Dear reader!\r\n\r\n" +
  766. "This is a test email to try and capture some of the corner cases that exist=\r\n" +
  767. " within\r\n" +
  768. "the quoted-printable encoding.\r\n" +
  769. "There are some wacky parts like =3D, and this input assumes UNIX line break=\r\n" +
  770. "s so\r\n" +
  771. "it can come out a little weird. Also, we need to support unicode so here's=\r\n" +
  772. " a fish: =F0=9F=90=9F\r\n")
  773. expected := []byte("Dear reader!\r\n\r\n" +
  774. "This is a test email to try and capture some of the corner cases that exist within\r\n" +
  775. "the quoted-printable encoding.\r\n" +
  776. "There are some wacky parts like =, and this input assumes UNIX line breaks so\r\n" +
  777. "it can come out a little weird. Also, we need to support unicode so here's a fish: 🐟\r\n")
  778. qp := quotedprintable.NewReader(bytes.NewReader(text))
  779. got, err := ioutil.ReadAll(qp)
  780. if err != nil {
  781. t.Fatal("quotePrintDecode: ", err)
  782. }
  783. if !bytes.Equal(got, expected) {
  784. t.Errorf("quotedPrintDecode generated incorrect results: %#q != %#q", got, expected)
  785. }
  786. }
  787. func Benchmark_base64Wrap(b *testing.B) {
  788. // Reasonable base case; 128K random bytes
  789. file := make([]byte, 128*1024)
  790. if _, err := rand.Read(file); err != nil {
  791. panic(err)
  792. }
  793. for i := 0; i <= b.N; i++ {
  794. base64Wrap(ioutil.Discard, file)
  795. }
  796. }
  797. func TestParseSender(t *testing.T) {
  798. var cases = []struct {
  799. e Email
  800. want string
  801. haserr bool
  802. }{
  803. {
  804. Email{From: "from@test.com"},
  805. "from@test.com",
  806. false,
  807. },
  808. {
  809. Email{Sender: "sender@test.com", From: "from@test.com"},
  810. "sender@test.com",
  811. false,
  812. },
  813. {
  814. Email{Sender: "bad_address_sender"},
  815. "",
  816. true,
  817. },
  818. {
  819. Email{Sender: "good@sender.com", From: "bad_address_from"},
  820. "good@sender.com",
  821. false,
  822. },
  823. }
  824. for i, testcase := range cases {
  825. got, err := testcase.e.parseSender()
  826. if got != testcase.want || (err != nil) != testcase.haserr {
  827. t.Errorf(`%d: got %s != want %s or error "%t" != "%t"`, i+1, got, testcase.want, err != nil, testcase.haserr)
  828. }
  829. }
  830. }