1package mail
2
3import (
4	"bytes"
5	"encoding/base64"
6	"io"
7	"io/ioutil"
8	"path/filepath"
9	"regexp"
10	"strconv"
11	"strings"
12	"testing"
13	"time"
14)
15
16func init() {
17	now = func() time.Time {
18		return time.Date(2014, 06, 25, 17, 46, 0, 0, time.UTC)
19	}
20}
21
22type message struct {
23	from    string
24	to      []string
25	content string
26}
27
28func TestMessage(t *testing.T) {
29	m := NewMessage()
30	m.SetAddressHeader("From", "from@example.com", "Señor From")
31	m.SetHeader("To", m.FormatAddress("to@example.com", "Señor To"), "tobis@example.com")
32	m.SetAddressHeader("Cc", "cc@example.com", "A, B")
33	m.SetAddressHeader("X-To", "ccbis@example.com", "à, b")
34	m.SetDateHeader("X-Date", now())
35	m.SetHeader("X-Date-2", m.FormatDate(now()))
36	m.SetHeader("Subject", "¡Hola, señor!")
37	m.SetHeaders(map[string][]string{
38		"X-Headers": {"Test", "Café"},
39	})
40	m.SetBody("text/plain", "¡Hola, señor!")
41
42	want := &message{
43		from: "from@example.com",
44		to: []string{
45			"to@example.com",
46			"tobis@example.com",
47			"cc@example.com",
48		},
49		content: "From: =?UTF-8?q?Se=C3=B1or_From?= <from@example.com>\r\n" +
50			"To: =?UTF-8?q?Se=C3=B1or_To?= <to@example.com>, tobis@example.com\r\n" +
51			"Cc: \"A, B\" <cc@example.com>\r\n" +
52			"X-To: =?UTF-8?b?w6AsIGI=?= <ccbis@example.com>\r\n" +
53			"X-Date: Wed, 25 Jun 2014 17:46:00 +0000\r\n" +
54			"X-Date-2: Wed, 25 Jun 2014 17:46:00 +0000\r\n" +
55			"X-Headers: Test, =?UTF-8?q?Caf=C3=A9?=\r\n" +
56			"Subject: =?UTF-8?q?=C2=A1Hola,_se=C3=B1or!?=\r\n" +
57			"Content-Type: text/plain; charset=UTF-8\r\n" +
58			"Content-Transfer-Encoding: quoted-printable\r\n" +
59			"\r\n" +
60			"=C2=A1Hola, se=C3=B1or!",
61	}
62
63	testMessage(t, m, 0, want)
64}
65
66func TestCustomMessage(t *testing.T) {
67	m := NewMessage(SetCharset("ISO-8859-1"), SetEncoding(Base64))
68	m.SetHeaders(map[string][]string{
69		"From":    {"from@example.com"},
70		"To":      {"to@example.com"},
71		"Subject": {"Café"},
72	})
73	m.SetBody("text/html", "¡Hola, señor!")
74
75	want := &message{
76		from: "from@example.com",
77		to:   []string{"to@example.com"},
78		content: "From: from@example.com\r\n" +
79			"To: to@example.com\r\n" +
80			"Subject: =?ISO-8859-1?b?Q2Fmw6k=?=\r\n" +
81			"Content-Type: text/html; charset=ISO-8859-1\r\n" +
82			"Content-Transfer-Encoding: base64\r\n" +
83			"\r\n" +
84			"wqFIb2xhLCBzZcOxb3Ih",
85	}
86
87	testMessage(t, m, 0, want)
88}
89
90func TestUnencodedMessage(t *testing.T) {
91	m := NewMessage(SetEncoding(Unencoded))
92	m.SetHeaders(map[string][]string{
93		"From":    {"from@example.com"},
94		"To":      {"to@example.com"},
95		"Subject": {"Café"},
96	})
97	m.SetBody("text/html", "¡Hola, señor!")
98
99	want := &message{
100		from: "from@example.com",
101		to:   []string{"to@example.com"},
102		content: "From: from@example.com\r\n" +
103			"To: to@example.com\r\n" +
104			"Subject: =?UTF-8?q?Caf=C3=A9?=\r\n" +
105			"Content-Type: text/html; charset=UTF-8\r\n" +
106			"Content-Transfer-Encoding: 8bit\r\n" +
107			"\r\n" +
108			"¡Hola, señor!",
109	}
110
111	testMessage(t, m, 0, want)
112}
113
114func TestRecipients(t *testing.T) {
115	m := NewMessage()
116	m.SetHeaders(map[string][]string{
117		"From":    {"from@example.com"},
118		"To":      {"to@example.com"},
119		"Cc":      {"cc@example.com"},
120		"Bcc":     {"bcc1@example.com", "bcc2@example.com"},
121		"Subject": {"Hello!"},
122	})
123	m.SetBody("text/plain", "Test message")
124
125	want := &message{
126		from: "from@example.com",
127		to:   []string{"to@example.com", "cc@example.com", "bcc1@example.com", "bcc2@example.com"},
128		content: "From: from@example.com\r\n" +
129			"To: to@example.com\r\n" +
130			"Cc: cc@example.com\r\n" +
131			"Subject: Hello!\r\n" +
132			"Content-Type: text/plain; charset=UTF-8\r\n" +
133			"Content-Transfer-Encoding: quoted-printable\r\n" +
134			"\r\n" +
135			"Test message",
136	}
137
138	testMessage(t, m, 0, want)
139}
140
141func TestAlternative(t *testing.T) {
142	m := NewMessage()
143	m.SetHeader("From", "from@example.com")
144	m.SetHeader("To", "to@example.com")
145	m.SetBody("text/plain", "¡Hola, señor!")
146	m.AddAlternative("text/html", "¡<b>Hola</b>, <i>señor</i>!</h1>")
147
148	want := &message{
149		from: "from@example.com",
150		to:   []string{"to@example.com"},
151		content: "From: from@example.com\r\n" +
152			"To: to@example.com\r\n" +
153			"Content-Type: multipart/alternative;\r\n" +
154			" boundary=_BOUNDARY_1_\r\n" +
155			"\r\n" +
156			"--_BOUNDARY_1_\r\n" +
157			"Content-Type: text/plain; charset=UTF-8\r\n" +
158			"Content-Transfer-Encoding: quoted-printable\r\n" +
159			"\r\n" +
160			"=C2=A1Hola, se=C3=B1or!\r\n" +
161			"--_BOUNDARY_1_\r\n" +
162			"Content-Type: text/html; charset=UTF-8\r\n" +
163			"Content-Transfer-Encoding: quoted-printable\r\n" +
164			"\r\n" +
165			"=C2=A1<b>Hola</b>, <i>se=C3=B1or</i>!</h1>\r\n" +
166			"--_BOUNDARY_1_--\r\n",
167	}
168
169	testMessage(t, m, 1, want)
170}
171
172func TestPartSetting(t *testing.T) {
173	m := NewMessage()
174	m.SetHeader("From", "from@example.com")
175	m.SetHeader("To", "to@example.com")
176	m.SetBody("text/plain; format=flowed", "¡Hola, señor!", SetPartEncoding(Unencoded))
177	m.AddAlternative("text/html", "¡<b>Hola</b>, <i>señor</i>!</h1>")
178
179	want := &message{
180		from: "from@example.com",
181		to:   []string{"to@example.com"},
182		content: "From: from@example.com\r\n" +
183			"To: to@example.com\r\n" +
184			"Content-Type: multipart/alternative;\r\n" +
185			" boundary=_BOUNDARY_1_\r\n" +
186			"\r\n" +
187			"--_BOUNDARY_1_\r\n" +
188			"Content-Type: text/plain; format=flowed; charset=UTF-8\r\n" +
189			"Content-Transfer-Encoding: 8bit\r\n" +
190			"\r\n" +
191			"¡Hola, señor!\r\n" +
192			"--_BOUNDARY_1_\r\n" +
193			"Content-Type: text/html; charset=UTF-8\r\n" +
194			"Content-Transfer-Encoding: quoted-printable\r\n" +
195			"\r\n" +
196			"=C2=A1<b>Hola</b>, <i>se=C3=B1or</i>!</h1>\r\n" +
197			"--_BOUNDARY_1_--\r\n",
198	}
199
200	testMessage(t, m, 1, want)
201}
202
203func TestPartSettingWithCustomBoundary(t *testing.T) {
204	m := NewMessage()
205	m.SetBoundary("lalalaDaiMne3Ryblya")
206	m.SetHeader("From", "from@example.com")
207	m.SetHeader("To", "to@example.com")
208	m.SetBody("text/plain; format=flowed", "¡Hola, señor!", SetPartEncoding(Unencoded))
209	m.AddAlternative("text/html", "¡<b>Hola</b>, <i>señor</i>!</h1>")
210
211	want := &message{
212		from: "from@example.com",
213		to:   []string{"to@example.com"},
214		content: "From: from@example.com\r\n" +
215			"To: to@example.com\r\n" +
216			"Content-Type: multipart/alternative;\r\n" +
217			" boundary=lalalaDaiMne3Ryblya\r\n" +
218			"\r\n" +
219			"--lalalaDaiMne3Ryblya\r\n" +
220			"Content-Type: text/plain; format=flowed; charset=UTF-8\r\n" +
221			"Content-Transfer-Encoding: 8bit\r\n" +
222			"\r\n" +
223			"¡Hola, señor!\r\n" +
224			"--lalalaDaiMne3Ryblya\r\n" +
225			"Content-Type: text/html; charset=UTF-8\r\n" +
226			"Content-Transfer-Encoding: quoted-printable\r\n" +
227			"\r\n" +
228			"=C2=A1<b>Hola</b>, <i>se=C3=B1or</i>!</h1>\r\n" +
229			"--lalalaDaiMne3Ryblya--\r\n",
230	}
231
232	testMessage(t, m, 1, want)
233}
234
235func TestBodyWriter(t *testing.T) {
236	m := NewMessage()
237	m.SetHeader("From", "from@example.com")
238	m.SetHeader("To", "to@example.com")
239	m.SetBodyWriter("text/plain", func(w io.Writer) error {
240		_, err := w.Write([]byte("Test message"))
241		return err
242	})
243	m.AddAlternativeWriter("text/html", func(w io.Writer) error {
244		_, err := w.Write([]byte("Test HTML"))
245		return err
246	})
247
248	want := &message{
249		from: "from@example.com",
250		to:   []string{"to@example.com"},
251		content: "From: from@example.com\r\n" +
252			"To: to@example.com\r\n" +
253			"Content-Type: multipart/alternative;\r\n" +
254			" boundary=_BOUNDARY_1_\r\n" +
255			"\r\n" +
256			"--_BOUNDARY_1_\r\n" +
257			"Content-Type: text/plain; charset=UTF-8\r\n" +
258			"Content-Transfer-Encoding: quoted-printable\r\n" +
259			"\r\n" +
260			"Test message\r\n" +
261			"--_BOUNDARY_1_\r\n" +
262			"Content-Type: text/html; charset=UTF-8\r\n" +
263			"Content-Transfer-Encoding: quoted-printable\r\n" +
264			"\r\n" +
265			"Test HTML\r\n" +
266			"--_BOUNDARY_1_--\r\n",
267	}
268
269	testMessage(t, m, 1, want)
270}
271
272func TestAttachmentReader(t *testing.T) {
273	m := NewMessage()
274	m.SetHeader("From", "from@example.com")
275	m.SetHeader("To", "to@example.com")
276
277	var b bytes.Buffer
278	b.Write([]byte("Test file"))
279	m.AttachReader("file.txt", &b)
280
281	want := &message{
282		from: "from@example.com",
283		to:   []string{"to@example.com"},
284		content: "From: from@example.com\r\n" +
285			"To: to@example.com\r\n" +
286			"Content-Type: text/plain; charset=utf-8; name=\"file.txt\"\r\n" +
287			"Content-Disposition: attachment; filename=\"file.txt\"\r\n" +
288			"Content-Transfer-Encoding: base64\r\n" +
289			"\r\n" +
290			base64.StdEncoding.EncodeToString([]byte("Test file")),
291	}
292
293	testMessage(t, m, 0, want)
294}
295
296func TestAttachmentOnly(t *testing.T) {
297	m := NewMessage()
298	m.SetHeader("From", "from@example.com")
299	m.SetHeader("To", "to@example.com")
300	m.Attach(mockCopyFile("/tmp/test.pdf"))
301
302	want := &message{
303		from: "from@example.com",
304		to:   []string{"to@example.com"},
305		content: "From: from@example.com\r\n" +
306			"To: to@example.com\r\n" +
307			"Content-Type: application/pdf; name=\"test.pdf\"\r\n" +
308			"Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
309			"Content-Transfer-Encoding: base64\r\n" +
310			"\r\n" +
311			base64.StdEncoding.EncodeToString([]byte("Content of test.pdf")),
312	}
313
314	testMessage(t, m, 0, want)
315}
316
317func TestAttachment(t *testing.T) {
318	m := NewMessage()
319	m.SetHeader("From", "from@example.com")
320	m.SetHeader("To", "to@example.com")
321	m.SetBody("text/plain", "Test")
322	m.Attach(mockCopyFile("/tmp/test.pdf"))
323
324	want := &message{
325		from: "from@example.com",
326		to:   []string{"to@example.com"},
327		content: "From: from@example.com\r\n" +
328			"To: to@example.com\r\n" +
329			"Content-Type: multipart/mixed;\r\n" +
330			" boundary=_BOUNDARY_1_\r\n" +
331			"\r\n" +
332			"--_BOUNDARY_1_\r\n" +
333			"Content-Type: text/plain; charset=UTF-8\r\n" +
334			"Content-Transfer-Encoding: quoted-printable\r\n" +
335			"\r\n" +
336			"Test\r\n" +
337			"--_BOUNDARY_1_\r\n" +
338			"Content-Type: application/pdf; name=\"test.pdf\"\r\n" +
339			"Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
340			"Content-Transfer-Encoding: base64\r\n" +
341			"\r\n" +
342			base64.StdEncoding.EncodeToString([]byte("Content of test.pdf")) + "\r\n" +
343			"--_BOUNDARY_1_--\r\n",
344	}
345
346	testMessage(t, m, 1, want)
347}
348
349func TestRename(t *testing.T) {
350	m := NewMessage()
351	m.SetHeader("From", "from@example.com")
352	m.SetHeader("To", "to@example.com")
353	m.SetBody("text/plain", "Test")
354	name, copy := mockCopyFile("/tmp/test.pdf")
355	rename := Rename("another.pdf")
356	m.Attach(name, copy, rename)
357
358	want := &message{
359		from: "from@example.com",
360		to:   []string{"to@example.com"},
361		content: "From: from@example.com\r\n" +
362			"To: to@example.com\r\n" +
363			"Content-Type: multipart/mixed;\r\n" +
364			" boundary=_BOUNDARY_1_\r\n" +
365			"\r\n" +
366			"--_BOUNDARY_1_\r\n" +
367			"Content-Type: text/plain; charset=UTF-8\r\n" +
368			"Content-Transfer-Encoding: quoted-printable\r\n" +
369			"\r\n" +
370			"Test\r\n" +
371			"--_BOUNDARY_1_\r\n" +
372			"Content-Type: application/pdf; name=\"another.pdf\"\r\n" +
373			"Content-Disposition: attachment; filename=\"another.pdf\"\r\n" +
374			"Content-Transfer-Encoding: base64\r\n" +
375			"\r\n" +
376			base64.StdEncoding.EncodeToString([]byte("Content of test.pdf")) + "\r\n" +
377			"--_BOUNDARY_1_--\r\n",
378	}
379
380	testMessage(t, m, 1, want)
381}
382
383func TestAttachmentsOnly(t *testing.T) {
384	m := NewMessage()
385	m.SetHeader("From", "from@example.com")
386	m.SetHeader("To", "to@example.com")
387	m.Attach(mockCopyFile("/tmp/test.pdf"))
388	m.Attach(mockCopyFile("/tmp/test.zip"))
389
390	want := &message{
391		from: "from@example.com",
392		to:   []string{"to@example.com"},
393		content: "From: from@example.com\r\n" +
394			"To: to@example.com\r\n" +
395			"Content-Type: multipart/mixed;\r\n" +
396			" boundary=_BOUNDARY_1_\r\n" +
397			"\r\n" +
398			"--_BOUNDARY_1_\r\n" +
399			"Content-Type: application/pdf; name=\"test.pdf\"\r\n" +
400			"Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
401			"Content-Transfer-Encoding: base64\r\n" +
402			"\r\n" +
403			base64.StdEncoding.EncodeToString([]byte("Content of test.pdf")) + "\r\n" +
404			"--_BOUNDARY_1_\r\n" +
405			"Content-Type: application/zip; name=\"test.zip\"\r\n" +
406			"Content-Disposition: attachment; filename=\"test.zip\"\r\n" +
407			"Content-Transfer-Encoding: base64\r\n" +
408			"\r\n" +
409			base64.StdEncoding.EncodeToString([]byte("Content of test.zip")) + "\r\n" +
410			"--_BOUNDARY_1_--\r\n",
411	}
412
413	testMessage(t, m, 1, want)
414}
415
416func TestAttachments(t *testing.T) {
417	m := NewMessage()
418	m.SetHeader("From", "from@example.com")
419	m.SetHeader("To", "to@example.com")
420	m.SetBody("text/plain", "Test")
421	m.Attach(mockCopyFile("/tmp/test.pdf"))
422	m.Attach(mockCopyFile("/tmp/test.zip"))
423
424	want := &message{
425		from: "from@example.com",
426		to:   []string{"to@example.com"},
427		content: "From: from@example.com\r\n" +
428			"To: to@example.com\r\n" +
429			"Content-Type: multipart/mixed;\r\n" +
430			" boundary=_BOUNDARY_1_\r\n" +
431			"\r\n" +
432			"--_BOUNDARY_1_\r\n" +
433			"Content-Type: text/plain; charset=UTF-8\r\n" +
434			"Content-Transfer-Encoding: quoted-printable\r\n" +
435			"\r\n" +
436			"Test\r\n" +
437			"--_BOUNDARY_1_\r\n" +
438			"Content-Type: application/pdf; name=\"test.pdf\"\r\n" +
439			"Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
440			"Content-Transfer-Encoding: base64\r\n" +
441			"\r\n" +
442			base64.StdEncoding.EncodeToString([]byte("Content of test.pdf")) + "\r\n" +
443			"--_BOUNDARY_1_\r\n" +
444			"Content-Type: application/zip; name=\"test.zip\"\r\n" +
445			"Content-Disposition: attachment; filename=\"test.zip\"\r\n" +
446			"Content-Transfer-Encoding: base64\r\n" +
447			"\r\n" +
448			base64.StdEncoding.EncodeToString([]byte("Content of test.zip")) + "\r\n" +
449			"--_BOUNDARY_1_--\r\n",
450	}
451
452	testMessage(t, m, 1, want)
453}
454
455func TestEmbeddedReader(t *testing.T) {
456	m := NewMessage()
457	m.SetHeader("From", "from@example.com")
458	m.SetHeader("To", "to@example.com")
459
460	var b bytes.Buffer
461	b.Write([]byte("Test file"))
462	m.EmbedReader("file.txt", &b)
463
464	want := &message{
465		from: "from@example.com",
466		to:   []string{"to@example.com"},
467		content: "From: from@example.com\r\n" +
468			"To: to@example.com\r\n" +
469			"Content-Type: text/plain; charset=utf-8; name=\"file.txt\"\r\n" +
470			"Content-Transfer-Encoding: base64\r\n" +
471			"Content-Disposition: inline; filename=\"file.txt\"\r\n" +
472			"Content-ID: <file.txt>\r\n" +
473			"\r\n" +
474			base64.StdEncoding.EncodeToString([]byte("Test file")),
475	}
476
477	testMessage(t, m, 0, want)
478}
479
480func TestEmbedded(t *testing.T) {
481	m := NewMessage()
482	m.SetHeader("From", "from@example.com")
483	m.SetHeader("To", "to@example.com")
484	m.Embed(mockCopyFileWithHeader(m, "image1.jpg", map[string][]string{"Content-ID": {"<test-content-id>"}}))
485	m.Embed(mockCopyFile("image2.jpg"))
486	m.SetBody("text/plain", "Test")
487
488	want := &message{
489		from: "from@example.com",
490		to:   []string{"to@example.com"},
491		content: "From: from@example.com\r\n" +
492			"To: to@example.com\r\n" +
493			"Content-Type: multipart/related;\r\n" +
494			" boundary=_BOUNDARY_1_\r\n" +
495			"\r\n" +
496			"--_BOUNDARY_1_\r\n" +
497			"Content-Type: text/plain; charset=UTF-8\r\n" +
498			"Content-Transfer-Encoding: quoted-printable\r\n" +
499			"\r\n" +
500			"Test\r\n" +
501			"--_BOUNDARY_1_\r\n" +
502			"Content-Type: image/jpeg; name=\"image1.jpg\"\r\n" +
503			"Content-Disposition: inline; filename=\"image1.jpg\"\r\n" +
504			"Content-ID: <test-content-id>\r\n" +
505			"Content-Transfer-Encoding: base64\r\n" +
506			"\r\n" +
507			base64.StdEncoding.EncodeToString([]byte("Content of image1.jpg")) + "\r\n" +
508			"--_BOUNDARY_1_\r\n" +
509			"Content-Type: image/jpeg; name=\"image2.jpg\"\r\n" +
510			"Content-Disposition: inline; filename=\"image2.jpg\"\r\n" +
511			"Content-ID: <image2.jpg>\r\n" +
512			"Content-Transfer-Encoding: base64\r\n" +
513			"\r\n" +
514			base64.StdEncoding.EncodeToString([]byte("Content of image2.jpg")) + "\r\n" +
515			"--_BOUNDARY_1_--\r\n",
516	}
517
518	testMessage(t, m, 1, want)
519}
520
521func TestFullMessage(t *testing.T) {
522	m := NewMessage()
523	m.SetHeader("From", "from@example.com")
524	m.SetHeader("To", "to@example.com")
525	m.SetBody("text/plain", "¡Hola, señor!")
526	m.AddAlternative("text/html", "¡<b>Hola</b>, <i>señor</i>!</h1>")
527	m.Attach(mockCopyFile("test.pdf"))
528	m.Embed(mockCopyFile("image.jpg"))
529
530	want := &message{
531		from: "from@example.com",
532		to:   []string{"to@example.com"},
533		content: "From: from@example.com\r\n" +
534			"To: to@example.com\r\n" +
535			"Content-Type: multipart/mixed;\r\n" +
536			" boundary=_BOUNDARY_1_\r\n" +
537			"\r\n" +
538			"--_BOUNDARY_1_\r\n" +
539			"Content-Type: multipart/related;\r\n" +
540			" boundary=_BOUNDARY_2_\r\n" +
541			"\r\n" +
542			"--_BOUNDARY_2_\r\n" +
543			"Content-Type: multipart/alternative;\r\n" +
544			" boundary=_BOUNDARY_3_\r\n" +
545			"\r\n" +
546			"--_BOUNDARY_3_\r\n" +
547			"Content-Type: text/plain; charset=UTF-8\r\n" +
548			"Content-Transfer-Encoding: quoted-printable\r\n" +
549			"\r\n" +
550			"=C2=A1Hola, se=C3=B1or!\r\n" +
551			"--_BOUNDARY_3_\r\n" +
552			"Content-Type: text/html; charset=UTF-8\r\n" +
553			"Content-Transfer-Encoding: quoted-printable\r\n" +
554			"\r\n" +
555			"=C2=A1<b>Hola</b>, <i>se=C3=B1or</i>!</h1>\r\n" +
556			"--_BOUNDARY_3_--\r\n" +
557			"\r\n" +
558			"--_BOUNDARY_2_\r\n" +
559			"Content-Type: image/jpeg; name=\"image.jpg\"\r\n" +
560			"Content-Disposition: inline; filename=\"image.jpg\"\r\n" +
561			"Content-ID: <image.jpg>\r\n" +
562			"Content-Transfer-Encoding: base64\r\n" +
563			"\r\n" +
564			base64.StdEncoding.EncodeToString([]byte("Content of image.jpg")) + "\r\n" +
565			"--_BOUNDARY_2_--\r\n" +
566			"\r\n" +
567			"--_BOUNDARY_1_\r\n" +
568			"Content-Type: application/pdf; name=\"test.pdf\"\r\n" +
569			"Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
570			"Content-Transfer-Encoding: base64\r\n" +
571			"\r\n" +
572			base64.StdEncoding.EncodeToString([]byte("Content of test.pdf")) + "\r\n" +
573			"--_BOUNDARY_1_--\r\n",
574	}
575
576	testMessage(t, m, 3, want)
577
578	want = &message{
579		from: "from@example.com",
580		to:   []string{"to@example.com"},
581		content: "From: from@example.com\r\n" +
582			"To: to@example.com\r\n" +
583			"Content-Type: text/plain; charset=UTF-8\r\n" +
584			"Content-Transfer-Encoding: quoted-printable\r\n" +
585			"\r\n" +
586			"Test reset",
587	}
588	m.Reset()
589	m.SetHeader("From", "from@example.com")
590	m.SetHeader("To", "to@example.com")
591	m.SetBody("text/plain", "Test reset")
592	testMessage(t, m, 0, want)
593}
594
595func TestQpLineLength(t *testing.T) {
596	m := NewMessage()
597	m.SetHeader("From", "from@example.com")
598	m.SetHeader("To", "to@example.com")
599	m.SetBody("text/plain",
600		strings.Repeat("0", 76)+"\r\n"+
601			strings.Repeat("0", 75)+"à\r\n"+
602			strings.Repeat("0", 74)+"à\r\n"+
603			strings.Repeat("0", 73)+"à\r\n"+
604			strings.Repeat("0", 72)+"à\r\n"+
605			strings.Repeat("0", 75)+"\r\n"+
606			strings.Repeat("0", 76)+"\n")
607
608	want := &message{
609		from: "from@example.com",
610		to:   []string{"to@example.com"},
611		content: "From: from@example.com\r\n" +
612			"To: to@example.com\r\n" +
613			"Content-Type: text/plain; charset=UTF-8\r\n" +
614			"Content-Transfer-Encoding: quoted-printable\r\n" +
615			"\r\n" +
616			strings.Repeat("0", 75) + "=\r\n0\r\n" +
617			strings.Repeat("0", 75) + "=\r\n=C3=A0\r\n" +
618			strings.Repeat("0", 74) + "=\r\n=C3=A0\r\n" +
619			strings.Repeat("0", 73) + "=\r\n=C3=A0\r\n" +
620			strings.Repeat("0", 72) + "=C3=\r\n=A0\r\n" +
621			strings.Repeat("0", 75) + "\r\n" +
622			strings.Repeat("0", 75) + "=\r\n0\r\n",
623	}
624
625	testMessage(t, m, 0, want)
626}
627
628func TestBase64LineLength(t *testing.T) {
629	m := NewMessage(SetCharset("UTF-8"), SetEncoding(Base64))
630	m.SetHeader("From", "from@example.com")
631	m.SetHeader("To", "to@example.com")
632	m.SetBody("text/plain", strings.Repeat("0", 58))
633
634	want := &message{
635		from: "from@example.com",
636		to:   []string{"to@example.com"},
637		content: "From: from@example.com\r\n" +
638			"To: to@example.com\r\n" +
639			"Content-Type: text/plain; charset=UTF-8\r\n" +
640			"Content-Transfer-Encoding: base64\r\n" +
641			"\r\n" +
642			strings.Repeat("MDAw", 19) + "\r\nMA==",
643	}
644
645	testMessage(t, m, 0, want)
646}
647
648func TestEmptyName(t *testing.T) {
649	m := NewMessage()
650	m.SetAddressHeader("From", "from@example.com", "")
651
652	want := &message{
653		from:    "from@example.com",
654		content: "From: from@example.com\r\n",
655	}
656
657	testMessage(t, m, 0, want)
658}
659
660func TestEmptyHeader(t *testing.T) {
661	m := NewMessage()
662	m.SetHeaders(map[string][]string{
663		"From":    {"from@example.com"},
664		"X-Empty": nil,
665	})
666
667	want := &message{
668		from: "from@example.com",
669		content: "From: from@example.com\r\n" +
670			"X-Empty:\r\n",
671	}
672
673	testMessage(t, m, 0, want)
674}
675
676func testMessage(t *testing.T, m *Message, bCount int, want *message) {
677	err := Send(stubSendMail(t, bCount, want), m)
678	if err != nil {
679		t.Error(err)
680	}
681}
682
683func stubSendMail(t *testing.T, bCount int, want *message) SendFunc {
684	return func(from string, to []string, m io.WriterTo) error {
685		if from != want.from {
686			t.Fatalf("Invalid from, got %q, want %q", from, want.from)
687		}
688
689		if len(to) != len(want.to) {
690			t.Fatalf("Invalid recipient count, \ngot %d: %q\nwant %d: %q",
691				len(to), to,
692				len(want.to), want.to,
693			)
694		}
695		for i := range want.to {
696			if to[i] != want.to[i] {
697				t.Fatalf("Invalid recipient, got %q, want %q",
698					to[i], want.to[i],
699				)
700			}
701		}
702
703		buf := new(bytes.Buffer)
704		_, err := m.WriteTo(buf)
705		if err != nil {
706			t.Error(err)
707		}
708		got := buf.String()
709		wantMsg := string("MIME-Version: 1.0\r\n" +
710			"Date: Wed, 25 Jun 2014 17:46:00 +0000\r\n" +
711			want.content)
712		if bCount > 0 {
713			boundaries := getBoundaries(t, bCount, got)
714			for i, b := range boundaries {
715				wantMsg = strings.Replace(wantMsg, "_BOUNDARY_"+strconv.Itoa(i+1)+"_", b, -1)
716			}
717		}
718
719		compareBodies(t, got, wantMsg)
720
721		return nil
722	}
723}
724
725func compareBodies(t *testing.T, got, want string) {
726	// We cannot do a simple comparison since the ordering of headers' fields
727	// is random.
728	gotLines := strings.Split(got, "\r\n")
729	wantLines := strings.Split(want, "\r\n")
730
731	// We only test for too many lines, missing lines are tested after
732	if len(gotLines) > len(wantLines) {
733		t.Fatalf("Message has too many lines, \ngot %d:\n%s\nwant %d:\n%s", len(gotLines), got, len(wantLines), want)
734	}
735
736	isInHeader := true
737	headerStart := 0
738	for i, line := range wantLines {
739		if line == gotLines[i] {
740			if line == "" {
741				isInHeader = false
742			} else if !isInHeader && len(line) > 2 && line[:2] == "--" {
743				isInHeader = true
744				headerStart = i + 1
745			}
746			continue
747		}
748
749		if !isInHeader {
750			missingLine(t, line, got, want)
751		}
752
753		isMissing := true
754		for j := headerStart; j < len(gotLines); j++ {
755			if gotLines[j] == "" {
756				break
757			}
758			if gotLines[j] == line {
759				isMissing = false
760				break
761			}
762		}
763		if isMissing {
764			missingLine(t, line, got, want)
765		}
766	}
767}
768
769func missingLine(t *testing.T, line, got, want string) {
770	t.Fatalf("Missing line %q\ngot:\n%s\nwant:\n%s", line, got, want)
771}
772
773func getBoundaries(t *testing.T, count int, m string) []string {
774	if matches := boundaryRegExp.FindAllStringSubmatch(m, count); matches != nil {
775		boundaries := make([]string, count)
776		for i, match := range matches {
777			boundaries[i] = match[1]
778		}
779		return boundaries
780	}
781
782	t.Fatal("Boundary not found in body")
783	return []string{""}
784}
785
786var boundaryRegExp = regexp.MustCompile("boundary=(\\w+)")
787
788func mockCopyFile(name string) (string, FileSetting) {
789	return name, SetCopyFunc(func(w io.Writer) error {
790		_, err := w.Write([]byte("Content of " + filepath.Base(name)))
791		return err
792	})
793}
794
795func mockCopyFileWithHeader(m *Message, name string, h map[string][]string) (string, FileSetting, FileSetting) {
796	name, f := mockCopyFile(name)
797	return name, f, SetHeader(h)
798}
799
800func BenchmarkFull(b *testing.B) {
801	discardFunc := SendFunc(func(from string, to []string, m io.WriterTo) error {
802		_, err := m.WriteTo(ioutil.Discard)
803		return err
804	})
805
806	m := NewMessage()
807	b.ResetTimer()
808	for n := 0; n < b.N; n++ {
809		m.SetAddressHeader("From", "from@example.com", "Señor From")
810		m.SetHeaders(map[string][]string{
811			"To":      {"to@example.com"},
812			"Cc":      {"cc@example.com"},
813			"Bcc":     {"bcc1@example.com", "bcc2@example.com"},
814			"Subject": {"¡Hola, señor!"},
815		})
816		m.SetBody("text/plain", "¡Hola, señor!")
817		m.AddAlternative("text/html", "<p>¡Hola, señor!</p>")
818		m.Attach(mockCopyFile("benchmark.txt"))
819		m.Embed(mockCopyFile("benchmark.jpg"))
820
821		if err := Send(discardFunc, m); err != nil {
822			panic(err)
823		}
824		m.Reset()
825	}
826}
827