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 25: {"<jdoe@[fd42::de:ad:be:ef]>", "invalid IP address in domain-literal"},
400 }
401
402 for i, tc := range mustErrTestCases {
403 _, err := ParseAddress(tc.text)
404 if err == nil || !strings.Contains(err.Error(), tc.wantErrText) {
405 t.Errorf(`mail.ParseAddress(%q) #%d want %q, got %v`, tc.text, i, tc.wantErrText, err)
406 }
407 }
408
409 t.Run("CustomWordDecoder", func(t *testing.T) {
410 p := &AddressParser{WordDecoder: &mime.WordDecoder{}}
411 for i, tc := range mustErrTestCases {
412 _, err := p.Parse(tc.text)
413 if err == nil || !strings.Contains(err.Error(), tc.wantErrText) {
414 t.Errorf(`p.Parse(%q) #%d want %q, got %v`, tc.text, i, tc.wantErrText, err)
415 }
416 }
417 })
418
419 }
420
421 func TestAddressParsing(t *testing.T) {
422 tests := []struct {
423 addrsStr string
424 exp []*Address
425 }{
426
427 {
428 `jdoe@machine.example`,
429 []*Address{{
430 Address: "jdoe@machine.example",
431 }},
432 },
433
434 {
435 `John Doe <jdoe@machine.example>`,
436 []*Address{{
437 Name: "John Doe",
438 Address: "jdoe@machine.example",
439 }},
440 },
441
442 {
443 `"Joe Q. Public" <john.q.public@example.com>`,
444 []*Address{{
445 Name: "Joe Q. Public",
446 Address: "john.q.public@example.com",
447 }},
448 },
449
450 {
451 `John (middle) Doe <jdoe@machine.example>`,
452 []*Address{{
453 Name: "John Doe",
454 Address: "jdoe@machine.example",
455 }},
456 },
457
458 {
459 `"John (middle) Doe" <jdoe@machine.example>`,
460 []*Address{{
461 Name: "John (middle) Doe",
462 Address: "jdoe@machine.example",
463 }},
464 },
465 {
466 `"John <middle> Doe" <jdoe@machine.example>`,
467 []*Address{{
468 Name: "John <middle> Doe",
469 Address: "jdoe@machine.example",
470 }},
471 },
472 {
473 `Mary Smith <mary@x.test>, jdoe@example.org, Who? <one@y.test>`,
474 []*Address{
475 {
476 Name: "Mary Smith",
477 Address: "mary@x.test",
478 },
479 {
480 Address: "jdoe@example.org",
481 },
482 {
483 Name: "Who?",
484 Address: "one@y.test",
485 },
486 },
487 },
488 {
489 `<boss@nil.test>, "Giant; \"Big\" Box" <sysservices@example.net>`,
490 []*Address{
491 {
492 Address: "boss@nil.test",
493 },
494 {
495 Name: `Giant; "Big" Box`,
496 Address: "sysservices@example.net",
497 },
498 },
499 },
500
501 {
502 `Joe Q. Public <john.q.public@example.com>`,
503 []*Address{{
504 Name: "Joe Q. Public",
505 Address: "john.q.public@example.com",
506 }},
507 },
508
509 {
510 `group1: groupaddr1@example.com;`,
511 []*Address{
512 {
513 Name: "",
514 Address: "groupaddr1@example.com",
515 },
516 },
517 },
518 {
519 `empty group: ;`,
520 []*Address(nil),
521 },
522 {
523 `A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;`,
524 []*Address{
525 {
526 Name: "Ed Jones",
527 Address: "c@a.test",
528 },
529 {
530 Name: "",
531 Address: "joe@where.test",
532 },
533 {
534 Name: "John",
535 Address: "jdoe@one.test",
536 },
537 },
538 },
539
540 {
541 ` , joe@where.test,,John <jdoe@one.test>,`,
542 []*Address{
543 {
544 Name: "",
545 Address: "joe@where.test",
546 },
547 {
548 Name: "John",
549 Address: "jdoe@one.test",
550 },
551 },
552 },
553 {
554 ` , joe@where.test,,John <jdoe@one.test>,,`,
555 []*Address{
556 {
557 Name: "",
558 Address: "joe@where.test",
559 },
560 {
561 Name: "John",
562 Address: "jdoe@one.test",
563 },
564 },
565 },
566 {
567 `Group1: <addr1@example.com>;, Group 2: addr2@example.com;, John <addr3@example.com>`,
568 []*Address{
569 {
570 Name: "",
571 Address: "addr1@example.com",
572 },
573 {
574 Name: "",
575 Address: "addr2@example.com",
576 },
577 {
578 Name: "John",
579 Address: "addr3@example.com",
580 },
581 },
582 },
583
584 {
585 `=?iso-8859-1?q?J=F6rg_Doe?= <joerg@example.com>`,
586 []*Address{
587 {
588 Name: `Jörg Doe`,
589 Address: "joerg@example.com",
590 },
591 },
592 },
593
594 {
595 `=?us-ascii?q?J=6Frg_Doe?= <joerg@example.com>`,
596 []*Address{
597 {
598 Name: `Jorg Doe`,
599 Address: "joerg@example.com",
600 },
601 },
602 },
603
604 {
605 `=?utf-8?q?J=C3=B6rg_Doe?= <joerg@example.com>`,
606 []*Address{
607 {
608 Name: `Jörg Doe`,
609 Address: "joerg@example.com",
610 },
611 },
612 },
613
614 {
615 `=?utf-8?q?J=C3=B6rg?= =?utf-8?q?Doe?= <joerg@example.com>`,
616 []*Address{
617 {
618 Name: `JörgDoe`,
619 Address: "joerg@example.com",
620 },
621 },
622 },
623
624 {
625 `=?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>`,
626 []*Address{
627 {
628 Name: `André Pirard`,
629 Address: "PIRARD@vm1.ulg.ac.be",
630 },
631 },
632 },
633
634 {
635 `=?ISO-8859-1?B?SvZyZw==?= <joerg@example.com>`,
636 []*Address{
637 {
638 Name: `Jörg`,
639 Address: "joerg@example.com",
640 },
641 },
642 },
643
644 {
645 `=?UTF-8?B?SsO2cmc=?= <joerg@example.com>`,
646 []*Address{
647 {
648 Name: `Jörg`,
649 Address: "joerg@example.com",
650 },
651 },
652 },
653
654 {
655 `Asem H. <noreply@example.com>`,
656 []*Address{
657 {
658 Name: `Asem H.`,
659 Address: "noreply@example.com",
660 },
661 },
662 },
663
664 {
665 `"Gø Pher" <gopher@example.com>`,
666 []*Address{
667 {
668 Name: `Gø Pher`,
669 Address: "gopher@example.com",
670 },
671 },
672 },
673
674 {
675 `µ <micro@example.com>`,
676 []*Address{
677 {
678 Name: `µ`,
679 Address: "micro@example.com",
680 },
681 },
682 },
683
684 {
685 `Micro <µ@example.com>`,
686 []*Address{
687 {
688 Name: `Micro`,
689 Address: "µ@example.com",
690 },
691 },
692 },
693
694 {
695 `Micro <micro@µ.example.com>`,
696 []*Address{
697 {
698 Name: `Micro`,
699 Address: "micro@µ.example.com",
700 },
701 },
702 },
703
704 {
705 `"" <emptystring@example.com>`,
706 []*Address{
707 {
708 Name: "",
709 Address: "emptystring@example.com",
710 },
711 },
712 },
713
714 {
715 `<cfws@example.com> (CFWS (cfws)) (another comment)`,
716 []*Address{
717 {
718 Name: "",
719 Address: "cfws@example.com",
720 },
721 },
722 },
723 {
724 `<cfws@example.com> () (another comment), <cfws2@example.com> (another)`,
725 []*Address{
726 {
727 Name: "",
728 Address: "cfws@example.com",
729 },
730 {
731 Name: "",
732 Address: "cfws2@example.com",
733 },
734 },
735 },
736
737 {
738 `john@example.com (John Doe)`,
739 []*Address{
740 {
741 Name: "John Doe",
742 Address: "john@example.com",
743 },
744 },
745 },
746
747 {
748 `John Doe <john@example.com> (Joey)`,
749 []*Address{
750 {
751 Name: "John Doe",
752 Address: "john@example.com",
753 },
754 },
755 },
756
757 {
758 `john@example.com(John Doe)`,
759 []*Address{
760 {
761 Name: "John Doe",
762 Address: "john@example.com",
763 },
764 },
765 },
766
767 {
768 `asjo@example.com (Adam =?utf-8?Q?Sj=C3=B8gren?=)`,
769 []*Address{
770 {
771 Name: "Adam Sjøgren",
772 Address: "asjo@example.com",
773 },
774 },
775 },
776
777 {
778 `asjo@example.com (Adam =?utf-8?Q?Sj=C3=B8gren?=)`,
779 []*Address{
780 {
781 Name: "Adam Sjøgren",
782 Address: "asjo@example.com",
783 },
784 },
785 },
786
787 {
788 `asjo@example.com (Adam =?utf-8?Q?Sj=C3=B8gren?= (Debian))`,
789 []*Address{
790 {
791 Name: "Adam Sjøgren (Debian)",
792 Address: "asjo@example.com",
793 },
794 },
795 },
796
797 {
798 `group (comment:): a@example.com, b@example.com;`,
799 []*Address{
800 {
801 Address: "a@example.com",
802 },
803 {
804 Address: "b@example.com",
805 },
806 },
807 },
808 {
809 `x(:"):"@a.example;("@b.example;`,
810 []*Address{
811 {
812 Address: `@a.example;(@b.example`,
813 },
814 },
815 },
816
817 {
818 `jdoe@[192.168.0.1]`,
819 []*Address{{
820 Address: "jdoe@[192.168.0.1]",
821 }},
822 },
823 {
824 `John Doe <jdoe@[192.168.0.1]>`,
825 []*Address{{
826 Name: "John Doe",
827 Address: "jdoe@[192.168.0.1]",
828 }},
829 },
830
831 {
832 `jdoe@[IPv6:fd42::dead:beef:1234]`,
833 []*Address{{
834 Address: "jdoe@[IPv6:fd42::dead:beef:1234]",
835 }},
836 },
837 {
838 `John Doe <jdoe@[IPv6:fd42::dead:beef:1234]>`,
839 []*Address{{
840 Name: "John Doe",
841 Address: "jdoe@[IPv6:fd42::dead:beef:1234]",
842 }},
843 },
844 }
845 for _, test := range tests {
846 if len(test.exp) == 1 {
847 addr, err := ParseAddress(test.addrsStr)
848 if err != nil {
849 t.Errorf("Failed parsing (single) %q: %v", test.addrsStr, err)
850 continue
851 }
852 if !reflect.DeepEqual([]*Address{addr}, test.exp) {
853 t.Errorf("Parse (single) of %q: got %+v, want %+v", test.addrsStr, addr, test.exp)
854 }
855 }
856
857 addrs, err := ParseAddressList(test.addrsStr)
858 if err != nil {
859 t.Errorf("Failed parsing (list) %q: %v", test.addrsStr, err)
860 continue
861 }
862 if !reflect.DeepEqual(addrs, test.exp) {
863 t.Errorf("Parse (list) of %q: got %+v, want %+v", test.addrsStr, addrs, test.exp)
864 }
865 }
866 }
867
868 func TestAddressParser(t *testing.T) {
869 tests := []struct {
870 addrsStr string
871 exp []*Address
872 }{
873
874 {
875 `jdoe@machine.example`,
876 []*Address{{
877 Address: "jdoe@machine.example",
878 }},
879 },
880
881 {
882 `John Doe <jdoe@machine.example>`,
883 []*Address{{
884 Name: "John Doe",
885 Address: "jdoe@machine.example",
886 }},
887 },
888
889 {
890 `"Joe Q. Public" <john.q.public@example.com>`,
891 []*Address{{
892 Name: "Joe Q. Public",
893 Address: "john.q.public@example.com",
894 }},
895 },
896 {
897 `Mary Smith <mary@x.test>, jdoe@example.org, Who? <one@y.test>`,
898 []*Address{
899 {
900 Name: "Mary Smith",
901 Address: "mary@x.test",
902 },
903 {
904 Address: "jdoe@example.org",
905 },
906 {
907 Name: "Who?",
908 Address: "one@y.test",
909 },
910 },
911 },
912 {
913 `<boss@nil.test>, "Giant; \"Big\" Box" <sysservices@example.net>`,
914 []*Address{
915 {
916 Address: "boss@nil.test",
917 },
918 {
919 Name: `Giant; "Big" Box`,
920 Address: "sysservices@example.net",
921 },
922 },
923 },
924
925 {
926 `=?iso-8859-1?q?J=F6rg_Doe?= <joerg@example.com>`,
927 []*Address{
928 {
929 Name: `Jörg Doe`,
930 Address: "joerg@example.com",
931 },
932 },
933 },
934
935 {
936 `=?us-ascii?q?J=6Frg_Doe?= <joerg@example.com>`,
937 []*Address{
938 {
939 Name: `Jorg Doe`,
940 Address: "joerg@example.com",
941 },
942 },
943 },
944
945 {
946 `=?ISO-8859-15?Q?J=F6rg_Doe?= <joerg@example.com>`,
947 []*Address{
948 {
949 Name: `Jörg Doe`,
950 Address: "joerg@example.com",
951 },
952 },
953 },
954
955 {
956 `=?windows-1252?q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>`,
957 []*Address{
958 {
959 Name: `André Pirard`,
960 Address: "PIRARD@vm1.ulg.ac.be",
961 },
962 },
963 },
964
965 {
966 `=?ISO-8859-15?B?SvZyZw==?= <joerg@example.com>`,
967 []*Address{
968 {
969 Name: `Jörg`,
970 Address: "joerg@example.com",
971 },
972 },
973 },
974
975 {
976 `=?UTF-8?B?SsO2cmc=?= <joerg@example.com>`,
977 []*Address{
978 {
979 Name: `Jörg`,
980 Address: "joerg@example.com",
981 },
982 },
983 },
984
985 {
986 `Asem H. <noreply@example.com>`,
987 []*Address{
988 {
989 Name: `Asem H.`,
990 Address: "noreply@example.com",
991 },
992 },
993 },
994
995 {
996 `jdoe@[192.168.0.1]`,
997 []*Address{{
998 Address: "jdoe@[192.168.0.1]",
999 }},
1000 },
1001 {
1002 `John Doe <jdoe@[192.168.0.1]>`,
1003 []*Address{{
1004 Name: "John Doe",
1005 Address: "jdoe@[192.168.0.1]",
1006 }},
1007 },
1008
1009 {
1010 `jdoe@[IPv6:fd42::dead:beef:1234]`,
1011 []*Address{{
1012 Address: "jdoe@[IPv6:fd42::dead:beef:1234]",
1013 }},
1014 },
1015 {
1016 `John Doe <jdoe@[IPv6:fd42::dead:beef:1234]>`,
1017 []*Address{{
1018 Name: "John Doe",
1019 Address: "jdoe@[IPv6:fd42::dead:beef:1234]",
1020 }},
1021 },
1022 }
1023
1024 ap := AddressParser{WordDecoder: &mime.WordDecoder{
1025 CharsetReader: func(charset string, input io.Reader) (io.Reader, error) {
1026 in, err := io.ReadAll(input)
1027 if err != nil {
1028 return nil, err
1029 }
1030
1031 switch charset {
1032 case "iso-8859-15":
1033 in = bytes.ReplaceAll(in, []byte("\xf6"), []byte("ö"))
1034 case "windows-1252":
1035 in = bytes.ReplaceAll(in, []byte("\xe9"), []byte("é"))
1036 }
1037
1038 return bytes.NewReader(in), nil
1039 },
1040 }}
1041
1042 for _, test := range tests {
1043 if len(test.exp) == 1 {
1044 addr, err := ap.Parse(test.addrsStr)
1045 if err != nil {
1046 t.Errorf("Failed parsing (single) %q: %v", test.addrsStr, err)
1047 continue
1048 }
1049 if !reflect.DeepEqual([]*Address{addr}, test.exp) {
1050 t.Errorf("Parse (single) of %q: got %+v, want %+v", test.addrsStr, addr, test.exp)
1051 }
1052 }
1053
1054 addrs, err := ap.ParseList(test.addrsStr)
1055 if err != nil {
1056 t.Errorf("Failed parsing (list) %q: %v", test.addrsStr, err)
1057 continue
1058 }
1059 if !reflect.DeepEqual(addrs, test.exp) {
1060 t.Errorf("Parse (list) of %q: got %+v, want %+v", test.addrsStr, addrs, test.exp)
1061 }
1062 }
1063 }
1064
1065 func TestAddressString(t *testing.T) {
1066 tests := []struct {
1067 addr *Address
1068 exp string
1069 }{
1070 {
1071 &Address{Address: "bob@example.com"},
1072 "<bob@example.com>",
1073 },
1074 {
1075 &Address{Address: `my@idiot@address@example.com`},
1076 `<"my@idiot@address"@example.com>`,
1077 },
1078 {
1079 &Address{Address: ` @example.com`},
1080 `<" "@example.com>`,
1081 },
1082 {
1083 &Address{Name: "Bob", Address: "bob@example.com"},
1084 `"Bob" <bob@example.com>`,
1085 },
1086 {
1087
1088 &Address{Name: "Böb", Address: "bob@example.com"},
1089 `=?utf-8?q?B=C3=B6b?= <bob@example.com>`,
1090 },
1091 {
1092 &Address{Name: "Bob Jane", Address: "bob@example.com"},
1093 `"Bob Jane" <bob@example.com>`,
1094 },
1095 {
1096 &Address{Name: "Böb Jacöb", Address: "bob@example.com"},
1097 `=?utf-8?q?B=C3=B6b_Jac=C3=B6b?= <bob@example.com>`,
1098 },
1099 {
1100 &Address{Name: "Rob", Address: ""},
1101 `"Rob" <@>`,
1102 },
1103 {
1104 &Address{Name: "Rob", Address: "@"},
1105 `"Rob" <@>`,
1106 },
1107 {
1108 &Address{Name: "Böb, Jacöb", Address: "bob@example.com"},
1109 `=?utf-8?b?QsO2YiwgSmFjw7Zi?= <bob@example.com>`,
1110 },
1111 {
1112 &Address{Name: "=??Q?x?=", Address: "hello@world.com"},
1113 `"=??Q?x?=" <hello@world.com>`,
1114 },
1115 {
1116 &Address{Name: "=?hello", Address: "hello@world.com"},
1117 `"=?hello" <hello@world.com>`,
1118 },
1119 {
1120 &Address{Name: "world?=", Address: "hello@world.com"},
1121 `"world?=" <hello@world.com>`,
1122 },
1123 {
1124
1125 &Address{Name: string([]byte{0xed, 0xa0, 0x80}), Address: "invalid-utf8@example.net"},
1126 "=?utf-8?q?=ED=A0=80?= <invalid-utf8@example.net>",
1127 },
1128
1129 {
1130 &Address{Address: "bob@[192.168.0.1]"},
1131 "<bob@[192.168.0.1]>",
1132 },
1133 {
1134 &Address{Name: "Bob", Address: "bob@[192.168.0.1]"},
1135 `"Bob" <bob@[192.168.0.1]>`,
1136 },
1137
1138 {
1139 &Address{Address: "bob@[IPv6:fd42::dead:beef:1234]"},
1140 "<bob@[IPv6:fd42::dead:beef:1234]>",
1141 },
1142 {
1143 &Address{Name: "Bob", Address: "bob@[IPv6:fd42::dead:beef:1234]"},
1144 `"Bob" <bob@[IPv6:fd42::dead:beef:1234]>`,
1145 },
1146 }
1147 for _, test := range tests {
1148 s := test.addr.String()
1149 if s != test.exp {
1150 t.Errorf("Address%+v.String() = %v, want %v", *test.addr, s, test.exp)
1151 continue
1152 }
1153
1154
1155 if test.addr.Address != "" && test.addr.Address != "@" {
1156 a, err := ParseAddress(test.exp)
1157 if err != nil {
1158 t.Errorf("ParseAddress(%#q): %v", test.exp, err)
1159 continue
1160 }
1161 if a.Name != test.addr.Name || a.Address != test.addr.Address {
1162 t.Errorf("ParseAddress(%#q) = %#v, want %#v", test.exp, a, test.addr)
1163 }
1164 }
1165 }
1166 }
1167
1168
1169 func TestAddressParsingAndFormatting(t *testing.T) {
1170
1171
1172 tests := []string{
1173 `<Bob@example.com>`,
1174 `<bob.bob@example.com>`,
1175 `<".bob"@example.com>`,
1176 `<" "@example.com>`,
1177 `<some.mail-with-dash@example.com>`,
1178 `<"dot.and space"@example.com>`,
1179 `<"very.unusual.@.unusual.com"@example.com>`,
1180 `<admin@mailserver1>`,
1181 `<postmaster@localhost>`,
1182 "<#!$%&'*+-/=?^_`{}|~@example.org>",
1183 `<"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com>`,
1184 `<"()<>[]:,;@\\\"!#$%&'*+-/=?^_{}| ~.a"@example.org>`,
1185 `<"Abc\\@def"@example.com>`,
1186 `<"Joe\\Blow"@example.com>`,
1187 `<test1/test2=test3@example.com>`,
1188 `<def!xyz%abc@example.com>`,
1189 `<_somename@example.com>`,
1190 `<joe@uk>`,
1191 `<~@example.com>`,
1192 `<"..."@test.com>`,
1193 `<"john..doe"@example.com>`,
1194 `<"john.doe."@example.com>`,
1195 `<".john.doe"@example.com>`,
1196 `<"."@example.com>`,
1197 `<".."@example.com>`,
1198 `<"0:"@0>`,
1199 `<Bob@[192.168.0.1]>`,
1200 }
1201
1202 for _, test := range tests {
1203 addr, err := ParseAddress(test)
1204 if err != nil {
1205 t.Errorf("Couldn't parse address %s: %s", test, err.Error())
1206 continue
1207 }
1208 str := addr.String()
1209 addr, err = ParseAddress(str)
1210 if err != nil {
1211 t.Errorf("ParseAddr(%q) error: %v", test, err)
1212 continue
1213 }
1214
1215 if addr.String() != test {
1216 t.Errorf("String() round-trip = %q; want %q", addr, test)
1217 continue
1218 }
1219
1220 }
1221
1222
1223 badTests := []string{
1224 `<Abc.example.com>`,
1225 `<A@b@c@example.com>`,
1226 `<a"b(c)d,e:f;g<h>i[j\k]l@example.com>`,
1227 `<just"not"right@example.com>`,
1228 `<this is"not\allowed@example.com>`,
1229 `<this\ still\"not\\allowed@example.com>`,
1230 `<john..doe@example.com>`,
1231 `<john.doe@example..com>`,
1232 `<john.doe@example..com>`,
1233 `<john.doe.@example.com>`,
1234 `<john.doe.@.example.com>`,
1235 `<.john.doe@example.com>`,
1236 `<@example.com>`,
1237 `<.@example.com>`,
1238 `<test@.>`,
1239 `< @example.com>`,
1240 `<""test""blah""@example.com>`,
1241 `<""@0>`,
1242 }
1243
1244 for _, test := range badTests {
1245 _, err := ParseAddress(test)
1246 if err == nil {
1247 t.Errorf("Should have failed to parse address: %s", test)
1248 continue
1249 }
1250
1251 }
1252
1253 }
1254
1255 func TestAddressFormattingAndParsing(t *testing.T) {
1256 tests := []*Address{
1257 {Name: "@lïce", Address: "alice@example.com"},
1258 {Name: "Böb O'Connor", Address: "bob@example.com"},
1259 {Name: "???", Address: "bob@example.com"},
1260 {Name: "Böb ???", Address: "bob@example.com"},
1261 {Name: "Böb (Jacöb)", Address: "bob@example.com"},
1262 {Name: "à#$%&'(),.:;<>@[]^`{|}~'", Address: "bob@example.com"},
1263
1264 {Name: "\"\\\x1f,\"", Address: "0@0"},
1265
1266 {Name: "naé, mée", Address: "test.mail@gmail.com"},
1267 }
1268
1269 for i, test := range tests {
1270 parsed, err := ParseAddress(test.String())
1271 if err != nil {
1272 t.Errorf("test #%d: ParseAddr(%q) error: %v", i, test.String(), err)
1273 continue
1274 }
1275 if parsed.Name != test.Name {
1276 t.Errorf("test #%d: Parsed name = %q; want %q", i, parsed.Name, test.Name)
1277 }
1278 if parsed.Address != test.Address {
1279 t.Errorf("test #%d: Parsed address = %q; want %q", i, parsed.Address, test.Address)
1280 }
1281 }
1282 }
1283
1284 func TestEmptyAddress(t *testing.T) {
1285 parsed, err := ParseAddress("")
1286 if parsed != nil || err == nil {
1287 t.Errorf(`ParseAddress("") = %v, %v, want nil, error`, parsed, err)
1288 }
1289 list, err := ParseAddressList("")
1290 if len(list) > 0 || err == nil {
1291 t.Errorf(`ParseAddressList("") = %v, %v, want nil, error`, list, err)
1292 }
1293 list, err = ParseAddressList(",")
1294 if len(list) > 0 || err == nil {
1295 t.Errorf(`ParseAddressList("") = %v, %v, want nil, error`, list, err)
1296 }
1297 list, err = ParseAddressList("a@b c@d")
1298 if len(list) > 0 || err == nil {
1299 t.Errorf(`ParseAddressList("") = %v, %v, want nil, error`, list, err)
1300 }
1301 }
1302
1303 func BenchmarkConsumePhrase(b *testing.B) {
1304 for _, n := range []int{10, 100, 1000, 10000} {
1305 b.Run(fmt.Sprintf("words-%d", n), func(b *testing.B) {
1306 input := strings.Repeat("=?utf-8?q?hello?= ", n) + "<user@example.com>"
1307 for b.Loop() {
1308 (&addrParser{s: input}).consumePhrase()
1309 }
1310 })
1311 }
1312 }
1313
1314 func BenchmarkConsumeComment(b *testing.B) {
1315 for _, n := range []int{10, 100, 1000, 10000} {
1316 b.Run(fmt.Sprintf("depth-%d", n), func(b *testing.B) {
1317
1318 open := strings.Repeat("(", n)
1319 close := strings.Repeat(")", n)
1320
1321
1322
1323 input := open[:n-1] + "a" + close
1324 for b.Loop() {
1325 p := addrParser{s: input}
1326 p.consumeComment()
1327 }
1328 })
1329 }
1330 }
1331
View as plain text