aboutsummaryrefslogtreecommitdiff
path: root/lib/email
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2023-04-08 16:55:12 +0700
committerShulhan <ms@kilabit.info>2023-04-08 16:56:26 +0700
commit2d669a4d7f33c2af91ba2aba1ce72553ce0ece6a (patch)
tree74e8ddfe81f88f9fffafdd1bdfb4edff5f542072 /lib/email
parentbba662858aded3c09a5dd62cb3e1ef14e0316c54 (diff)
downloadpakakeh.go-2d669a4d7f33c2af91ba2aba1ce72553ce0ece6a.tar.xz
lib/email: refactoring ParseMailboxes using lib/bytes#Parser
The libio.Reader will be deprecated and replaced with libbytes.Parser in the future.
Diffstat (limited to 'lib/email')
-rw-r--r--lib/email/field_test.go2
-rw-r--r--lib/email/mailbox.go384
-rw-r--r--lib/email/mailbox_test.go359
-rw-r--r--lib/email/message_test.go10
4 files changed, 422 insertions, 333 deletions
diff --git a/lib/email/field_test.go b/lib/email/field_test.go
index a6885131..0b8d2280 100644
--- a/lib/email/field_test.go
+++ b/lib/email/field_test.go
@@ -273,7 +273,7 @@ func TestUnpackMailbox(t *testing.T) {
in []byte
}{{
in: []byte("Sender: local\r\n"),
- expErr: `ParseMailboxes "local": empty or invalid address`,
+ expErr: `ParseMailboxes: empty or invalid address`,
}, {
in: []byte("Sender: test@one, test@two\r\n"),
expErr: "multiple address in sender: 'test@one, test@two\r\n'",
diff --git a/lib/email/mailbox.go b/lib/email/mailbox.go
index b51c9bcb..3460c71d 100644
--- a/lib/email/mailbox.go
+++ b/lib/email/mailbox.go
@@ -10,20 +10,11 @@ import (
"fmt"
"strings"
- libio "github.com/shuLhan/share/lib/io"
+ libbytes "github.com/shuLhan/share/lib/bytes"
libjson "github.com/shuLhan/share/lib/json"
libnet "github.com/shuLhan/share/lib/net"
)
-const (
- stateBegin = 1 << iota // 1
- stateDisplayName // 2
- stateLocalPart // 4
- stateDomain // 8
- stateEnd // 16
- stateGroupEnd // 32
-)
-
// Mailbox represent an invidual mailbox.
type Mailbox struct {
Address string // address contains the combination of "local@domain"
@@ -62,12 +53,12 @@ func ParseMailbox(raw []byte) (mbox *Mailbox) {
return nil
}
if len(mboxes) > 0 {
- return mboxes[0]
+ mbox = mboxes[0]
}
- return nil
+ return mbox
}
-// ParseMailboxes parse raw address into single or multiple mailboxes.
+// ParseMailboxes parse raw [address] into single or multiple mailboxes.
// Raw address can be a group of address, list of mailbox, or single mailbox.
//
// A group of address have the following syntax,
@@ -91,184 +82,206 @@ func ParseMailbox(raw []byte) (mbox *Mailbox) {
// A comment have the following syntax,
//
// "(" text [comment] ")"
+//
+// [address]: https://www.rfc-editor.org/rfc/rfc5322.html#section-3.4
func ParseMailboxes(raw []byte) (mboxes []*Mailbox, err error) {
+ var logp = `ParseMailboxes`
+
raw = bytes.TrimSpace(raw)
if len(raw) == 0 {
- return nil, fmt.Errorf("ParseMailboxes %q: empty address", raw)
+ return nil, fmt.Errorf(`%s: empty address`, logp)
}
- r := &libio.Reader{}
- r.Init(raw)
-
var (
- seps = []byte{'(', ':', '<', '@', '>', ',', ';'}
- tok []byte
- value []byte
+ delims = []byte{'(', ':', '<', '@', '>', ',', ';'}
+ parser = libbytes.NewParser(raw, delims)
+
+ token []byte
isGroup bool
c byte
mbox *Mailbox
- state = stateBegin
)
- _ = r.SkipSpaces()
- tok, _, c = r.ReadUntil(seps, nil)
- for {
- switch c {
- case '(':
- _, err = skipComment(r)
- if err != nil {
- return nil, fmt.Errorf("ParseMailboxes %q: %w", raw, err)
- }
- if len(tok) > 0 {
- value = append(value, tok...)
- }
+ token, c, err = parseMailboxText(parser)
+ if err != nil {
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
+ }
+ if c == 0 || c == '>' || c == ',' || c == ';' {
+ return nil, fmt.Errorf(`%s: empty or invalid address`, logp)
+ }
- case ':':
- if state != stateBegin {
- return nil, fmt.Errorf("ParseMailboxes %q: invalid character: ':'", raw)
- }
- isGroup = true
- value = nil
- state = stateDisplayName
- _ = r.SkipSpaces()
+ token = bytes.TrimSpace(token)
+ mbox = &Mailbox{}
+ if c == ':' {
+ // We are parsing group of mailbox.
+ isGroup = true
+ } else if c == '<' {
+ mbox.isAngle = true
+ mbox.Name = token
+ } else if c == '@' {
+ if len(token) == 0 {
+ return nil, fmt.Errorf(`%s: empty local`, logp)
+ }
+ if !IsValidLocal(token) {
+ return nil, fmt.Errorf(`%s: invalid local '%s'`, logp, token)
+ }
+ mbox.Local = token
+ }
- case '<':
- if state >= stateLocalPart {
- return nil, fmt.Errorf("ParseMailboxes %q: invalid character: '<'", raw)
- }
- value = append(value, tok...)
- value = bytes.TrimSpace(value)
- mbox = &Mailbox{
- isAngle: true,
- }
- if len(value) > 0 {
- mbox.Name = value
- }
- value = nil
- state = stateLocalPart
+ c, err = parseMailbox(mbox, parser, c)
+ if err != nil {
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
+ }
+ mboxes = append(mboxes, mbox)
- case '@':
- if state >= stateDomain {
- return nil, fmt.Errorf("ParseMailboxes %q: invalid character: '@'", raw)
- }
- value = append(value, tok...)
- value = bytes.TrimSpace(value)
+ for c == ',' {
+ mbox = &Mailbox{}
+ c, err = parseMailbox(mbox, parser, c)
+ if err != nil {
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
+ }
+ mboxes = append(mboxes, mbox)
+ }
+
+ if isGroup {
+ if c != ';' {
+ return nil, fmt.Errorf(`%s: missing group terminator ';'`, logp)
+ }
+ } else {
+ if c != 0 {
+ return nil, fmt.Errorf(`%s: invalid character '%c'`, logp, c)
+ }
+ }
+
+ return mboxes, nil
+}
+
+// parseMailbox continue parsing single mailbox based on previous delimiter
+// prevd.
+// On success it will return the last delimiter that is not for mailbox,
+// either ',' for list of mailbox or ';' for end of group.
+func parseMailbox(mbox *Mailbox, parser *libbytes.Parser, prevd byte) (c byte, err error) {
+ var (
+ logp = `parseMailbox`
+
+ value []byte
+ )
+
+ c = prevd
+ if c == ':' || c == ',' {
+ // Get the name or local part.
+ value, c, err = parseMailboxText(parser)
+ if err != nil {
+ return c, fmt.Errorf(`%s: %w`, logp, err)
+ }
+
+ if c == '<' {
+ mbox.isAngle = true
+ mbox.Name = value
+ } else if c == '@' {
if len(value) == 0 {
- return nil, fmt.Errorf("ParseMailboxes %q: empty local", raw)
- }
- if mbox == nil {
- mbox = &Mailbox{}
+ return c, fmt.Errorf(`%s: empty local`, logp)
}
if !IsValidLocal(value) {
- return nil, fmt.Errorf("ParseMailboxes %q: invalid local: '%s'", raw, value)
+ return c, fmt.Errorf(`%s: invalid local '%s'`, logp, value)
}
mbox.Local = value
- value = nil
- state = stateDomain
+ }
+ }
+ if c == '<' {
+ // Get the local or domain part.
+ value, c, err = parseMailboxText(parser)
+ if err != nil {
+ return c, fmt.Errorf(`%s: %w`, logp, err)
+ }
+ if c == '>' {
+ // No local part, only domain part, "<domain>".
+ goto domain
+ }
+ if c != '@' {
+ return c, fmt.Errorf(`%s: invalid character '%c'`, logp, c)
+ }
+ if !IsValidLocal(value) {
+ return c, fmt.Errorf(`%s: invalid local '%s'`, logp, value)
+ }
+ mbox.Local = value
+ }
- case '>':
- if state > stateDomain || !mbox.isAngle {
- return nil, fmt.Errorf("ParseMailboxes %q: invalid character: '>'", raw)
- }
- value = append(value, tok...)
- value = bytes.TrimSpace(value)
- if state == stateDomain {
- if !libnet.IsHostnameValid(value, false) {
- return nil, fmt.Errorf("ParseMailboxes %q: invalid domain: '%s'", raw, value)
- }
- }
- mbox.Domain = value
- mbox.Address = fmt.Sprintf("%s@%s", mbox.Local, mbox.Domain)
- mboxes = append(mboxes, mbox)
- mbox = nil
- value = nil
- state = stateEnd
+ // Get the domain part.
+ value, c, err = parseMailboxText(parser)
+ if err != nil {
+ return c, fmt.Errorf(`%s: %w`, logp, err)
+ }
+domain:
+ if len(value) != 0 {
+ if !libnet.IsHostnameValid(value, false) {
+ return c, fmt.Errorf(`%s: invalid domain '%s'`, logp, value)
+ }
+ }
+ if len(mbox.Local) != 0 && len(value) == 0 {
+ return c, fmt.Errorf(`%s: invalid domain '%s'`, logp, value)
+ }
+ mbox.Domain = value
+ mbox.Address = fmt.Sprintf(`%s@%s`, mbox.Local, mbox.Domain)
- case ';':
- if state < stateDomain || !isGroup {
- return nil, fmt.Errorf("ParseMailboxes %q: invalid character: ';'", raw)
- }
- if mbox != nil && mbox.isAngle {
- return nil, fmt.Errorf("ParseMailboxes %q: missing '>'", raw)
- }
- value = append(value, tok...)
- value = bytes.TrimSpace(value)
- switch state {
- case stateDomain:
- if !libnet.IsHostnameValid(value, false) {
- return nil, fmt.Errorf("ParseMailboxes %q: invalid domain: '%s'", raw, value)
- }
- mbox.Domain = value
- mbox.Address = fmt.Sprintf("%s@%s", mbox.Local, mbox.Domain)
- mboxes = append(mboxes, mbox)
- mbox = nil
- case stateEnd:
- if len(value) > 0 {
- return nil, fmt.Errorf("ParseMailboxes %q: invalid token: '%s'", raw, value)
- }
- }
- isGroup = false
- value = nil
- state = stateGroupEnd
- case ',':
- if state < stateDomain {
- return nil, fmt.Errorf("ParseMailboxes %q: invalid character: ','", raw)
- }
- if mbox != nil && mbox.isAngle {
- return nil, fmt.Errorf("ParseMailboxes %q: missing '>'", raw)
- }
- value = append(value, tok...)
- value = bytes.TrimSpace(value)
- switch state {
- case stateDomain:
- if !libnet.IsHostnameValid(value, false) {
- return nil, fmt.Errorf("ParseMailboxes %q: invalid domain: '%s'", raw, value)
- }
- mbox.Domain = value
- mbox.Address = fmt.Sprintf("%s@%s", mbox.Local, mbox.Domain)
- mboxes = append(mboxes, mbox)
- mbox = nil
- case stateEnd:
- if len(value) > 0 {
- return nil, fmt.Errorf("ParseMailboxes %q: invalid token: '%s'", raw, value)
- }
- }
- value = nil
- state = stateBegin
- case 0:
- if state < stateDomain {
- return nil, fmt.Errorf("ParseMailboxes %q: empty or invalid address", raw)
- }
- if state != stateEnd && mbox != nil && mbox.isAngle {
- return nil, fmt.Errorf("ParseMailboxes %q: missing '>'", raw)
- }
- if isGroup {
- return nil, fmt.Errorf("ParseMailboxes %q: missing ';'", raw)
- }
+ if mbox.isAngle {
+ if c != '>' {
+ return c, fmt.Errorf(`%s: missing '>'`, logp)
+ }
+ value, c, err = parseMailboxText(parser)
+ if err != nil {
+ return c, fmt.Errorf(`%s: %w`, logp, err)
+ }
+ if len(value) != 0 {
+ return c, fmt.Errorf(`%s: unknown token '%s'`, logp, value)
+ }
+ }
- value = append(value, tok...)
- value = bytes.TrimSpace(value)
- if state == stateGroupEnd {
- if len(value) > 0 {
- return nil, fmt.Errorf("ParseMailboxes %q: trailing text: '%s'", raw, value)
- }
- }
+ if c == ',' || c == ';' || c == 0 {
+ return c, nil
+ }
+
+ return c, fmt.Errorf(`%s: invalid character '%c'`, logp, c)
+}
+
+// parseMailboxText parse text (display-name, local-part, or domain) probably
+// with comment inside.
+func parseMailboxText(parser *libbytes.Parser) (text []byte, c byte, err error) {
+ var (
+ logp = `parseMailboxText`
+ delims = []byte{'\\', ')'}
+
+ token []byte
+ )
+
+ parser.AddDelimiters(delims)
+ defer parser.RemoveDelimiters(delims)
- if state == stateDomain {
- if !libnet.IsHostnameValid(value, false) {
- return nil, fmt.Errorf("ParseMailboxes %q: invalid domain: '%s'", raw, value)
- }
- mbox.Domain = value
- mbox.Address = fmt.Sprintf("%s@%s", mbox.Local, mbox.Domain)
- mboxes = append(mboxes, mbox)
- mbox = nil
+ token, c = parser.ReadNoSpace()
+ for {
+ text = append(text, token...)
+
+ if c == ')' {
+ return nil, c, fmt.Errorf(`%s: invalid character '%c'`, logp, c)
+ }
+ if c == '\\' {
+ token, _ = parser.ReadN(1)
+ text = append(text, token...)
+ token, c = parser.ReadNoSpace()
+ continue
+ }
+ if c == '(' {
+ err = skipComment(parser)
+ if err != nil {
+ return nil, 0, fmt.Errorf(`%s: %w`, logp, err)
}
- goto out
+ token, c = parser.ReadNoSpace()
+ continue
}
- tok, _, c = r.ReadUntil(seps, nil)
+ break
}
-out:
- return mboxes, nil
+ text = bytes.TrimSpace(text)
+ return text, c, nil
}
// skipComment skip all characters inside parentheses, '(' and ')'.
@@ -278,33 +291,30 @@ out:
// "( a \) comment)".
//
// A comment can be nested, for example "(a (comment))"
-func skipComment(r *libio.Reader) (c byte, err error) {
- seps := []byte{'\\', '(', ')'}
- c = r.SkipUntil(seps)
+func skipComment(parser *libbytes.Parser) (err error) {
+ var c = parser.Skip()
for {
- switch c {
- case 0:
- return c, errors.New("missing comment close parentheses")
- case '\\':
- // We found backslash, skip one character and continue
- // looking for separator.
- r.SkipN(1)
- case '(':
- c, err = skipComment(r)
+ if c == 0 {
+ return errors.New(`missing comment close parentheses`)
+ }
+ if c == ')' {
+ break
+ }
+ if c == '(' {
+ err = skipComment(parser)
if err != nil {
- return c, err
+ return err
}
- case ')':
- c = r.SkipSpaces()
- if c != '(' {
- goto out
- }
- r.SkipN(1)
+ c = parser.Skip()
+ continue
}
- c = r.SkipUntil(seps)
+ // c == '\\'
+ // We found backslash, skip one character and continue
+ // looking for next delimiter.
+ parser.SkipN(1)
+ c = parser.Skip()
}
-out:
- return c, nil
+ return nil
}
func (mbox *Mailbox) UnmarshalJSON(b []byte) (err error) {
diff --git a/lib/email/mailbox_test.go b/lib/email/mailbox_test.go
index 9915e235..4b855190 100644
--- a/lib/email/mailbox_test.go
+++ b/lib/email/mailbox_test.go
@@ -9,238 +9,317 @@ import (
"fmt"
"testing"
- libio "github.com/shuLhan/share/lib/io"
+ libbytes "github.com/shuLhan/share/lib/bytes"
"github.com/shuLhan/share/lib/test"
)
+func TestParseMailbox(t *testing.T) {
+ type testCase struct {
+ in string
+ exp *Mailbox
+ }
+
+ var cases = []testCase{{
+ in: `(empty)`,
+ exp: nil,
+ }, {
+ in: `one@example`,
+ exp: &Mailbox{
+ Local: []byte(`one`),
+ Domain: []byte(`example`),
+ Address: `one@example`,
+ },
+ }, {
+ in: `one@example , two@example`,
+ exp: &Mailbox{
+ Local: []byte(`one`),
+ Domain: []byte(`example`),
+ Address: `one@example`,
+ },
+ }}
+
+ var (
+ c testCase
+ got *Mailbox
+ )
+ for _, c = range cases {
+ got = ParseMailbox([]byte(c.in))
+ test.Assert(t, c.in, c.exp, got)
+ }
+}
+
func TestParseMailboxes(t *testing.T) {
- cases := []struct {
+ type testCase struct {
desc string
in string
expErr string
exp string
- }{{
- desc: "With empty input",
- expErr: "ParseMailboxes %q: empty address",
+ }
+ var cases = []testCase{{
+ desc: `With empty input`,
+ expErr: `ParseMailboxes: empty address`,
}, {
- desc: "With comment only",
- in: "(comment)",
- expErr: "ParseMailboxes %q: empty or invalid address",
+ desc: `With comment only`,
+ in: `(comment)`,
+ expErr: `ParseMailboxes: empty or invalid address`,
}, {
- desc: "With no domain",
- in: "(comment)local(comment)",
- expErr: "ParseMailboxes %q: empty or invalid address",
+ desc: `With no domain`,
+ in: `(comment)local(comment)`,
+ expErr: `ParseMailboxes: empty or invalid address`,
}, {
- desc: "With no opening comment",
- in: "comment)local@domain",
- expErr: "ParseMailboxes %q: invalid local: 'comment)local'",
+ desc: `With no opening comment`,
+ in: `comment)local@domain`,
+ expErr: `ParseMailboxes: parseMailboxText: invalid character ')'`,
}, {
- desc: "With no closing comment",
- in: "(commentlocal@domain",
- expErr: "ParseMailboxes %q: missing comment close parentheses",
+ desc: `With no closing comment`,
+ in: `(commentlocal@domain`,
+ expErr: `ParseMailboxes: parseMailboxText: missing comment close parentheses`,
}, {
- desc: "With no opening bracket",
- in: "(comment)local(comment)@domain>",
- expErr: "ParseMailboxes %q: invalid character: '>'",
+ desc: `With no opening bracket`,
+ in: `(comment)local(comment)@domain>`,
+ expErr: `ParseMailboxes: parseMailbox: invalid character '>'`,
}, {
- desc: "With no closing bracket",
- in: "<(comment)local(comment)@domain",
- expErr: "ParseMailboxes %q: missing '>'",
+ desc: `With no closing bracket`,
+ in: `<(comment)local(comment)@domain`,
+ expErr: `ParseMailboxes: parseMailbox: missing '>'`,
}, {
- desc: "With ':' inside mailbox",
- in: "<local:part@domain>",
- expErr: "ParseMailboxes %q: invalid character: ':'",
+ desc: `With ':' inside mailbox`,
+ in: `<local:part@domain>`,
+ expErr: `ParseMailboxes: parseMailbox: invalid character ':'`,
}, {
- desc: "With '<' inside local part",
- in: "local<part@domain>",
- exp: "[local <part@domain>]",
+ desc: `With '<' inside local part`,
+ in: `local<part@domain>`,
+ exp: `[local <part@domain>]`,
}, {
- desc: "With multiple '<'",
- in: "Name <local<part@domain>",
- expErr: "ParseMailboxes %q: invalid character: '<'",
+ desc: `With multiple '<'`,
+ in: `Name <local<part@domain>`,
+ expErr: `ParseMailboxes: parseMailbox: invalid character '<'`,
}, {
- desc: "With multiple '@'",
- in: "Name <local@part@domain>",
- expErr: "ParseMailboxes %q: invalid character: '@'",
+ desc: `With multiple '@'`,
+ in: `Name <local@part@domain>`,
+ expErr: `ParseMailboxes: parseMailbox: missing '>'`,
}, {
- desc: "With no domain",
- in: "Name <local>",
- exp: "[Name <@local>]",
+ desc: `With no local-part`,
+ in: `Name <local>`,
+ exp: `[Name <@local>]`,
}, {
- desc: "With empty local",
- in: "Name <@domain>",
- expErr: "ParseMailboxes %q: empty local",
+ desc: `With empty local`,
+ in: `@domain`,
+ expErr: `ParseMailboxes: empty local`,
}, {
- desc: "With empty domain",
- in: "Name <local@>, test@domain",
- expErr: "ParseMailboxes %q: invalid domain: ''",
+ desc: `With empty local`,
+ in: `Name <@domain>`,
+ expErr: `ParseMailboxes: parseMailbox: invalid local ''`,
}, {
- desc: "With invalid domain",
- in: "Name <local@dom[ain>, test@domain",
- expErr: "ParseMailboxes %q: invalid domain: 'dom[ain'",
+ desc: `With invalid local`,
+ in: `e[ample@domain`,
+ expErr: `ParseMailboxes: invalid local 'e[ample'`,
}, {
- desc: "With no bracket, single address",
- in: "local@domain",
- exp: "[<local@domain>]",
+ desc: `With empty domain`,
+ in: `Name <local@>, test@domain`,
+ expErr: `ParseMailboxes: parseMailbox: invalid domain ''`,
}, {
- desc: "With no bracket, comments between single address",
- in: "(comment)local(comment)@domain",
- exp: "[<local@domain>]",
+ desc: `With invalid domain`,
+ in: `Name <local@dom[ain>, test@domain`,
+ expErr: `ParseMailboxes: parseMailbox: invalid domain 'dom[ain'`,
}, {
- desc: "With bracket, comments between local part",
- in: "<(comment)local(comment)@domain>",
- exp: "[<local@domain>]",
+ desc: `With no bracket, single address`,
+ in: `local@domain`,
+ exp: `[<local@domain>]`,
}, {
- desc: "With bracket, single address",
- in: "<(comment)local(comment)@(comment)domain>",
- exp: "[<local@domain>]",
+ desc: `With no bracket, comments between single address`,
+ in: `(comment)local(comment)@domain`,
+ exp: `[<local@domain>]`,
}, {
- desc: "With bracket, single address",
- in: "<(comment)local(comment)@(comment)domain(comment(comment))>",
- exp: "[<local@domain>]",
+ desc: `With bracket, comments between local part`,
+ in: `<(comment)local(comment)@domain>`,
+ exp: `[<local@domain>]`,
}, {
- desc: "With ';' on multiple mailboxes",
- in: "One <one@example> ; (comment)",
- expErr: "ParseMailboxes %q: invalid character: ';'",
+ desc: `With bracket, single address`,
+ in: `<(comment)local(comment)@(comment)domain>`,
+ exp: `[<local@domain>]`,
}, {
- desc: "With group list, single address",
- in: "Group name: <(c)local(c)@(c)domain(c)>;(c)",
- exp: "[<local@domain>]",
+ desc: `With bracket, single address`,
+ in: `<(comment)local(comment)@(comment)domain(comment(comment))>`,
+ exp: `[<local@domain>]`,
}, {
- desc: "With group, missing '>'",
- in: "Group name:One <one@example ; (comment)",
- expErr: "ParseMailboxes %q: missing '>'",
+ desc: `With ';' on multiple mailboxes`,
+ in: `One <one@example> ; (comment)`,
+ expErr: `ParseMailboxes: invalid character ';'`,
}, {
- desc: "With group, missing ';'",
- in: "Group name:One <one@example>",
- expErr: "ParseMailboxes %q: missing ';'",
+ desc: `With group list, single address`,
+ in: `Group name: <(c)local(c)@(c)domain(c)>;(c)`,
+ exp: `[<local@domain>]`,
}, {
- desc: "With group, without bracket",
- in: "Group name: one@example ; (comment)",
- exp: "[<one@example>]",
+ desc: `With group, missing '>'`,
+ in: `Group name:One <one@example ; (comment)`,
+ expErr: `ParseMailboxes: parseMailbox: missing '>'`,
}, {
- desc: "With group, without bracket, invalid domain",
- in: "Group name: one@exa[mple ; (comment)",
- expErr: "ParseMailboxes %q: invalid domain: 'exa[mple'",
+ desc: `With group, missing ';'`,
+ in: `Group name:One <one@example>`,
+ expErr: `ParseMailboxes: missing group terminator ';'`,
}, {
- desc: "With group, trailing text before ';'",
- in: "Group name: <one@example> trail ; (comment)",
- expErr: "ParseMailboxes %q: invalid token: 'trail'",
+ desc: `With group, without bracket`,
+ in: `Group name: one@example ; (comment)`,
+ exp: `[<one@example>]`,
}, {
- desc: "With group, trailing text",
- in: "Group name: <(c)local(c)@(c)domain(c)>; trail(c)",
- expErr: "ParseMailboxes %q: trailing text: 'trail'",
+ desc: `With group, without bracket, invalid domain`,
+ in: `Group name: one@exa[mple ; (comment)`,
+ expErr: `ParseMailboxes: parseMailbox: invalid domain 'exa[mple'`,
}, {
- desc: "With group, multiple addresses",
- in: "(c)Group name(c): <(c)local(c)@(c)domain(c)>, Test One <test@one>;(c)",
- exp: "[<local@domain> Test One <test@one>]",
+ desc: `With group, trailing text before ';'`,
+ in: `Group name: <one@example> trail ; (comment)`,
+ expErr: `ParseMailboxes: parseMailbox: unknown token 'trail'`,
}, {
- desc: "With list, invalid ','",
- in: "on,e@example , two@example",
- expErr: "ParseMailboxes %q: invalid character: ','",
+ desc: `With group, trailing text`,
+ in: `Group name: <(c)local(c)@(c)domain(c)>; trail(c)`,
+ exp: `[<local@domain>]`,
}, {
- desc: "With list, missing '>'",
- in: "<one@example , <two@example>",
- expErr: "ParseMailboxes %q: missing '>'",
+ desc: `With group, multiple addresses`,
+ in: `(c)Group name(c): <(c)local(c)@(c)domain(c)>, Test One <test@one>;(c)`,
+ exp: `[<local@domain> Test One <test@one>]`,
}, {
- desc: "With list, invalid domain",
- in: "one@ex[ample , <two@example>",
- expErr: "ParseMailboxes %q: invalid domain: 'ex[ample'",
+ desc: `With list, invalid ','`,
+ in: `on,e@example , two@example`,
+ expErr: `ParseMailboxes: empty or invalid address`,
}, {
- desc: "With list, invalid domain",
- in: "one@example, two@exa[mple",
- expErr: "ParseMailboxes %q: invalid domain: 'exa[mple'",
+ desc: `With list, missing '>'`,
+ in: `<one@example , <two@example>`,
+ expErr: `ParseMailboxes: parseMailbox: missing '>'`,
}, {
- desc: "With list, trailing text after '>'",
- in: "<one@example> trail, <two@example>",
- expErr: "ParseMailboxes %q: invalid token: 'trail'",
+ desc: `With list, invalid local #0`,
+ in: `one@example, @example`,
+ expErr: `ParseMailboxes: parseMailbox: empty local`,
}, {
- desc: "RFC 5322 example",
+ desc: `With list, invalid local #1`,
+ in: `one@example, t[o@example`,
+ expErr: `ParseMailboxes: parseMailbox: invalid local 't[o'`,
+ }, {
+ desc: `With list, invalid local #2`,
+ in: `one@example, t)o@example`,
+ expErr: `ParseMailboxes: parseMailbox: parseMailboxText: invalid character ')'`,
+ }, {
+ desc: `With list, invalid local #3`,
+ in: `one@example, <t)o@example>`,
+ expErr: `ParseMailboxes: parseMailbox: parseMailboxText: invalid character ')'`,
+ }, {
+ desc: `With list, invalid domain #0`,
+ in: `one@ex[ample , <two@example>`,
+ expErr: `ParseMailboxes: parseMailbox: invalid domain 'ex[ample'`,
+ }, {
+ desc: `With list, invalid domain #1`,
+ in: `one@example, two@exa[mple`,
+ expErr: `ParseMailboxes: parseMailbox: invalid domain 'exa[mple'`,
+ }, {
+ desc: `With list, invalid domain #2`,
+ in: `one@example, two@exa)mple`,
+ expErr: `ParseMailboxes: parseMailbox: parseMailboxText: invalid character ')'`,
+ }, {
+ desc: `With list, trailing text #0`,
+ in: `<one@example> trail, <two@example>`,
+ expErr: `ParseMailboxes: parseMailbox: unknown token 'trail'`,
+ }, {
+ desc: `With list, trailing text #1`,
+ in: `<one@example> ), <two@example>`,
+ expErr: `ParseMailboxes: parseMailbox: parseMailboxText: invalid character ')'`,
+ }, {
+ desc: `RFC 5322 example`,
in: "A Group(Some people)\r\n" +
" :Chris Jones <c@(Chris's host.)public.example>,\r\n" +
" joe@example.org,\r\n" +
" John <jdoe@one.test> (my dear friend); (the end of the group)\r\n",
- exp: "[Chris Jones <c@public.example> <joe@example.org> John <jdoe@one.test>]",
+ exp: `[Chris Jones <c@public.example> <joe@example.org> John <jdoe@one.test>]`,
}, {
- desc: "With null address (for Return-Path)",
- in: "<>",
- exp: "[<>]",
+ desc: `With null address (for Return-Path)`,
+ in: `<>`,
+ exp: `[<>]`,
}, {
- desc: "With null address (for Return-Path)",
- in: "<(comment(comment))(comment)>",
- exp: "[<>]",
+ desc: `With null address (for Return-Path) #2`,
+ in: `<(comment(comment))(comment)>`,
+ exp: `[<>]`,
}}
- for _, c := range cases {
+ var (
+ c testCase
+ mboxes []*Mailbox
+ got string
+ err error
+ )
+
+ for _, c = range cases {
t.Log(c.desc)
- mboxes, err := ParseMailboxes([]byte(c.in))
+ mboxes, err = ParseMailboxes([]byte(c.in))
if err != nil {
- exp := fmt.Sprintf(c.expErr, c.in)
- test.Assert(t, "error", exp, err.Error())
+ test.Assert(t, `error`, c.expErr, err.Error())
continue
}
- got := fmt.Sprintf("%+v", mboxes)
- test.Assert(t, "Mailboxes", c.exp, got)
+ got = fmt.Sprintf(`%+v`, mboxes)
+ test.Assert(t, `Mailboxes`, c.exp, got)
}
}
-func TestSkipComment(t *testing.T) {
+func TestParseMailboxText(t *testing.T) {
cases := []struct {
desc string
in string
expErr string
exp string
}{{
- desc: "With empty input",
- in: "",
- expErr: "missing comment close parentheses",
+ desc: `With empty input`,
+ in: ``,
}, {
- desc: "With empty comment",
- in: "()",
+ desc: `With empty comment`,
+ in: `()`,
}, {
- desc: "With quoted-pair",
+ desc: `With quoted-pair`,
in: `(\)) x`,
exp: `x`,
}, {
- desc: "With no closing parentheses",
+ desc: `With no closing parentheses`,
in: `(\) x`,
- expErr: "missing comment close parentheses",
+ expErr: `parseMailboxText: missing comment close parentheses`,
}, {
- desc: "With invalid nested comments",
+ desc: `With invalid nested comments`,
in: `((comment x`,
- expErr: "missing comment close parentheses",
+ expErr: `parseMailboxText: missing comment close parentheses`,
}, {
- desc: "With invalid nested comments",
+ desc: `With invalid nested comments`,
in: `((comment) x`,
- expErr: "missing comment close parentheses",
+ expErr: `parseMailboxText: missing comment close parentheses`,
}, {
desc: "With nested comments",
in: `(((\(comment))) x`,
exp: `x`,
}, {
- desc: "With multiple comments",
+ desc: `With multiple comments`,
in: `(comment) (comment) x`,
exp: `x`,
+ }, {
+ desc: `With escaped char`,
+ in: `\(x(comment)`,
+ exp: `(x`,
}}
- r := &libio.Reader{}
+ var (
+ parser = libbytes.NewParser(nil, nil)
+ got []byte
+ err error
+ )
for _, c := range cases {
t.Log(c.desc)
- r.Init([]byte(c.in))
- r.SkipN(1)
- _, err := skipComment(r)
+ parser.Reset([]byte(c.in), []byte{'\\', '(', ')'})
+
+ got, _, err = parseMailboxText(parser)
if err != nil {
- test.Assert(t, "error", c.expErr, err.Error())
+ test.Assert(t, `error`, c.expErr, err.Error())
continue
}
- got := string(r.Rest())
-
- test.Assert(t, "rest", c.exp, got)
+ test.Assert(t, `text`, c.exp, string(got))
}
}
diff --git a/lib/email/message_test.go b/lib/email/message_test.go
index 1510aa1a..7813929b 100644
--- a/lib/email/message_test.go
+++ b/lib/email/message_test.go
@@ -165,7 +165,7 @@ func TestMessage_AddCC(t *testing.T) {
}, {
desc: "Invalid mailbox",
mailboxes: "a",
- expError: `AddCC: ParseMailboxes "a": empty or invalid address`,
+ expError: `AddCC: ParseMailboxes: empty or invalid address`,
expMsg: "cc:one <a@b.c>\r\n\r\n",
}, {
desc: "Multiple mailboxes",
@@ -203,7 +203,7 @@ func TestMessage_AddTo(t *testing.T) {
}, {
desc: "Invalid mailbox",
mailboxes: "a",
- expError: `AddTo: ParseMailboxes "a": empty or invalid address`,
+ expError: `AddTo: ParseMailboxes: empty or invalid address`,
expMsg: "to:one <a@b.c>\r\n\r\n",
}, {
desc: "Multiple mailboxes",
@@ -464,7 +464,7 @@ func TestMessage_SetCC(t *testing.T) {
}, {
desc: "Invalid mailbox",
mailboxes: "a",
- expError: `SetCC: Set: ParseMailboxes "a": empty or invalid address`,
+ expError: `SetCC: Set: ParseMailboxes: empty or invalid address`,
expMsg: "cc:test <a@b.c>\r\n\r\n",
}, {
desc: "Multiple mailboxes",
@@ -502,7 +502,7 @@ func TestMessage_SetFrom(t *testing.T) {
}, {
desc: "Invalid mailbox",
mailbox: "a",
- expError: `SetFrom: Set: ParseMailboxes "a": empty or invalid address`,
+ expError: `SetFrom: Set: ParseMailboxes: empty or invalid address`,
expMsg: "from:test <a@b.c>\r\n\r\n",
}, {
desc: "New mailbox",
@@ -569,7 +569,7 @@ func TestMessage_SetTo(t *testing.T) {
desc: "Invalid mailbox",
mailboxes: "a",
expMsg: "to:test <a@b.c>\r\n\r\n",
- expError: `SetTo: Set: ParseMailboxes "a": empty or invalid address`,
+ expError: `SetTo: Set: ParseMailboxes: empty or invalid address`,
}, {
desc: "Multiple mailboxes",
mailboxes: "new <a@b.c>, from <a@b.c>",