mirror of
https://github.com/golang/go
synced 2024-11-18 09:14:43 -07:00
net/mail: enhanced Address.String and ParseAddress to match RFC 5322
Updated Address.String so it restores quoted local parts, which wasn't done before. When parsing `<" "@example.com>`, the formatted string returned `< @example>`, which doens't match RFC 5322, since a space is not atext. Another example is `<"bob@valid"@example.com>` which returned `<bob@valid@example.com>`, which is completely invalid. I also added support for quotes and backslashes in a quoted local part. Besides formatting a parsed Address, the ParseAddress function also needed more testing and finetuning for special cases. Things like `<.john.doe@example.com>` and `<john..doe@example.com>` e.a. were accepted, but are invalid. I fixed those details and add tests for some other special cases. Fixes #10768 Change-Id: Ib0caf8ad603eb21e32fcb957a5f1a0fe5d1c6e6e Reviewed-on: https://go-review.googlesource.com/8724 Reviewed-by: Russ Cox <rsc@golang.org>
This commit is contained in:
parent
5201bf7ad1
commit
daaa45073e
@ -168,10 +168,42 @@ func (p *AddressParser) ParseList(list string) ([]*Address, error) {
|
|||||||
// If the address's name contains non-ASCII characters
|
// If the address's name contains non-ASCII characters
|
||||||
// the name will be rendered according to RFC 2047.
|
// the name will be rendered according to RFC 2047.
|
||||||
func (a *Address) String() string {
|
func (a *Address) String() string {
|
||||||
s := "<" + a.Address + ">"
|
|
||||||
|
// Format address local@domain
|
||||||
|
at := strings.LastIndex(a.Address, "@")
|
||||||
|
local, domain := a.Address[:at], a.Address[at+1:]
|
||||||
|
|
||||||
|
// Add quotes if needed
|
||||||
|
// TODO: rendering quoted local part and rendering printable name
|
||||||
|
// should be merged in helper function.
|
||||||
|
quoteLocal := false
|
||||||
|
for i := 0; i < len(local); i++ {
|
||||||
|
ch := local[i]
|
||||||
|
if isAtext(ch, false) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '.' {
|
||||||
|
// Dots are okay if they are surrounded by atext.
|
||||||
|
// We only need to check that the previous byte is
|
||||||
|
// not a dot, and this isn't the end of the string.
|
||||||
|
if i > 0 && local[i-1] != '.' && i < len(local)-1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
quoteLocal = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if quoteLocal {
|
||||||
|
local = quoteString(local)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
s := "<" + local + "@" + domain + ">"
|
||||||
|
|
||||||
if a.Name == "" {
|
if a.Name == "" {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// If every character is printable ASCII, quoting is simple.
|
// If every character is printable ASCII, quoting is simple.
|
||||||
allPrintable := true
|
allPrintable := true
|
||||||
for i := 0; i < len(a.Name); i++ {
|
for i := 0; i < len(a.Name); i++ {
|
||||||
@ -301,7 +333,7 @@ func (p *addrParser) consumeAddrSpec() (spec string, err error) {
|
|||||||
} else {
|
} else {
|
||||||
// dot-atom
|
// dot-atom
|
||||||
debug.Printf("consumeAddrSpec: parsing dot-atom")
|
debug.Printf("consumeAddrSpec: parsing dot-atom")
|
||||||
localPart, err = p.consumeAtom(true)
|
localPart, err = p.consumeAtom(true, false)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debug.Printf("consumeAddrSpec: failed: %v", err)
|
debug.Printf("consumeAddrSpec: failed: %v", err)
|
||||||
@ -319,7 +351,7 @@ func (p *addrParser) consumeAddrSpec() (spec string, err error) {
|
|||||||
return "", errors.New("mail: no domain in addr-spec")
|
return "", errors.New("mail: no domain in addr-spec")
|
||||||
}
|
}
|
||||||
// TODO(dsymonds): Handle domain-literal
|
// TODO(dsymonds): Handle domain-literal
|
||||||
domain, err = p.consumeAtom(true)
|
domain, err = p.consumeAtom(true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -346,7 +378,7 @@ func (p *addrParser) consumePhrase() (phrase string, err error) {
|
|||||||
// atom
|
// atom
|
||||||
// We actually parse dot-atom here to be more permissive
|
// We actually parse dot-atom here to be more permissive
|
||||||
// than what RFC 5322 specifies.
|
// than what RFC 5322 specifies.
|
||||||
word, err = p.consumeAtom(true)
|
word, err = p.consumeAtom(true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -402,7 +434,9 @@ Loop:
|
|||||||
|
|
||||||
// consumeAtom parses an RFC 5322 atom at the start of p.
|
// consumeAtom parses an RFC 5322 atom at the start of p.
|
||||||
// If dot is true, consumeAtom parses an RFC 5322 dot-atom instead.
|
// If dot is true, consumeAtom parses an RFC 5322 dot-atom instead.
|
||||||
func (p *addrParser) consumeAtom(dot bool) (atom string, err error) {
|
// If permissive is true, consumeAtom will not fail on
|
||||||
|
// leading/trailing/double dots in the atom (see golang.org/issue/4938).
|
||||||
|
func (p *addrParser) consumeAtom(dot bool, permissive bool) (atom string, err error) {
|
||||||
if !isAtext(p.peek(), false) {
|
if !isAtext(p.peek(), false) {
|
||||||
return "", errors.New("mail: invalid string")
|
return "", errors.New("mail: invalid string")
|
||||||
}
|
}
|
||||||
@ -410,6 +444,17 @@ func (p *addrParser) consumeAtom(dot bool) (atom string, err error) {
|
|||||||
for ; i < p.len() && isAtext(p.s[i], dot); i++ {
|
for ; i < p.len() && isAtext(p.s[i], dot); i++ {
|
||||||
}
|
}
|
||||||
atom, p.s = string(p.s[:i]), p.s[i:]
|
atom, p.s = string(p.s[:i]), p.s[i:]
|
||||||
|
if !permissive {
|
||||||
|
if strings.HasPrefix(atom, ".") {
|
||||||
|
return "", errors.New("mail: leading dot in atom")
|
||||||
|
}
|
||||||
|
if strings.Contains(atom, "..") {
|
||||||
|
return "", errors.New("mail: double dot in atom")
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(atom, ".") {
|
||||||
|
return "", errors.New("mail: trailing dot in atom")
|
||||||
|
}
|
||||||
|
}
|
||||||
return atom, nil
|
return atom, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,6 +536,23 @@ func isQtext(c byte) bool {
|
|||||||
return '!' <= c && c <= '~'
|
return '!' <= c && c <= '~'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// quoteString renders a string as a RFC5322 quoted-string.
|
||||||
|
func quoteString(s string) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteByte('"')
|
||||||
|
for _, c := range s {
|
||||||
|
ch := byte(c)
|
||||||
|
if isQtext(ch) || isWSP(ch) {
|
||||||
|
buf.WriteByte(ch)
|
||||||
|
} else if isVchar(ch) {
|
||||||
|
buf.WriteByte('\\')
|
||||||
|
buf.WriteByte(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteByte('"')
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
// isVchar reports whether c is an RFC 5322 VCHAR character.
|
// isVchar reports whether c is an RFC 5322 VCHAR character.
|
||||||
func isVchar(c byte) bool {
|
func isVchar(c byte) bool {
|
||||||
// Visible (printing) characters.
|
// Visible (printing) characters.
|
||||||
|
@ -458,6 +458,14 @@ func TestAddressFormatting(t *testing.T) {
|
|||||||
&Address{Address: "bob@example.com"},
|
&Address{Address: "bob@example.com"},
|
||||||
"<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"},
|
&Address{Name: "Bob", Address: "bob@example.com"},
|
||||||
`"Bob" <bob@example.com>`,
|
`"Bob" <bob@example.com>`,
|
||||||
@ -483,3 +491,87 @@ func TestAddressFormatting(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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>`,
|
||||||
|
}
|
||||||
|
|
||||||
|
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>`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range badTests {
|
||||||
|
_, err := ParseAddress(test)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Should have failed to parse address: %s", test)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user