[release-branch.go1.21] net/mail: properly handle special characters in phrase and obs-phrase

Fixes a couple of misalignments with RFC 5322 which introduce
significant diffs between (mostly) conformant parsers.

This change reverts the changes made in CL50911, which allowed certain
special RFC 5322 characters to appear unquoted in the "phrase" syntax.
It is unclear why this change was made in the first place, and created
a divergence from comformant parsers. In particular this resulted in
treating comments in display names incorrectly.

Additionally properly handle trailing malformed comments in the group
syntax.

For #65083
Fixes #65848

Change-Id: I00dddc044c6ae3381154e43236632604c390f672
Reviewed-on: https://go-review.googlesource.com/c/go/+/555596
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/566195
Reviewed-by: Carlos Amedee <carlos@golang.org>
This commit is contained in:
Roland Shoemaker 2024-01-10 11:02:14 -08:00 committed by Carlos Amedee
parent 99e44c71f6
commit 263c059b09
2 changed files with 50 additions and 28 deletions

View File

@ -280,7 +280,7 @@ func (a *Address) String() string {
// Add quotes if needed // Add quotes if needed
quoteLocal := false quoteLocal := false
for i, r := range local { for i, r := range local {
if isAtext(r, false, false) { if isAtext(r, false) {
continue continue
} }
if r == '.' { if r == '.' {
@ -444,7 +444,7 @@ func (p *addrParser) parseAddress(handleGroup bool) ([]*Address, error) {
if !p.consume('<') { if !p.consume('<') {
atext := true atext := true
for _, r := range displayName { for _, r := range displayName {
if !isAtext(r, true, false) { if !isAtext(r, true) {
atext = false atext = false
break break
} }
@ -479,7 +479,9 @@ func (p *addrParser) consumeGroupList() ([]*Address, error) {
// handle empty group. // handle empty group.
p.skipSpace() p.skipSpace()
if p.consume(';') { if p.consume(';') {
p.skipCFWS() if !p.skipCFWS() {
return nil, errors.New("mail: misformatted parenthetical comment")
}
return group, nil return group, nil
} }
@ -496,7 +498,9 @@ func (p *addrParser) consumeGroupList() ([]*Address, error) {
return nil, errors.New("mail: misformatted parenthetical comment") return nil, errors.New("mail: misformatted parenthetical comment")
} }
if p.consume(';') { if p.consume(';') {
p.skipCFWS() if !p.skipCFWS() {
return nil, errors.New("mail: misformatted parenthetical comment")
}
break break
} }
if !p.consume(',') { if !p.consume(',') {
@ -566,6 +570,12 @@ func (p *addrParser) consumePhrase() (phrase string, err error) {
var words []string var words []string
var isPrevEncoded bool var isPrevEncoded bool
for { for {
// obs-phrase allows CFWS after one word
if len(words) > 0 {
if !p.skipCFWS() {
return "", errors.New("mail: misformatted parenthetical comment")
}
}
// word = atom / quoted-string // word = atom / quoted-string
var word string var word string
p.skipSpace() p.skipSpace()
@ -661,7 +671,6 @@ Loop:
// If dot is true, consumeAtom parses an RFC 5322 dot-atom instead. // If dot is true, consumeAtom parses an RFC 5322 dot-atom instead.
// If permissive is true, consumeAtom will not fail on: // If permissive is true, consumeAtom will not fail on:
// - leading/trailing/double dots in the atom (see golang.org/issue/4938) // - leading/trailing/double dots in the atom (see golang.org/issue/4938)
// - special characters (RFC 5322 3.2.3) except '<', '>', ':' and '"' (see golang.org/issue/21018)
func (p *addrParser) consumeAtom(dot bool, permissive bool) (atom string, err error) { func (p *addrParser) consumeAtom(dot bool, permissive bool) (atom string, err error) {
i := 0 i := 0
@ -672,7 +681,7 @@ Loop:
case size == 1 && r == utf8.RuneError: case size == 1 && r == utf8.RuneError:
return "", fmt.Errorf("mail: invalid utf-8 in address: %q", p.s) return "", fmt.Errorf("mail: invalid utf-8 in address: %q", p.s)
case size == 0 || !isAtext(r, dot, permissive): case size == 0 || !isAtext(r, dot):
break Loop break Loop
default: default:
@ -850,18 +859,13 @@ func (e charsetError) Error() string {
// isAtext reports whether r is an RFC 5322 atext character. // isAtext reports whether r is an RFC 5322 atext character.
// If dot is true, period is included. // If dot is true, period is included.
// If permissive is true, RFC 5322 3.2.3 specials is included, func isAtext(r rune, dot bool) bool {
// except '<', '>', ':' and '"'.
func isAtext(r rune, dot, permissive bool) bool {
switch r { switch r {
case '.': case '.':
return dot return dot
// RFC 5322 3.2.3. specials // RFC 5322 3.2.3. specials
case '(', ')', '[', ']', ';', '@', '\\', ',': case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '"': // RFC 5322 3.2.3. specials
return permissive
case '<', '>', '"', ':':
return false return false
} }
return isVchar(r) return isVchar(r)

View File

@ -385,8 +385,11 @@ func TestAddressParsingError(t *testing.T) {
13: {"group not closed: null@example.com", "expected comma"}, 13: {"group not closed: null@example.com", "expected comma"},
14: {"group: first@example.com, second@example.com;", "group with multiple addresses"}, 14: {"group: first@example.com, second@example.com;", "group with multiple addresses"},
15: {"john.doe", "missing '@' or angle-addr"}, 15: {"john.doe", "missing '@' or angle-addr"},
16: {"john.doe@", "no angle-addr"}, 16: {"john.doe@", "missing '@' or angle-addr"},
17: {"John Doe@foo.bar", "no 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:"},
} }
for i, tc := range mustErrTestCases { for i, tc := range mustErrTestCases {
@ -436,6 +439,15 @@ func TestAddressParsing(t *testing.T) {
Address: "john.q.public@example.com", 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>`, `"John (middle) Doe" <jdoe@machine.example>`,
[]*Address{{ []*Address{{
@ -443,20 +455,6 @@ func TestAddressParsing(t *testing.T) {
Address: "jdoe@machine.example", Address: "jdoe@machine.example",
}}, }},
}, },
{
`John (middle) Doe <jdoe@machine.example>`,
[]*Address{{
Name: "John (middle) Doe",
Address: "jdoe@machine.example",
}},
},
{
`John !@M@! Doe <jdoe@machine.example>`,
[]*Address{{
Name: "John !@M@! Doe",
Address: "jdoe@machine.example",
}},
},
{ {
`"John <middle> Doe" <jdoe@machine.example>`, `"John <middle> Doe" <jdoe@machine.example>`,
[]*Address{{ []*Address{{
@ -788,6 +786,26 @@ func TestAddressParsing(t *testing.T) {
}, },
}, },
}, },
// 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`,
},
},
},
} }
for _, test := range tests { for _, test := range tests {
if len(test.exp) == 1 { if len(test.exp) == 1 {