diff options
| author | Shulhan <ms@kilabit.info> | 2023-04-08 16:55:12 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2023-04-08 16:56:26 +0700 |
| commit | 2d669a4d7f33c2af91ba2aba1ce72553ce0ece6a (patch) | |
| tree | 74e8ddfe81f88f9fffafdd1bdfb4edff5f542072 | |
| parent | bba662858aded3c09a5dd62cb3e1ef14e0316c54 (diff) | |
| download | pakakeh.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.
| -rw-r--r-- | lib/email/field_test.go | 2 | ||||
| -rw-r--r-- | lib/email/mailbox.go | 384 | ||||
| -rw-r--r-- | lib/email/mailbox_test.go | 359 | ||||
| -rw-r--r-- | lib/email/message_test.go | 10 |
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>", |
