Source file
src/net/mail/message_test.go
1
2
3
4
5 package mail
6
7 import (
8 "bytes"
9 "fmt"
10 "io"
11 "mime"
12 "reflect"
13 "slices"
14 "strings"
15 "testing"
16 "time"
17 )
18
19 var parseTests = []struct {
20 in string
21 header Header
22 body string
23 }{
24 {
25
26 in: `From: John Doe <jdoe@machine.example>
27 To: Mary Smith <mary@example.net>
28 Subject: Saying Hello
29 Date: Fri, 21 Nov 1997 09:55:06 -0600
30 Message-ID: <1234@local.machine.example>
31
32 This is a message just to say hello.
33 So, "Hello".
34 `,
35 header: Header{
36 "From": []string{"John Doe <jdoe@machine.example>"},
37 "To": []string{"Mary Smith <mary@example.net>"},
38 "Subject": []string{"Saying Hello"},
39 "Date": []string{"Fri, 21 Nov 1997 09:55:06 -0600"},
40 "Message-Id": []string{"<1234@local.machine.example>"},
41 },
42 body: "This is a message just to say hello.\nSo, \"Hello\".\n",
43 },
44 {
45
46 in: `Feedback-Type: abuse
47 User-Agent: SomeGenerator/1.0
48 Version: 1
49 `,
50 header: Header{
51 "Feedback-Type": []string{"abuse"},
52 "User-Agent": []string{"SomeGenerator/1.0"},
53 "Version": []string{"1"},
54 },
55 body: "",
56 },
57 {
58
59
60 in: `From: iant@golang.org
61 Custom/Header: v
62
63 Body
64 `,
65 header: Header{
66 "From": []string{"iant@golang.org"},
67 "Custom/Header": []string{"v"},
68 },
69 body: "Body\n",
70 },
71 {
72
73
74 in: `From iant@golang.org Mon Jun 19 00:00:00 2023
75 From: iant@golang.org
76
77 Hello, gophers!
78 `,
79 header: Header{
80 "From": []string{"iant@golang.org"},
81 "From iant@golang.org Mon Jun 19 00": []string{"00:00 2023"},
82 },
83 body: "Hello, gophers!\n",
84 },
85 }
86
87 func TestParsing(t *testing.T) {
88 for i, test := range parseTests {
89 msg, err := ReadMessage(bytes.NewBuffer([]byte(test.in)))
90 if err != nil {
91 t.Errorf("test #%d: Failed parsing message: %v", i, err)
92 continue
93 }
94 if !headerEq(msg.Header, test.header) {
95 t.Errorf("test #%d: Incorrectly parsed message header.\nGot:\n%+v\nWant:\n%+v",
96 i, msg.Header, test.header)
97 }
98 body, err := io.ReadAll(msg.Body)
99 if err != nil {
100 t.Errorf("test #%d: Failed reading body: %v", i, err)
101 continue
102 }
103 bodyStr := string(body)
104 if bodyStr != test.body {
105 t.Errorf("test #%d: Incorrectly parsed message body.\nGot:\n%+v\nWant:\n%+v",
106 i, bodyStr, test.body)
107 }
108 }
109 }
110
111 func headerEq(a, b Header) bool {
112 if len(a) != len(b) {
113 return false
114 }
115 for k, as := range a {
116 bs, ok := b[k]
117 if !ok {
118 return false
119 }
120 if !slices.Equal(as, bs) {
121 return false
122 }
123 }
124 return true
125 }
126
127 func TestDateParsing(t *testing.T) {
128 tests := []struct {
129 dateStr string
130 exp time.Time
131 }{
132
133 {
134 "Fri, 21 Nov 1997 09:55:06 -0600",
135 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
136 },
137
138
139 {
140 "21 Nov 97 09:55:06 GMT",
141 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("GMT", 0)),
142 },
143
144 {
145 "Fri, 21 Nov 1997 09:55:06 -0600 (MDT)",
146 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
147 },
148 {
149 "Thu, 20 Nov 1997 09:55:06 -0600 (MDT)",
150 time.Date(1997, 11, 20, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
151 },
152 {
153 "Thu, 20 Nov 1997 09:55:06 GMT (GMT)",
154 time.Date(1997, 11, 20, 9, 55, 6, 0, time.UTC),
155 },
156 {
157 "Fri, 21 Nov 1997 09:55:06 +1300 (TOT)",
158 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", +13*60*60)),
159 },
160 }
161 for _, test := range tests {
162 hdr := Header{
163 "Date": []string{test.dateStr},
164 }
165 date, err := hdr.Date()
166 if err != nil {
167 t.Errorf("Header(Date: %s).Date(): %v", test.dateStr, err)
168 } else if !date.Equal(test.exp) {
169 t.Errorf("Header(Date: %s).Date() = %+v, want %+v", test.dateStr, date, test.exp)
170 }
171
172 date, err = ParseDate(test.dateStr)
173 if err != nil {
174 t.Errorf("ParseDate(%s): %v", test.dateStr, err)
175 } else if !date.Equal(test.exp) {
176 t.Errorf("ParseDate(%s) = %+v, want %+v", test.dateStr, date, test.exp)
177 }
178 }
179 }
180
181 func TestDateParsingCFWS(t *testing.T) {
182 tests := []struct {
183 dateStr string
184 exp time.Time
185 valid bool
186 }{
187
188 {
189 " ",
190
191 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
192 false,
193 },
194
195 {
196 " Fri, 21 Nov 1997 09:55:06 -0600",
197 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
198 true,
199 },
200 {
201 "21 Nov 1997 09:55:06 -0600",
202 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
203 true,
204 },
205 {
206 "Fri 21 Nov 1997 09:55:06 -0600",
207 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
208 false,
209 },
210
211 {
212 "Fri, 21 Nov 1997 09:55:06 -0600",
213 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
214 true,
215 },
216
217 {
218 "Fri, 21 Nov 1997 09:55:06 -0600",
219 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
220 true,
221 },
222
223 {
224 "Fri, 21 Nov 1997 09:55:06 CST",
225 time.Time{},
226 true,
227 },
228
229 {
230 "Fri, 21 Nov 1997 09:55:06 CST (no leading FWS and a trailing CRLF) \r\n",
231 time.Time{},
232 true,
233 },
234
235 {
236 "Fri, 21 Nov 1997 09:55:06 -0600 (MDT and non-US-ASCII signs éèç )",
237 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
238 true,
239 },
240
241
242 {
243 "Fri, 21 Nov 1997 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ",
244 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
245 true,
246 },
247
248 {
249 "Fri, 21 Nov 1997 \r 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ",
250 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
251 false,
252 },
253
254 {
255 "Fri, 21 Nov 199\r\n7 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ",
256 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
257 true,
258 },
259
260 {
261 "Fri, 21 Nov 1997 ù 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ",
262 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
263 false,
264 },
265
266 {
267 "Fri, 21 Nov () 1997 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ",
268 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
269 false,
270 },
271
272 {
273 "Fri, 21 Nov 1997 09:55:06 -060 \r\n (Thisisa(valid)cfws) \t ",
274 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
275 false,
276 },
277
278 {
279 "Fri, 21 1997 09:55:06 -0600",
280 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
281 false,
282 },
283
284 {
285 "Fri, 21 OCT 1997 09:55:06 CST",
286 time.Time{},
287 false,
288 },
289
290 {
291 "Fri, 21 Nov 1997 09:55:06 -060",
292 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
293 false,
294 },
295
296 {
297 "Fri, 21 1997 09:55:06 GT",
298 time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
299 false,
300 },
301
302
303 {
304 "Tue, 26 May 2020 14:04:40 GMT",
305 time.Date(2020, 05, 26, 14, 04, 40, 0, time.UTC),
306 true,
307 },
308 {
309 "Tue, 26 May 2020 14:04:40 UT",
310 time.Date(2020, 05, 26, 14, 04, 40, 0, time.UTC),
311 true,
312 },
313 {
314 "Thu, 21 May 2020 14:04:40 UT",
315 time.Date(2020, 05, 21, 14, 04, 40, 0, time.UTC),
316 true,
317 },
318 {
319 "Tue, 26 May 2020 14:04:40 XT",
320 time.Date(2020, 05, 26, 14, 04, 40, 0, time.UTC),
321 false,
322 },
323 {
324 "Thu, 21 May 2020 14:04:40 XT",
325 time.Date(2020, 05, 21, 14, 04, 40, 0, time.UTC),
326 false,
327 },
328 {
329 "Thu, 21 May 2020 14:04:40 UTC",
330 time.Date(2020, 05, 21, 14, 04, 40, 0, time.UTC),
331 true,
332 },
333 {
334 "Fri, 21 Nov 1997 09:55:06 GMT (GMT)",
335 time.Date(1997, 11, 21, 9, 55, 6, 0, time.UTC),
336 true,
337 },
338 }
339 for _, test := range tests {
340 hdr := Header{
341 "Date": []string{test.dateStr},
342 }
343 date, err := hdr.Date()
344 if err != nil && test.valid {
345 t.Errorf("Header(Date: %s).Date(): %v", test.dateStr, err)
346 } else if err == nil && test.exp.IsZero() {
347
348
349 } else if err == nil && !date.Equal(test.exp) && test.valid {
350 t.Errorf("Header(Date: %s).Date() = %+v, want %+v", test.dateStr, date, test.exp)
351 } else if err == nil && !test.valid {
352 t.Errorf("Header(Date: %s).Date() did not return an error but %v", test.dateStr, date)
353 }
354
355 date, err = ParseDate(test.dateStr)
356 if err != nil && test.valid {
357 t.Errorf("ParseDate(%s): %v", test.dateStr, err)
358 } else if err == nil && test.exp.IsZero() {
359
360
361 } else if err == nil && !test.valid {
362 t.Errorf("ParseDate(%s) did not return an error but %v", test.dateStr, date)
363 } else if err == nil && test.valid && !date.Equal(test.exp) {
364 t.Errorf("ParseDate(%s) = %+v, want %+v", test.dateStr, date, test.exp)
365 }
366 }
367 }
368
369 func TestAddressParsingError(t *testing.T) {
370 mustErrTestCases := [...]struct {
371 text string
372 wantErrText string
373 }{
374 0: {"=?iso-8859-2?Q?Bogl=E1rka_Tak=E1cs?= <unknown@gmail.com>", "charset not supported"},
375 1: {"a@gmail.com b@gmail.com", "expected single address"},
376 2: {string([]byte{0xed, 0xa0, 0x80}) + " <micro@example.net>", "invalid utf-8 in address"},
377 3: {"\"" + string([]byte{0xed, 0xa0, 0x80}) + "\" <half-surrogate@example.com>", "invalid utf-8 in quoted-string"},
378 4: {"\"\\" + string([]byte{0x80}) + "\" <escaped-invalid-unicode@example.net>", "invalid utf-8 in quoted-string"},
379 5: {"\"\x00\" <null@example.net>", "bad character in quoted-string"},
380 6: {"\"\\\x00\" <escaped-null@example.net>", "bad character in quoted-string"},
381 7: {"John Doe", "no angle-addr"},
382 8: {`<jdoe#machine.example>`, "missing @ in addr-spec"},
383 9: {`John <middle> Doe <jdoe@machine.example>`, "missing @ in addr-spec"},
384 10: {"cfws@example.com (", "misformatted parenthetical comment"},
385 11: {"empty group: ;", "empty group"},
386 12: {"root group: embed group: null@example.com;", "no angle-addr"},
387 13: {"group not closed: null@example.com", "expected comma"},
388 14: {"group: first@example.com, second@example.com;", "group with multiple addresses"},
389 15: {"john.doe", "missing '@' or angle-addr"},
390 16: {"john.doe@", "missing '@' or angle-addr"},
391 17: {"John Doe@foo.bar", "no angle-addr"},
392 18: {" group: null@example.com; (asd", "misformatted parenthetical comment"},
393 19: {" group: ; (asd", "misformatted parenthetical comment"},
394 20: {`(John) Doe <jdoe@machine.example>`, "missing word in phrase:"},
395 21: {"<jdoe@[" + string([]byte{0xed, 0xa0, 0x80}) + "192.168.0.1]>", "invalid utf-8 in domain-literal"},
396 22: {"<jdoe@[[192.168.0.1]>", "bad character in domain-literal"},
397 23: {"<jdoe@[192.168.0.1>", "unclosed domain-literal"},
398 24: {"<jdoe@[256.0.0.1]>", "invalid IP address in domain-literal"},
399 }
400
401 for i, tc := range mustErrTestCases {
402 _, err := ParseAddress(tc.text)
403 if err == nil || !strings.Contains(err.Error(), tc.wantErrText) {
404 t.Errorf(`mail.ParseAddress(%q) #%d want %q, got %v`, tc.text, i, tc.wantErrText, err)
405 }
406 }
407
408 t.Run("CustomWordDecoder", func(t *testing.T) {
409 p := &AddressParser{WordDecoder: &mime.WordDecoder{}}
410 for i, tc := range mustErrTestCases {
411 _, err := p.Parse(tc.text)
412 if err == nil || !strings.Contains(err.Error(), tc.wantErrText) {
413 t.Errorf(`p.Parse(%q) #%d want %q, got %v`, tc.text, i, tc.wantErrText, err)
414 }
415 }
416 })
417
418 }
419
420 func TestAddressParsing(t *testing.T) {
421 tests := []struct {
422 addrsStr string
423 exp []*Address
424 }{
425
426 {
427 `jdoe@machine.example`,
428 []*Address{{
429 Address: "jdoe@machine.example",
430 }},
431 },
432
433 {
434 `John Doe <jdoe@machine.example>`,
435 []*Address{{
436 Name: "John Doe",
437 Address: "jdoe@machine.example",
438 }},
439 },
440
441 {
442 `"Joe Q. Public" <john.q.public@example.com>`,
443 []*Address{{
444 Name: "Joe Q. Public",
445 Address: "john.q.public@example.com",
446 }},
447 },
448
449 {
450 `John (middle) Doe <jdoe@machine.example>`,
451 []*Address{{
452 Name: "John Doe",
453 Address: "jdoe@machine.example",
454 }},
455 },
456
457 {
458 `"John (middle) Doe" <jdoe@machine.example>`,
459 []*Address{{
460 Name: "John (middle) Doe",
461 Address: "jdoe@machine.example",
462 }},
463 },
464 {
465 `"John <middle> Doe" <jdoe@machine.example>`,
466 []*Address{{
467 Name: "John <middle> Doe",
468 Address: "jdoe@machine.example",
469 }},
470 },
471 {
472 `Mary Smith <mary@x.test>, jdoe@example.org, Who? <one@y.test>`,
473 []*Address{
474 {
475 Name: "Mary Smith",
476 Address: "mary@x.test",
477 },
478 {
479 Address: "jdoe@example.org",
480 },
481 {
482 Name: "Who?",
483 Address: "one@y.test",
484 },
485 },
486 },
487 {
488 `<boss@nil.test>, "Giant; \"Big\" Box" <sysservices@example.net>`,
489 []*Address{
490 {
491 Address: "boss@nil.test",
492 },
493 {
494 Name: `Giant; "Big" Box`,
495 Address: "sysservices@example.net",
496 },
497 },
498 },
499
500 {
501 `Joe Q. Public <john.q.public@example.com>`,
502 []*Address{{
503 Name: "Joe Q. Public",
504 Address: "john.q.public@example.com",
505 }},
506 },
507
508 {
509 `group1: groupaddr1@example.com;`,
510 []*Address{
511 {
512 Name: "",
513 Address: "groupaddr1@example.com",
514 },
515 },
516 },
517 {
518 `empty group: ;`,
519 []*Address(nil),
520 },
521 {
522 `A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;`,
523 []*Address{
524 {
525 Name: "Ed Jones",
526 Address: "c@a.test",
527 },
528 {
529 Name: "",
530 Address: "joe@where.test",
531 },
532 {
533 Name: "John",
534 Address: "jdoe@one.test",
535 },
536 },
537 },
538
539 {
540 ` , joe@where.test,,John <jdoe@one.test>,`,
541 []*Address{
542 {
543 Name: "",
544 Address: "joe@where.test",
545 },
546 {
547 Name: "John",
548 Address: "jdoe@one.test",
549 },
550 },
551 },
552 {
553 ` , joe@where.test,,John <jdoe@one.test>,,`,
554 []*Address{
555 {
556 Name: "",
557 Address: "joe@where.test",
558 },
559 {
560 Name: "John",
561 Address: "jdoe@one.test",
562 },
563 },
564 },
565 {
566 `Group1: <addr1@example.com>;, Group 2: addr2@example.com;, John <addr3@example.com>`,
567 []*Address{
568 {
569 Name: "",
570 Address: "addr1@example.com",
571 },
572 {
573 Name: "",
574 Address: "addr2@example.com",
575 },
576 {
577 Name: "John",
578 Address: "addr3@example.com",
579 },
580 },
581 },
582
583 {
584 `=?iso-8859-1?q?J=F6rg_Doe?= <joerg@example.com>`,
585 []*Address{
586 {
587 Name: `Jörg Doe`,
588 Address: "joerg@example.com",
589 },
590 },
591 },
592
593 {
594 `=?us-ascii?q?J=6Frg_Doe?= <joerg@example.com>`,
595 []*Address{
596 {
597 Name: `Jorg Doe`,
598 Address: "joerg@example.com",
599 },
600 },
601 },
602
603 {
604 `=?utf-8?q?J=C3=B6rg_Doe?= <joerg@example.com>`,
605 []*Address{
606 {
607 Name: `Jörg Doe`,
608 Address: "joerg@example.com",
609 },
610 },
611 },
612
613 {
614 `=?utf-8?q?J=C3=B6rg?= =?utf-8?q?Doe?= <joerg@example.com>`,
615 []*Address{
616 {
617 Name: `JörgDoe`,
618 Address: "joerg@example.com",
619 },
620 },
621 },
622
623 {
624 `=?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>`,
625 []*Address{
626 {
627 Name: `André Pirard`,
628 Address: "PIRARD@vm1.ulg.ac.be",
629 },
630 },
631 },
632
633 {
634 `=?ISO-8859-1?B?SvZyZw==?= <joerg@example.com>`,
635 []*Address{
636 {
637 Name: `Jörg`,
638 Address: "joerg@example.com",
639 },
640 },
641 },
642
643 {
644 `=?UTF-8?B?SsO2cmc=?= <joerg@example.com>`,
645 []*Address{
646 {
647 Name: `Jörg`,
648 Address: "joerg@example.com",
649 },
650 },
651 },
652
653 {
654 `Asem H. <noreply@example.com>`,
655 []*Address{
656 {
657 Name: `Asem H.`,
658 Address: "noreply@example.com",
659 },
660 },
661 },
662
663 {
664 `"Gø Pher" <gopher@example.com>`,
665 []*Address{
666 {
667 Name: `Gø Pher`,
668 Address: "gopher@example.com",
669 },
670 },
671 },
672
673 {
674 `µ <micro@example.com>`,
675 []*Address{
676 {
677 Name: `µ`,
678 Address: "micro@example.com",
679 },
680 },
681 },
682
683 {
684 `Micro <µ@example.com>`,
685 []*Address{
686 {
687 Name: `Micro`,
688 Address: "µ@example.com",
689 },
690 },
691 },
692
693 {
694 `Micro <micro@µ.example.com>`,
695 []*Address{
696 {
697 Name: `Micro`,
698 Address: "micro@µ.example.com",
699 },
700 },
701 },
702
703 {
704 `"" <emptystring@example.com>`,
705 []*Address{
706 {
707 Name: "",
708 Address: "emptystring@example.com",
709 },
710 },
711 },
712
713 {
714 `<cfws@example.com> (CFWS (cfws)) (another comment)`,
715 []*Address{
716 {
717 Name: "",
718 Address: "cfws@example.com",
719 },
720 },
721 },
722 {
723 `<cfws@example.com> () (another comment), <cfws2@example.com> (another)`,
724 []*Address{
725 {
726 Name: "",
727 Address: "cfws@example.com",
728 },
729 {
730 Name: "",
731 Address: "cfws2@example.com",
732 },
733 },
734 },
735
736 {
737 `john@example.com (John Doe)`,
738 []*Address{
739 {
740 Name: "John Doe",
741 Address: "john@example.com",
742 },
743 },
744 },
745
746 {
747 `John Doe <john@example.com> (Joey)`,
748 []*Address{
749 {
750 Name: "John Doe",
751 Address: "john@example.com",
752 },
753 },
754 },
755
756 {
757 `john@example.com(John Doe)`,
758 []*Address{
759 {
760 Name: "John Doe",
761 Address: "john@example.com",
762 },
763 },
764 },
765
766 {
767 `asjo@example.com (Adam =?utf-8?Q?Sj=C3=B8gren?=)`,
768 []*Address{
769 {
770 Name: "Adam Sjøgren",
771 Address: "asjo@example.com",
772 },
773 },
774 },
775
776 {
777 `asjo@example.com (Adam =?utf-8?Q?Sj=C3=B8gren?=)`,
778 []*Address{
779 {
780 Name: "Adam Sjøgren",
781 Address: "asjo@example.com",
782 },
783 },
784 },
785
786 {
787 `asjo@example.com (Adam =?utf-8?Q?Sj=C3=B8gren?= (Debian))`,
788 []*Address{
789 {
790 Name: "Adam Sjøgren (Debian)",
791 Address: "asjo@example.com",
792 },
793 },
794 },
795
796 {
797 `group (comment:): a@example.com, b@example.com;`,
798 []*Address{
799 {
800 Address: "a@example.com",
801 },
802 {
803 Address: "b@example.com",
804 },
805 },
806 },
807 {
808 `x(:"):"@a.example;("@b.example;`,
809 []*Address{
810 {
811 Address: `@a.example;(@b.example`,
812 },
813 },
814 },
815
816 {
817 `jdoe@[192.168.0.1]`,
818 []*Address{{
819 Address: "jdoe@[192.168.0.1]",
820 }},
821 },
822 {
823 `John Doe <jdoe@[192.168.0.1]>`,
824 []*Address{{
825 Name: "John Doe",
826 Address: "jdoe@[192.168.0.1]",
827 }},
828 },
829 }
830 for _, test := range tests {
831 if len(test.exp) == 1 {
832 addr, err := ParseAddress(test.addrsStr)
833 if err != nil {
834 t.Errorf("Failed parsing (single) %q: %v", test.addrsStr, err)
835 continue
836 }
837 if !reflect.DeepEqual([]*Address{addr}, test.exp) {
838 t.Errorf("Parse (single) of %q: got %+v, want %+v", test.addrsStr, addr, test.exp)
839 }
840 }
841
842 addrs, err := ParseAddressList(test.addrsStr)
843 if err != nil {
844 t.Errorf("Failed parsing (list) %q: %v", test.addrsStr, err)
845 continue
846 }
847 if !reflect.DeepEqual(addrs, test.exp) {
848 t.Errorf("Parse (list) of %q: got %+v, want %+v", test.addrsStr, addrs, test.exp)
849 }
850 }
851 }
852
853 func TestAddressParser(t *testing.T) {
854 tests := []struct {
855 addrsStr string
856 exp []*Address
857 }{
858
859 {
860 `jdoe@machine.example`,
861 []*Address{{
862 Address: "jdoe@machine.example",
863 }},
864 },
865
866 {
867 `John Doe <jdoe@machine.example>`,
868 []*Address{{
869 Name: "John Doe",
870 Address: "jdoe@machine.example",
871 }},
872 },
873
874 {
875 `"Joe Q. Public" <john.q.public@example.com>`,
876 []*Address{{
877 Name: "Joe Q. Public",
878 Address: "john.q.public@example.com",
879 }},
880 },
881 {
882 `Mary Smith <mary@x.test>, jdoe@example.org, Who? <one@y.test>`,
883 []*Address{
884 {
885 Name: "Mary Smith",
886 Address: "mary@x.test",
887 },
888 {
889 Address: "jdoe@example.org",
890 },
891 {
892 Name: "Who?",
893 Address: "one@y.test",
894 },
895 },
896 },
897 {
898 `<boss@nil.test>, "Giant; \"Big\" Box" <sysservices@example.net>`,
899 []*Address{
900 {
901 Address: "boss@nil.test",
902 },
903 {
904 Name: `Giant; "Big" Box`,
905 Address: "sysservices@example.net",
906 },
907 },
908 },
909
910 {
911 `=?iso-8859-1?q?J=F6rg_Doe?= <joerg@example.com>`,
912 []*Address{
913 {
914 Name: `Jörg Doe`,
915 Address: "joerg@example.com",
916 },
917 },
918 },
919
920 {
921 `=?us-ascii?q?J=6Frg_Doe?= <joerg@example.com>`,
922 []*Address{
923 {
924 Name: `Jorg Doe`,
925 Address: "joerg@example.com",
926 },
927 },
928 },
929
930 {
931 `=?ISO-8859-15?Q?J=F6rg_Doe?= <joerg@example.com>`,
932 []*Address{
933 {
934 Name: `Jörg Doe`,
935 Address: "joerg@example.com",
936 },
937 },
938 },
939
940 {
941 `=?windows-1252?q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>`,
942 []*Address{
943 {
944 Name: `André Pirard`,
945 Address: "PIRARD@vm1.ulg.ac.be",
946 },
947 },
948 },
949
950 {
951 `=?ISO-8859-15?B?SvZyZw==?= <joerg@example.com>`,
952 []*Address{
953 {
954 Name: `Jörg`,
955 Address: "joerg@example.com",
956 },
957 },
958 },
959
960 {
961 `=?UTF-8?B?SsO2cmc=?= <joerg@example.com>`,
962 []*Address{
963 {
964 Name: `Jörg`,
965 Address: "joerg@example.com",
966 },
967 },
968 },
969
970 {
971 `Asem H. <noreply@example.com>`,
972 []*Address{
973 {
974 Name: `Asem H.`,
975 Address: "noreply@example.com",
976 },
977 },
978 },
979
980 {
981 `jdoe@[192.168.0.1]`,
982 []*Address{{
983 Address: "jdoe@[192.168.0.1]",
984 }},
985 },
986 {
987 `John Doe <jdoe@[192.168.0.1]>`,
988 []*Address{{
989 Name: "John Doe",
990 Address: "jdoe@[192.168.0.1]",
991 }},
992 },
993 }
994
995 ap := AddressParser{WordDecoder: &mime.WordDecoder{
996 CharsetReader: func(charset string, input io.Reader) (io.Reader, error) {
997 in, err := io.ReadAll(input)
998 if err != nil {
999 return nil, err
1000 }
1001
1002 switch charset {
1003 case "iso-8859-15":
1004 in = bytes.ReplaceAll(in, []byte("\xf6"), []byte("ö"))
1005 case "windows-1252":
1006 in = bytes.ReplaceAll(in, []byte("\xe9"), []byte("é"))
1007 }
1008
1009 return bytes.NewReader(in), nil
1010 },
1011 }}
1012
1013 for _, test := range tests {
1014 if len(test.exp) == 1 {
1015 addr, err := ap.Parse(test.addrsStr)
1016 if err != nil {
1017 t.Errorf("Failed parsing (single) %q: %v", test.addrsStr, err)
1018 continue
1019 }
1020 if !reflect.DeepEqual([]*Address{addr}, test.exp) {
1021 t.Errorf("Parse (single) of %q: got %+v, want %+v", test.addrsStr, addr, test.exp)
1022 }
1023 }
1024
1025 addrs, err := ap.ParseList(test.addrsStr)
1026 if err != nil {
1027 t.Errorf("Failed parsing (list) %q: %v", test.addrsStr, err)
1028 continue
1029 }
1030 if !reflect.DeepEqual(addrs, test.exp) {
1031 t.Errorf("Parse (list) of %q: got %+v, want %+v", test.addrsStr, addrs, test.exp)
1032 }
1033 }
1034 }
1035
1036 func TestAddressString(t *testing.T) {
1037 tests := []struct {
1038 addr *Address
1039 exp string
1040 }{
1041 {
1042 &Address{Address: "bob@example.com"},
1043 "<bob@example.com>",
1044 },
1045 {
1046 &Address{Address: `my@idiot@address@example.com`},
1047 `<"my@idiot@address"@example.com>`,
1048 },
1049 {
1050 &Address{Address: ` @example.com`},
1051 `<" "@example.com>`,
1052 },
1053 {
1054 &Address{Name: "Bob", Address: "bob@example.com"},
1055 `"Bob" <bob@example.com>`,
1056 },
1057 {
1058
1059 &Address{Name: "Böb", Address: "bob@example.com"},
1060 `=?utf-8?q?B=C3=B6b?= <bob@example.com>`,
1061 },
1062 {
1063 &Address{Name: "Bob Jane", Address: "bob@example.com"},
1064 `"Bob Jane" <bob@example.com>`,
1065 },
1066 {
1067 &Address{Name: "Böb Jacöb", Address: "bob@example.com"},
1068 `=?utf-8?q?B=C3=B6b_Jac=C3=B6b?= <bob@example.com>`,
1069 },
1070 {
1071 &Address{Name: "Rob", Address: ""},
1072 `"Rob" <@>`,
1073 },
1074 {
1075 &Address{Name: "Rob", Address: "@"},
1076 `"Rob" <@>`,
1077 },
1078 {
1079 &Address{Name: "Böb, Jacöb", Address: "bob@example.com"},
1080 `=?utf-8?b?QsO2YiwgSmFjw7Zi?= <bob@example.com>`,
1081 },
1082 {
1083 &Address{Name: "=??Q?x?=", Address: "hello@world.com"},
1084 `"=??Q?x?=" <hello@world.com>`,
1085 },
1086 {
1087 &Address{Name: "=?hello", Address: "hello@world.com"},
1088 `"=?hello" <hello@world.com>`,
1089 },
1090 {
1091 &Address{Name: "world?=", Address: "hello@world.com"},
1092 `"world?=" <hello@world.com>`,
1093 },
1094 {
1095
1096 &Address{Name: string([]byte{0xed, 0xa0, 0x80}), Address: "invalid-utf8@example.net"},
1097 "=?utf-8?q?=ED=A0=80?= <invalid-utf8@example.net>",
1098 },
1099
1100 {
1101 &Address{Address: "bob@[192.168.0.1]"},
1102 "<bob@[192.168.0.1]>",
1103 },
1104 {
1105 &Address{Name: "Bob", Address: "bob@[192.168.0.1]"},
1106 `"Bob" <bob@[192.168.0.1]>`,
1107 },
1108 }
1109 for _, test := range tests {
1110 s := test.addr.String()
1111 if s != test.exp {
1112 t.Errorf("Address%+v.String() = %v, want %v", *test.addr, s, test.exp)
1113 continue
1114 }
1115
1116
1117 if test.addr.Address != "" && test.addr.Address != "@" {
1118 a, err := ParseAddress(test.exp)
1119 if err != nil {
1120 t.Errorf("ParseAddress(%#q): %v", test.exp, err)
1121 continue
1122 }
1123 if a.Name != test.addr.Name || a.Address != test.addr.Address {
1124 t.Errorf("ParseAddress(%#q) = %#v, want %#v", test.exp, a, test.addr)
1125 }
1126 }
1127 }
1128 }
1129
1130
1131 func TestAddressParsingAndFormatting(t *testing.T) {
1132
1133
1134 tests := []string{
1135 `<Bob@example.com>`,
1136 `<bob.bob@example.com>`,
1137 `<".bob"@example.com>`,
1138 `<" "@example.com>`,
1139 `<some.mail-with-dash@example.com>`,
1140 `<"dot.and space"@example.com>`,
1141 `<"very.unusual.@.unusual.com"@example.com>`,
1142 `<admin@mailserver1>`,
1143 `<postmaster@localhost>`,
1144 "<#!$%&'*+-/=?^_`{}|~@example.org>",
1145 `<"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com>`,
1146 `<"()<>[]:,;@\\\"!#$%&'*+-/=?^_{}| ~.a"@example.org>`,
1147 `<"Abc\\@def"@example.com>`,
1148 `<"Joe\\Blow"@example.com>`,
1149 `<test1/test2=test3@example.com>`,
1150 `<def!xyz%abc@example.com>`,
1151 `<_somename@example.com>`,
1152 `<joe@uk>`,
1153 `<~@example.com>`,
1154 `<"..."@test.com>`,
1155 `<"john..doe"@example.com>`,
1156 `<"john.doe."@example.com>`,
1157 `<".john.doe"@example.com>`,
1158 `<"."@example.com>`,
1159 `<".."@example.com>`,
1160 `<"0:"@0>`,
1161 `<Bob@[192.168.0.1]>`,
1162 }
1163
1164 for _, test := range tests {
1165 addr, err := ParseAddress(test)
1166 if err != nil {
1167 t.Errorf("Couldn't parse address %s: %s", test, err.Error())
1168 continue
1169 }
1170 str := addr.String()
1171 addr, err = ParseAddress(str)
1172 if err != nil {
1173 t.Errorf("ParseAddr(%q) error: %v", test, err)
1174 continue
1175 }
1176
1177 if addr.String() != test {
1178 t.Errorf("String() round-trip = %q; want %q", addr, test)
1179 continue
1180 }
1181
1182 }
1183
1184
1185 badTests := []string{
1186 `<Abc.example.com>`,
1187 `<A@b@c@example.com>`,
1188 `<a"b(c)d,e:f;g<h>i[j\k]l@example.com>`,
1189 `<just"not"right@example.com>`,
1190 `<this is"not\allowed@example.com>`,
1191 `<this\ still\"not\\allowed@example.com>`,
1192 `<john..doe@example.com>`,
1193 `<john.doe@example..com>`,
1194 `<john.doe@example..com>`,
1195 `<john.doe.@example.com>`,
1196 `<john.doe.@.example.com>`,
1197 `<.john.doe@example.com>`,
1198 `<@example.com>`,
1199 `<.@example.com>`,
1200 `<test@.>`,
1201 `< @example.com>`,
1202 `<""test""blah""@example.com>`,
1203 `<""@0>`,
1204 }
1205
1206 for _, test := range badTests {
1207 _, err := ParseAddress(test)
1208 if err == nil {
1209 t.Errorf("Should have failed to parse address: %s", test)
1210 continue
1211 }
1212
1213 }
1214
1215 }
1216
1217 func TestAddressFormattingAndParsing(t *testing.T) {
1218 tests := []*Address{
1219 {Name: "@lïce", Address: "alice@example.com"},
1220 {Name: "Böb O'Connor", Address: "bob@example.com"},
1221 {Name: "???", Address: "bob@example.com"},
1222 {Name: "Böb ???", Address: "bob@example.com"},
1223 {Name: "Böb (Jacöb)", Address: "bob@example.com"},
1224 {Name: "à#$%&'(),.:;<>@[]^`{|}~'", Address: "bob@example.com"},
1225
1226 {Name: "\"\\\x1f,\"", Address: "0@0"},
1227
1228 {Name: "naé, mée", Address: "test.mail@gmail.com"},
1229 }
1230
1231 for i, test := range tests {
1232 parsed, err := ParseAddress(test.String())
1233 if err != nil {
1234 t.Errorf("test #%d: ParseAddr(%q) error: %v", i, test.String(), err)
1235 continue
1236 }
1237 if parsed.Name != test.Name {
1238 t.Errorf("test #%d: Parsed name = %q; want %q", i, parsed.Name, test.Name)
1239 }
1240 if parsed.Address != test.Address {
1241 t.Errorf("test #%d: Parsed address = %q; want %q", i, parsed.Address, test.Address)
1242 }
1243 }
1244 }
1245
1246 func TestEmptyAddress(t *testing.T) {
1247 parsed, err := ParseAddress("")
1248 if parsed != nil || err == nil {
1249 t.Errorf(`ParseAddress("") = %v, %v, want nil, error`, parsed, err)
1250 }
1251 list, err := ParseAddressList("")
1252 if len(list) > 0 || err == nil {
1253 t.Errorf(`ParseAddressList("") = %v, %v, want nil, error`, list, err)
1254 }
1255 list, err = ParseAddressList(",")
1256 if len(list) > 0 || err == nil {
1257 t.Errorf(`ParseAddressList("") = %v, %v, want nil, error`, list, err)
1258 }
1259 list, err = ParseAddressList("a@b c@d")
1260 if len(list) > 0 || err == nil {
1261 t.Errorf(`ParseAddressList("") = %v, %v, want nil, error`, list, err)
1262 }
1263 }
1264
1265 func BenchmarkConsumePhrase(b *testing.B) {
1266 for _, n := range []int{10, 100, 1000, 10000} {
1267 b.Run(fmt.Sprintf("words-%d", n), func(b *testing.B) {
1268 input := strings.Repeat("=?utf-8?q?hello?= ", n) + "<user@example.com>"
1269 for b.Loop() {
1270 (&addrParser{s: input}).consumePhrase()
1271 }
1272 })
1273 }
1274 }
1275
1276 func BenchmarkConsumeComment(b *testing.B) {
1277 for _, n := range []int{10, 100, 1000, 10000} {
1278 b.Run(fmt.Sprintf("depth-%d", n), func(b *testing.B) {
1279
1280 open := strings.Repeat("(", n)
1281 close := strings.Repeat(")", n)
1282
1283
1284
1285 input := open[:n-1] + "a" + close
1286 for b.Loop() {
1287 p := addrParser{s: input}
1288 p.consumeComment()
1289 }
1290 })
1291 }
1292 }
1293
View as plain text