Notable divergences:
* Obsolete address formats are not parsed, including addresses with
embedded route information.
- * Group addresses are not parsed.
* The full range of spacing (the CFWS syntax element) is not supported,
such as breaking addresses across lines.
* No unicode normalization is performed.
var list []*Address
for {
p.skipSpace()
- addr, err := p.parseAddress()
+ addrs, err := p.parseAddress(true)
if err != nil {
return nil, err
}
- list = append(list, addr)
+ list = append(list, addrs...)
if !p.skipCfws() {
return nil, errors.New("mail: misformatted parenthetical comment")
}
func (p *addrParser) parseSingleAddress() (*Address, error) {
- addr, err := p.parseAddress()
+ addrs, err := p.parseAddress(true)
if err != nil {
return nil, err
}
if !p.empty() {
return nil, fmt.Errorf("mail: expected single address, got %q", p.s)
}
- return addr, nil
+ if len(addrs) == 0 {
+ return nil, errors.New("mail: empty group")
+ }
+ if len(addrs) > 1 {
+ return nil, errors.New("mail: group with multiple addresses")
+ }
+ return addrs[0], nil
}
// parseAddress parses a single RFC 5322 address at the start of p.
-func (p *addrParser) parseAddress() (addr *Address, err error) {
+func (p *addrParser) parseAddress(handleGroup bool) ([]*Address, error) {
debug.Printf("parseAddress: %q", p.s)
p.skipSpace()
if p.empty() {
return nil, errors.New("mail: no address")
}
- // address = name-addr / addr-spec
- // TODO(dsymonds): Support parsing group address.
+ // address = mailbox / group
+ // mailbox = name-addr / addr-spec
+ // group = display-name ":" [group-list] ";" [CFWS]
// addr-spec has a more restricted grammar than name-addr,
// so try parsing it first, and fallback to name-addr.
// TODO(dsymonds): Is this really correct?
spec, err := p.consumeAddrSpec()
if err == nil {
- return &Address{
+ return []*Address{{
Address: spec,
- }, err
+ }}, err
}
debug.Printf("parseAddress: not an addr-spec: %v", err)
debug.Printf("parseAddress: state is now %q", p.s)
}
debug.Printf("parseAddress: displayName=%q", displayName)
- // angle-addr = "<" addr-spec ">"
p.skipSpace()
+ if handleGroup {
+ if p.consume(':') {
+ return p.consumeGroupList()
+ }
+ }
+ // angle-addr = "<" addr-spec ">"
if !p.consume('<') {
return nil, errors.New("mail: no angle-addr")
}
}
debug.Printf("parseAddress: spec=%q", spec)
- return &Address{
+ return []*Address{{
Name: displayName,
Address: spec,
- }, nil
+ }}, nil
+}
+
+func (p *addrParser) consumeGroupList() ([]*Address, error) {
+ var group []*Address
+ // handle empty group.
+ p.skipSpace()
+ if p.consume(';') {
+ p.skipCfws()
+ return group, nil
+ }
+
+ for {
+ p.skipSpace()
+ // embedded groups not allowed.
+ addrs, err := p.parseAddress(false)
+ if err != nil {
+ return nil, err
+ }
+ group = append(group, addrs...)
+
+ if !p.skipCfws() {
+ return nil, errors.New("mail: misformatted parenthetical comment")
+ }
+ if p.consume(';') {
+ p.skipCfws()
+ break
+ }
+ if !p.consume(',') {
+ return nil, errors.New("mail: expected comma")
+ }
+ }
+ return group, nil
}
// consumeAddrSpec parses a single RFC 5322 addr-spec at the start of p.
// If dot is true, consumeAtom parses an RFC 5322 dot-atom instead.
// If permissive is true, consumeAtom will not fail on:
// - 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)
+// - 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) {
i := 0
// isAtext reports whether r is an RFC 5322 atext character.
// If dot is true, period is included.
// If permissive is true, RFC 5322 3.2.3 specials is included,
-// except '<', '>' and '"'.
+// except '<', '>', ':' and '"'.
func isAtext(r rune, dot, permissive bool) bool {
switch r {
case '.':
return dot
// RFC 5322 3.2.3. specials
- case '(', ')', '[', ']', ':', ';', '@', '\\', ',':
+ case '(', ')', '[', ']', ';', '@', '\\', ',':
return permissive
- case '<', '>', '"':
+ case '<', '>', '"', ':':
return false
}
return isVchar(r)
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"},
}
for i, tc := range mustErrTestCases {
}},
},
// RFC 5322, Appendix A.1.3
- // TODO(dsymonds): Group addresses.
-
+ {
+ `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",
+ },
+ },
+ },
+ {
+ `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>`,