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