go/src/net/mail/message_test.go
Nuno Gonçalves d14cf8f91b net/mail: enhance address parser to include support for domain literals
The existing implementation of the mail package conforms to RFC 5322
for parsing mail messages, but it lacks support for domain literals. This
patch addresses this limitation by adding support for domain literals in
the address parser.

The Addr-Spec Specification, defined in RFC 5322 Section 3.4.1,
outlines the format for email addresses:
https://datatracker.ietf.org/doc/html/rfc5322\#section-3.4.1

Fixes #60206

Change-Id: Ic901418325bd1da69e70800d70b87d658b953738
GitHub-Last-Rev: bdda66f3fe098df3a62d803b1f69e63fef6281e3
GitHub-Pull-Request: golang/go#66075
Reviewed-on: https://go-review.googlesource.com/c/go/+/567777
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Ian Lance Taylor <iant@google.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: David Chase <drchase@google.com>
2024-03-19 11:31:03 +00:00

1262 lines
30 KiB
Go

// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mail
import (
"bytes"
"io"
"mime"
"reflect"
"strings"
"testing"
"time"
)
var parseTests = []struct {
in string
header Header
body string
}{
{
// RFC 5322, Appendix A.1.1
in: `From: John Doe <jdoe@machine.example>
To: Mary Smith <mary@example.net>
Subject: Saying Hello
Date: Fri, 21 Nov 1997 09:55:06 -0600
Message-ID: <1234@local.machine.example>
This is a message just to say hello.
So, "Hello".
`,
header: Header{
"From": []string{"John Doe <jdoe@machine.example>"},
"To": []string{"Mary Smith <mary@example.net>"},
"Subject": []string{"Saying Hello"},
"Date": []string{"Fri, 21 Nov 1997 09:55:06 -0600"},
"Message-Id": []string{"<1234@local.machine.example>"},
},
body: "This is a message just to say hello.\nSo, \"Hello\".\n",
},
{
// RFC 5965, Appendix B.1, a part of the multipart message (a header-only sub message)
in: `Feedback-Type: abuse
User-Agent: SomeGenerator/1.0
Version: 1
`,
header: Header{
"Feedback-Type": []string{"abuse"},
"User-Agent": []string{"SomeGenerator/1.0"},
"Version": []string{"1"},
},
body: "",
},
{
// RFC 5322 permits any printable ASCII character,
// except colon, in a header key. Issue #58862.
in: `From: iant@golang.org
Custom/Header: v
Body
`,
header: Header{
"From": []string{"iant@golang.org"},
"Custom/Header": []string{"v"},
},
body: "Body\n",
},
{
// RFC 4155 mbox format. We've historically permitted this,
// so we continue to permit it. Issue #60332.
in: `From iant@golang.org Mon Jun 19 00:00:00 2023
From: iant@golang.org
Hello, gophers!
`,
header: Header{
"From": []string{"iant@golang.org"},
"From iant@golang.org Mon Jun 19 00": []string{"00:00 2023"},
},
body: "Hello, gophers!\n",
},
}
func TestParsing(t *testing.T) {
for i, test := range parseTests {
msg, err := ReadMessage(bytes.NewBuffer([]byte(test.in)))
if err != nil {
t.Errorf("test #%d: Failed parsing message: %v", i, err)
continue
}
if !headerEq(msg.Header, test.header) {
t.Errorf("test #%d: Incorrectly parsed message header.\nGot:\n%+v\nWant:\n%+v",
i, msg.Header, test.header)
}
body, err := io.ReadAll(msg.Body)
if err != nil {
t.Errorf("test #%d: Failed reading body: %v", i, err)
continue
}
bodyStr := string(body)
if bodyStr != test.body {
t.Errorf("test #%d: Incorrectly parsed message body.\nGot:\n%+v\nWant:\n%+v",
i, bodyStr, test.body)
}
}
}
func headerEq(a, b Header) bool {
if len(a) != len(b) {
return false
}
for k, as := range a {
bs, ok := b[k]
if !ok {
return false
}
if !reflect.DeepEqual(as, bs) {
return false
}
}
return true
}
func TestDateParsing(t *testing.T) {
tests := []struct {
dateStr string
exp time.Time
}{
// RFC 5322, Appendix A.1.1
{
"Fri, 21 Nov 1997 09:55:06 -0600",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
},
// RFC 5322, Appendix A.6.2
// Obsolete date.
{
"21 Nov 97 09:55:06 GMT",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("GMT", 0)),
},
// Commonly found format not specified by RFC 5322.
{
"Fri, 21 Nov 1997 09:55:06 -0600 (MDT)",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
},
{
"Thu, 20 Nov 1997 09:55:06 -0600 (MDT)",
time.Date(1997, 11, 20, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
},
{
"Thu, 20 Nov 1997 09:55:06 GMT (GMT)",
time.Date(1997, 11, 20, 9, 55, 6, 0, time.UTC),
},
{
"Fri, 21 Nov 1997 09:55:06 +1300 (TOT)",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", +13*60*60)),
},
}
for _, test := range tests {
hdr := Header{
"Date": []string{test.dateStr},
}
date, err := hdr.Date()
if err != nil {
t.Errorf("Header(Date: %s).Date(): %v", test.dateStr, err)
} else if !date.Equal(test.exp) {
t.Errorf("Header(Date: %s).Date() = %+v, want %+v", test.dateStr, date, test.exp)
}
date, err = ParseDate(test.dateStr)
if err != nil {
t.Errorf("ParseDate(%s): %v", test.dateStr, err)
} else if !date.Equal(test.exp) {
t.Errorf("ParseDate(%s) = %+v, want %+v", test.dateStr, date, test.exp)
}
}
}
func TestDateParsingCFWS(t *testing.T) {
tests := []struct {
dateStr string
exp time.Time
valid bool
}{
// FWS-only. No date.
{
" ",
// nil is not allowed
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
false,
},
// FWS is allowed before optional day of week.
{
" Fri, 21 Nov 1997 09:55:06 -0600",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
true,
},
{
"21 Nov 1997 09:55:06 -0600",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
true,
},
{
"Fri 21 Nov 1997 09:55:06 -0600",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
false, // missing ,
},
// FWS is allowed before day of month but HTAB fails.
{
"Fri, 21 Nov 1997 09:55:06 -0600",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
true,
},
// FWS is allowed before and after year but HTAB fails.
{
"Fri, 21 Nov 1997 09:55:06 -0600",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
true,
},
// FWS is allowed before zone but HTAB is not handled. Obsolete timezone is handled.
{
"Fri, 21 Nov 1997 09:55:06 CST",
time.Time{},
true,
},
// FWS is allowed after date and a CRLF is already replaced.
{
"Fri, 21 Nov 1997 09:55:06 CST (no leading FWS and a trailing CRLF) \r\n",
time.Time{},
true,
},
// CFWS is a reduced set of US-ASCII where space and accentuated are obsolete. No error.
{
"Fri, 21 Nov 1997 09:55:06 -0600 (MDT and non-US-ASCII signs éèç )",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
true,
},
// CFWS is allowed after zone including a nested comment.
// Trailing FWS is allowed.
{
"Fri, 21 Nov 1997 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
true,
},
// CRLF is incomplete and misplaced.
{
"Fri, 21 Nov 1997 \r 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
false,
},
// CRLF is complete but misplaced. No error is returned.
{
"Fri, 21 Nov 199\r\n7 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
true, // should be false in the strict interpretation of RFC 5322.
},
// Invalid ASCII in date.
{
"Fri, 21 Nov 1997 ù 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
false,
},
// CFWS chars () in date.
{
"Fri, 21 Nov () 1997 09:55:06 -0600 \r\n (thisisa(valid)cfws) \t ",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
false,
},
// Timezone is invalid but T is found in comment.
{
"Fri, 21 Nov 1997 09:55:06 -060 \r\n (Thisisa(valid)cfws) \t ",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
false,
},
// Date has no month.
{
"Fri, 21 1997 09:55:06 -0600",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
false,
},
// Invalid month : OCT iso Oct
{
"Fri, 21 OCT 1997 09:55:06 CST",
time.Time{},
false,
},
// A too short time zone.
{
"Fri, 21 Nov 1997 09:55:06 -060",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
false,
},
// A too short obsolete time zone.
{
"Fri, 21 1997 09:55:06 GT",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.FixedZone("", -6*60*60)),
false,
},
// Ensure that the presence of "T" in the date
// doesn't trip out ParseDate, as per issue 39260.
{
"Tue, 26 May 2020 14:04:40 GMT",
time.Date(2020, 05, 26, 14, 04, 40, 0, time.UTC),
true,
},
{
"Tue, 26 May 2020 14:04:40 UT",
time.Date(2020, 05, 26, 14, 04, 40, 0, time.UTC),
true,
},
{
"Thu, 21 May 2020 14:04:40 UT",
time.Date(2020, 05, 21, 14, 04, 40, 0, time.UTC),
true,
},
{
"Tue, 26 May 2020 14:04:40 XT",
time.Date(2020, 05, 26, 14, 04, 40, 0, time.UTC),
false,
},
{
"Thu, 21 May 2020 14:04:40 XT",
time.Date(2020, 05, 21, 14, 04, 40, 0, time.UTC),
false,
},
{
"Thu, 21 May 2020 14:04:40 UTC",
time.Date(2020, 05, 21, 14, 04, 40, 0, time.UTC),
true,
},
{
"Fri, 21 Nov 1997 09:55:06 GMT (GMT)",
time.Date(1997, 11, 21, 9, 55, 6, 0, time.UTC),
true,
},
}
for _, test := range tests {
hdr := Header{
"Date": []string{test.dateStr},
}
date, err := hdr.Date()
if err != nil && test.valid {
t.Errorf("Header(Date: %s).Date(): %v", test.dateStr, err)
} else if err == nil && test.exp.IsZero() {
// OK. Used when exact result depends on the
// system's local zoneinfo.
} else if err == nil && !date.Equal(test.exp) && test.valid {
t.Errorf("Header(Date: %s).Date() = %+v, want %+v", test.dateStr, date, test.exp)
} else if err == nil && !test.valid { // an invalid expression was tested
t.Errorf("Header(Date: %s).Date() did not return an error but %v", test.dateStr, date)
}
date, err = ParseDate(test.dateStr)
if err != nil && test.valid {
t.Errorf("ParseDate(%s): %v", test.dateStr, err)
} else if err == nil && test.exp.IsZero() {
// OK. Used when exact result depends on the
// system's local zoneinfo.
} else if err == nil && !test.valid { // an invalid expression was tested
t.Errorf("ParseDate(%s) did not return an error but %v", test.dateStr, date)
} else if err == nil && test.valid && !date.Equal(test.exp) {
t.Errorf("ParseDate(%s) = %+v, want %+v", test.dateStr, date, test.exp)
}
}
}
func TestAddressParsingError(t *testing.T) {
mustErrTestCases := [...]struct {
text string
wantErrText string
}{
0: {"=?iso-8859-2?Q?Bogl=E1rka_Tak=E1cs?= <unknown@gmail.com>", "charset not supported"},
1: {"a@gmail.com b@gmail.com", "expected single address"},
2: {string([]byte{0xed, 0xa0, 0x80}) + " <micro@example.net>", "invalid utf-8 in address"},
3: {"\"" + string([]byte{0xed, 0xa0, 0x80}) + "\" <half-surrogate@example.com>", "invalid utf-8 in quoted-string"},
4: {"\"\\" + string([]byte{0x80}) + "\" <escaped-invalid-unicode@example.net>", "invalid utf-8 in quoted-string"},
5: {"\"\x00\" <null@example.net>", "bad character in quoted-string"},
6: {"\"\\\x00\" <escaped-null@example.net>", "bad character in quoted-string"},
7: {"John Doe", "no angle-addr"},
8: {`<jdoe#machine.example>`, "missing @ in addr-spec"},
9: {`John <middle> Doe <jdoe@machine.example>`, "missing @ in addr-spec"},
10: {"cfws@example.com (", "misformatted parenthetical comment"},
11: {"empty group: ;", "empty group"},
12: {"root group: embed group: null@example.com;", "no angle-addr"},
13: {"group not closed: null@example.com", "expected comma"},
14: {"group: first@example.com, second@example.com;", "group with multiple addresses"},
15: {"john.doe", "missing '@' or angle-addr"},
16: {"john.doe@", "missing '@' or angle-addr"},
17: {"John Doe@foo.bar", "no angle-addr"},
18: {" group: null@example.com; (asd", "misformatted parenthetical comment"},
19: {" group: ; (asd", "misformatted parenthetical comment"},
20: {`(John) Doe <jdoe@machine.example>`, "missing word in phrase:"},
21: {"<jdoe@[" + string([]byte{0xed, 0xa0, 0x80}) + "192.168.0.1]>", "invalid utf-8 in domain-literal"},
22: {"<jdoe@[[192.168.0.1]>", "bad character in domain-literal"},
23: {"<jdoe@[192.168.0.1>", "unclosed domain-literal"},
24: {"<jdoe@[256.0.0.1]>", "invalid IP address in domain-literal"},
}
for i, tc := range mustErrTestCases {
_, err := ParseAddress(tc.text)
if err == nil || !strings.Contains(err.Error(), tc.wantErrText) {
t.Errorf(`mail.ParseAddress(%q) #%d want %q, got %v`, tc.text, i, tc.wantErrText, err)
}
}
t.Run("CustomWordDecoder", func(t *testing.T) {
p := &AddressParser{WordDecoder: &mime.WordDecoder{}}
for i, tc := range mustErrTestCases {
_, err := p.Parse(tc.text)
if err == nil || !strings.Contains(err.Error(), tc.wantErrText) {
t.Errorf(`p.Parse(%q) #%d want %q, got %v`, tc.text, i, tc.wantErrText, err)
}
}
})
}
func TestAddressParsing(t *testing.T) {
tests := []struct {
addrsStr string
exp []*Address
}{
// Bare address
{
`jdoe@machine.example`,
[]*Address{{
Address: "jdoe@machine.example",
}},
},
// RFC 5322, Appendix A.1.1
{
`John Doe <jdoe@machine.example>`,
[]*Address{{
Name: "John Doe",
Address: "jdoe@machine.example",
}},
},
// RFC 5322, Appendix A.1.2
{
`"Joe Q. Public" <john.q.public@example.com>`,
[]*Address{{
Name: "Joe Q. Public",
Address: "john.q.public@example.com",
}},
},
// Comment in display name
{
`John (middle) Doe <jdoe@machine.example>`,
[]*Address{{
Name: "John Doe",
Address: "jdoe@machine.example",
}},
},
// Display name is quoted string, so comment is not a comment
{
`"John (middle) Doe" <jdoe@machine.example>`,
[]*Address{{
Name: "John (middle) Doe",
Address: "jdoe@machine.example",
}},
},
{
`"John <middle> Doe" <jdoe@machine.example>`,
[]*Address{{
Name: "John <middle> Doe",
Address: "jdoe@machine.example",
}},
},
{
`Mary Smith <mary@x.test>, jdoe@example.org, Who? <one@y.test>`,
[]*Address{
{
Name: "Mary Smith",
Address: "mary@x.test",
},
{
Address: "jdoe@example.org",
},
{
Name: "Who?",
Address: "one@y.test",
},
},
},
{
`<boss@nil.test>, "Giant; \"Big\" Box" <sysservices@example.net>`,
[]*Address{
{
Address: "boss@nil.test",
},
{
Name: `Giant; "Big" Box`,
Address: "sysservices@example.net",
},
},
},
// RFC 5322, Appendix A.6.1
{
`Joe Q. Public <john.q.public@example.com>`,
[]*Address{{
Name: "Joe Q. Public",
Address: "john.q.public@example.com",
}},
},
// RFC 5322, Appendix A.1.3
{
`group1: groupaddr1@example.com;`,
[]*Address{
{
Name: "",
Address: "groupaddr1@example.com",
},
},
},
{
`empty group: ;`,
[]*Address(nil),
},
{
`A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;`,
[]*Address{
{
Name: "Ed Jones",
Address: "c@a.test",
},
{
Name: "",
Address: "joe@where.test",
},
{
Name: "John",
Address: "jdoe@one.test",
},
},
},
// RFC5322 4.4 obs-addr-list
{
` , joe@where.test,,John <jdoe@one.test>,`,
[]*Address{
{
Name: "",
Address: "joe@where.test",
},
{
Name: "John",
Address: "jdoe@one.test",
},
},
},
{
` , joe@where.test,,John <jdoe@one.test>,,`,
[]*Address{
{
Name: "",
Address: "joe@where.test",
},
{
Name: "John",
Address: "jdoe@one.test",
},
},
},
{
`Group1: <addr1@example.com>;, Group 2: addr2@example.com;, John <addr3@example.com>`,
[]*Address{
{
Name: "",
Address: "addr1@example.com",
},
{
Name: "",
Address: "addr2@example.com",
},
{
Name: "John",
Address: "addr3@example.com",
},
},
},
// RFC 2047 "Q"-encoded ISO-8859-1 address.
{
`=?iso-8859-1?q?J=F6rg_Doe?= <joerg@example.com>`,
[]*Address{
{
Name: `Jörg Doe`,
Address: "joerg@example.com",
},
},
},
// RFC 2047 "Q"-encoded US-ASCII address. Dumb but legal.
{
`=?us-ascii?q?J=6Frg_Doe?= <joerg@example.com>`,
[]*Address{
{
Name: `Jorg Doe`,
Address: "joerg@example.com",
},
},
},
// RFC 2047 "Q"-encoded UTF-8 address.
{
`=?utf-8?q?J=C3=B6rg_Doe?= <joerg@example.com>`,
[]*Address{
{
Name: `Jörg Doe`,
Address: "joerg@example.com",
},
},
},
// RFC 2047 "Q"-encoded UTF-8 address with multiple encoded-words.
{
`=?utf-8?q?J=C3=B6rg?= =?utf-8?q?Doe?= <joerg@example.com>`,
[]*Address{
{
Name: `JörgDoe`,
Address: "joerg@example.com",
},
},
},
// RFC 2047, Section 8.
{
`=?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>`,
[]*Address{
{
Name: `André Pirard`,
Address: "PIRARD@vm1.ulg.ac.be",
},
},
},
// Custom example of RFC 2047 "B"-encoded ISO-8859-1 address.
{
`=?ISO-8859-1?B?SvZyZw==?= <joerg@example.com>`,
[]*Address{
{
Name: `Jörg`,
Address: "joerg@example.com",
},
},
},
// Custom example of RFC 2047 "B"-encoded UTF-8 address.
{
`=?UTF-8?B?SsO2cmc=?= <joerg@example.com>`,
[]*Address{
{
Name: `Jörg`,
Address: "joerg@example.com",
},
},
},
// Custom example with "." in name. For issue 4938
{
`Asem H. <noreply@example.com>`,
[]*Address{
{
Name: `Asem H.`,
Address: "noreply@example.com",
},
},
},
// RFC 6532 3.2.3, qtext /= UTF8-non-ascii
{
`"Gø Pher" <gopher@example.com>`,
[]*Address{
{
Name: `Gø Pher`,
Address: "gopher@example.com",
},
},
},
// RFC 6532 3.2, atext /= UTF8-non-ascii
{
`µ <micro@example.com>`,
[]*Address{
{
Name: `µ`,
Address: "micro@example.com",
},
},
},
// RFC 6532 3.2.2, local address parts allow UTF-8
{
`Micro <µ@example.com>`,
[]*Address{
{
Name: `Micro`,
Address: "µ@example.com",
},
},
},
// RFC 6532 3.2.4, domains parts allow UTF-8
{
`Micro <micro@µ.example.com>`,
[]*Address{
{
Name: `Micro`,
Address: "micro@µ.example.com",
},
},
},
// Issue 14866
{
`"" <emptystring@example.com>`,
[]*Address{
{
Name: "",
Address: "emptystring@example.com",
},
},
},
// CFWS
{
`<cfws@example.com> (CFWS (cfws)) (another comment)`,
[]*Address{
{
Name: "",
Address: "cfws@example.com",
},
},
},
{
`<cfws@example.com> () (another comment), <cfws2@example.com> (another)`,
[]*Address{
{
Name: "",
Address: "cfws@example.com",
},
{
Name: "",
Address: "cfws2@example.com",
},
},
},
// Comment as display name
{
`john@example.com (John Doe)`,
[]*Address{
{
Name: "John Doe",
Address: "john@example.com",
},
},
},
// Comment and display name
{
`John Doe <john@example.com> (Joey)`,
[]*Address{
{
Name: "John Doe",
Address: "john@example.com",
},
},
},
// Comment as display name, no space
{
`john@example.com(John Doe)`,
[]*Address{
{
Name: "John Doe",
Address: "john@example.com",
},
},
},
// Comment as display name, Q-encoded
{
`asjo@example.com (Adam =?utf-8?Q?Sj=C3=B8gren?=)`,
[]*Address{
{
Name: "Adam Sjøgren",
Address: "asjo@example.com",
},
},
},
// Comment as display name, Q-encoded and tab-separated
{
`asjo@example.com (Adam =?utf-8?Q?Sj=C3=B8gren?=)`,
[]*Address{
{
Name: "Adam Sjøgren",
Address: "asjo@example.com",
},
},
},
// Nested comment as display name, Q-encoded
{
`asjo@example.com (Adam =?utf-8?Q?Sj=C3=B8gren?= (Debian))`,
[]*Address{
{
Name: "Adam Sjøgren (Debian)",
Address: "asjo@example.com",
},
},
},
// Comment in group display name
{
`group (comment:): a@example.com, b@example.com;`,
[]*Address{
{
Address: "a@example.com",
},
{
Address: "b@example.com",
},
},
},
{
`x(:"):"@a.example;("@b.example;`,
[]*Address{
{
Address: `@a.example;(@b.example`,
},
},
},
// Domain-literal
{
`jdoe@[192.168.0.1]`,
[]*Address{{
Address: "jdoe@[192.168.0.1]",
}},
},
{
`John Doe <jdoe@[192.168.0.1]>`,
[]*Address{{
Name: "John Doe",
Address: "jdoe@[192.168.0.1]",
}},
},
}
for _, test := range tests {
if len(test.exp) == 1 {
addr, err := ParseAddress(test.addrsStr)
if err != nil {
t.Errorf("Failed parsing (single) %q: %v", test.addrsStr, err)
continue
}
if !reflect.DeepEqual([]*Address{addr}, test.exp) {
t.Errorf("Parse (single) of %q: got %+v, want %+v", test.addrsStr, addr, test.exp)
}
}
addrs, err := ParseAddressList(test.addrsStr)
if err != nil {
t.Errorf("Failed parsing (list) %q: %v", test.addrsStr, err)
continue
}
if !reflect.DeepEqual(addrs, test.exp) {
t.Errorf("Parse (list) of %q: got %+v, want %+v", test.addrsStr, addrs, test.exp)
}
}
}
func TestAddressParser(t *testing.T) {
tests := []struct {
addrsStr string
exp []*Address
}{
// Bare address
{
`jdoe@machine.example`,
[]*Address{{
Address: "jdoe@machine.example",
}},
},
// RFC 5322, Appendix A.1.1
{
`John Doe <jdoe@machine.example>`,
[]*Address{{
Name: "John Doe",
Address: "jdoe@machine.example",
}},
},
// RFC 5322, Appendix A.1.2
{
`"Joe Q. Public" <john.q.public@example.com>`,
[]*Address{{
Name: "Joe Q. Public",
Address: "john.q.public@example.com",
}},
},
{
`Mary Smith <mary@x.test>, jdoe@example.org, Who? <one@y.test>`,
[]*Address{
{
Name: "Mary Smith",
Address: "mary@x.test",
},
{
Address: "jdoe@example.org",
},
{
Name: "Who?",
Address: "one@y.test",
},
},
},
{
`<boss@nil.test>, "Giant; \"Big\" Box" <sysservices@example.net>`,
[]*Address{
{
Address: "boss@nil.test",
},
{
Name: `Giant; "Big" Box`,
Address: "sysservices@example.net",
},
},
},
// RFC 2047 "Q"-encoded ISO-8859-1 address.
{
`=?iso-8859-1?q?J=F6rg_Doe?= <joerg@example.com>`,
[]*Address{
{
Name: `Jörg Doe`,
Address: "joerg@example.com",
},
},
},
// RFC 2047 "Q"-encoded US-ASCII address. Dumb but legal.
{
`=?us-ascii?q?J=6Frg_Doe?= <joerg@example.com>`,
[]*Address{
{
Name: `Jorg Doe`,
Address: "joerg@example.com",
},
},
},
// RFC 2047 "Q"-encoded ISO-8859-15 address.
{
`=?ISO-8859-15?Q?J=F6rg_Doe?= <joerg@example.com>`,
[]*Address{
{
Name: `Jörg Doe`,
Address: "joerg@example.com",
},
},
},
// RFC 2047 "B"-encoded windows-1252 address.
{
`=?windows-1252?q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>`,
[]*Address{
{
Name: `André Pirard`,
Address: "PIRARD@vm1.ulg.ac.be",
},
},
},
// Custom example of RFC 2047 "B"-encoded ISO-8859-15 address.
{
`=?ISO-8859-15?B?SvZyZw==?= <joerg@example.com>`,
[]*Address{
{
Name: `Jörg`,
Address: "joerg@example.com",
},
},
},
// Custom example of RFC 2047 "B"-encoded UTF-8 address.
{
`=?UTF-8?B?SsO2cmc=?= <joerg@example.com>`,
[]*Address{
{
Name: `Jörg`,
Address: "joerg@example.com",
},
},
},
// Custom example with "." in name. For issue 4938
{
`Asem H. <noreply@example.com>`,
[]*Address{
{
Name: `Asem H.`,
Address: "noreply@example.com",
},
},
},
// Domain-literal
{
`jdoe@[192.168.0.1]`,
[]*Address{{
Address: "jdoe@[192.168.0.1]",
}},
},
{
`John Doe <jdoe@[192.168.0.1]>`,
[]*Address{{
Name: "John Doe",
Address: "jdoe@[192.168.0.1]",
}},
},
}
ap := AddressParser{WordDecoder: &mime.WordDecoder{
CharsetReader: func(charset string, input io.Reader) (io.Reader, error) {
in, err := io.ReadAll(input)
if err != nil {
return nil, err
}
switch charset {
case "iso-8859-15":
in = bytes.ReplaceAll(in, []byte("\xf6"), []byte("ö"))
case "windows-1252":
in = bytes.ReplaceAll(in, []byte("\xe9"), []byte("é"))
}
return bytes.NewReader(in), nil
},
}}
for _, test := range tests {
if len(test.exp) == 1 {
addr, err := ap.Parse(test.addrsStr)
if err != nil {
t.Errorf("Failed parsing (single) %q: %v", test.addrsStr, err)
continue
}
if !reflect.DeepEqual([]*Address{addr}, test.exp) {
t.Errorf("Parse (single) of %q: got %+v, want %+v", test.addrsStr, addr, test.exp)
}
}
addrs, err := ap.ParseList(test.addrsStr)
if err != nil {
t.Errorf("Failed parsing (list) %q: %v", test.addrsStr, err)
continue
}
if !reflect.DeepEqual(addrs, test.exp) {
t.Errorf("Parse (list) of %q: got %+v, want %+v", test.addrsStr, addrs, test.exp)
}
}
}
func TestAddressString(t *testing.T) {
tests := []struct {
addr *Address
exp string
}{
{
&Address{Address: "bob@example.com"},
"<bob@example.com>",
},
{ // quoted local parts: RFC 5322, 3.4.1. and 3.2.4.
&Address{Address: `my@idiot@address@example.com`},
`<"my@idiot@address"@example.com>`,
},
{ // quoted local parts
&Address{Address: ` @example.com`},
`<" "@example.com>`,
},
{
&Address{Name: "Bob", Address: "bob@example.com"},
`"Bob" <bob@example.com>`,
},
{
// note the ö (o with an umlaut)
&Address{Name: "Böb", Address: "bob@example.com"},
`=?utf-8?q?B=C3=B6b?= <bob@example.com>`,
},
{
&Address{Name: "Bob Jane", Address: "bob@example.com"},
`"Bob Jane" <bob@example.com>`,
},
{
&Address{Name: "Böb Jacöb", Address: "bob@example.com"},
`=?utf-8?q?B=C3=B6b_Jac=C3=B6b?= <bob@example.com>`,
},
{ // https://golang.org/issue/12098
&Address{Name: "Rob", Address: ""},
`"Rob" <@>`,
},
{ // https://golang.org/issue/12098
&Address{Name: "Rob", Address: "@"},
`"Rob" <@>`,
},
{
&Address{Name: "Böb, Jacöb", Address: "bob@example.com"},
`=?utf-8?b?QsO2YiwgSmFjw7Zi?= <bob@example.com>`,
},
{
&Address{Name: "=??Q?x?=", Address: "hello@world.com"},
`"=??Q?x?=" <hello@world.com>`,
},
{
&Address{Name: "=?hello", Address: "hello@world.com"},
`"=?hello" <hello@world.com>`,
},
{
&Address{Name: "world?=", Address: "hello@world.com"},
`"world?=" <hello@world.com>`,
},
{
// should q-encode even for invalid utf-8.
&Address{Name: string([]byte{0xed, 0xa0, 0x80}), Address: "invalid-utf8@example.net"},
"=?utf-8?q?=ED=A0=80?= <invalid-utf8@example.net>",
},
// Domain-literal
{
&Address{Address: "bob@[192.168.0.1]"},
"<bob@[192.168.0.1]>",
},
{
&Address{Name: "Bob", Address: "bob@[192.168.0.1]"},
`"Bob" <bob@[192.168.0.1]>`,
},
}
for _, test := range tests {
s := test.addr.String()
if s != test.exp {
t.Errorf("Address%+v.String() = %v, want %v", *test.addr, s, test.exp)
continue
}
// Check round-trip.
if test.addr.Address != "" && test.addr.Address != "@" {
a, err := ParseAddress(test.exp)
if err != nil {
t.Errorf("ParseAddress(%#q): %v", test.exp, err)
continue
}
if a.Name != test.addr.Name || a.Address != test.addr.Address {
t.Errorf("ParseAddress(%#q) = %#v, want %#v", test.exp, a, test.addr)
}
}
}
}
// Check if all valid addresses can be parsed, formatted and parsed again
func TestAddressParsingAndFormatting(t *testing.T) {
// Should pass
tests := []string{
`<Bob@example.com>`,
`<bob.bob@example.com>`,
`<".bob"@example.com>`,
`<" "@example.com>`,
`<some.mail-with-dash@example.com>`,
`<"dot.and space"@example.com>`,
`<"very.unusual.@.unusual.com"@example.com>`,
`<admin@mailserver1>`,
`<postmaster@localhost>`,
"<#!$%&'*+-/=?^_`{}|~@example.org>",
`<"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com>`, // escaped quotes
`<"()<>[]:,;@\\\"!#$%&'*+-/=?^_{}| ~.a"@example.org>`, // escaped backslashes
`<"Abc\\@def"@example.com>`,
`<"Joe\\Blow"@example.com>`,
`<test1/test2=test3@example.com>`,
`<def!xyz%abc@example.com>`,
`<_somename@example.com>`,
`<joe@uk>`,
`<~@example.com>`,
`<"..."@test.com>`,
`<"john..doe"@example.com>`,
`<"john.doe."@example.com>`,
`<".john.doe"@example.com>`,
`<"."@example.com>`,
`<".."@example.com>`,
`<"0:"@0>`,
`<Bob@[192.168.0.1]>`,
}
for _, test := range tests {
addr, err := ParseAddress(test)
if err != nil {
t.Errorf("Couldn't parse address %s: %s", test, err.Error())
continue
}
str := addr.String()
addr, err = ParseAddress(str)
if err != nil {
t.Errorf("ParseAddr(%q) error: %v", test, err)
continue
}
if addr.String() != test {
t.Errorf("String() round-trip = %q; want %q", addr, test)
continue
}
}
// Should fail
badTests := []string{
`<Abc.example.com>`,
`<A@b@c@example.com>`,
`<a"b(c)d,e:f;g<h>i[j\k]l@example.com>`,
`<just"not"right@example.com>`,
`<this is"not\allowed@example.com>`,
`<this\ still\"not\\allowed@example.com>`,
`<john..doe@example.com>`,
`<john.doe@example..com>`,
`<john.doe@example..com>`,
`<john.doe.@example.com>`,
`<john.doe.@.example.com>`,
`<.john.doe@example.com>`,
`<@example.com>`,
`<.@example.com>`,
`<test@.>`,
`< @example.com>`,
`<""test""blah""@example.com>`,
`<""@0>`,
}
for _, test := range badTests {
_, err := ParseAddress(test)
if err == nil {
t.Errorf("Should have failed to parse address: %s", test)
continue
}
}
}
func TestAddressFormattingAndParsing(t *testing.T) {
tests := []*Address{
{Name: "@lïce", Address: "alice@example.com"},
{Name: "Böb O'Connor", Address: "bob@example.com"},
{Name: "???", Address: "bob@example.com"},
{Name: "Böb ???", Address: "bob@example.com"},
{Name: "Böb (Jacöb)", Address: "bob@example.com"},
{Name: "à#$%&'(),.:;<>@[]^`{|}~'", Address: "bob@example.com"},
// https://golang.org/issue/11292
{Name: "\"\\\x1f,\"", Address: "0@0"},
// https://golang.org/issue/12782
{Name: "naé, mée", Address: "test.mail@gmail.com"},
}
for i, test := range tests {
parsed, err := ParseAddress(test.String())
if err != nil {
t.Errorf("test #%d: ParseAddr(%q) error: %v", i, test.String(), err)
continue
}
if parsed.Name != test.Name {
t.Errorf("test #%d: Parsed name = %q; want %q", i, parsed.Name, test.Name)
}
if parsed.Address != test.Address {
t.Errorf("test #%d: Parsed address = %q; want %q", i, parsed.Address, test.Address)
}
}
}
func TestEmptyAddress(t *testing.T) {
parsed, err := ParseAddress("")
if parsed != nil || err == nil {
t.Errorf(`ParseAddress("") = %v, %v, want nil, error`, parsed, err)
}
list, err := ParseAddressList("")
if len(list) > 0 || err == nil {
t.Errorf(`ParseAddressList("") = %v, %v, want nil, error`, list, err)
}
list, err = ParseAddressList(",")
if len(list) > 0 || err == nil {
t.Errorf(`ParseAddressList("") = %v, %v, want nil, error`, list, err)
}
list, err = ParseAddressList("a@b c@d")
if len(list) > 0 || err == nil {
t.Errorf(`ParseAddressList("") = %v, %v, want nil, error`, list, err)
}
}