-
Notifications
You must be signed in to change notification settings - Fork 2
/
parse_test.go
348 lines (292 loc) · 12.4 KB
/
parse_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
package mailaddress
import (
"crypto/rand"
"fmt"
"math/big"
"testing"
)
var validAddresses = map[string]Address{
"addr-spec@example.net": {Address: "addr-spec@example.net"},
"<angle-addr@example.net>": {Name: "", Address: "angle-addr@example.net"},
"First Last <mailbox@example.net>": {Name: "First Last", Address: "mailbox@example.net"},
`"First Last" <mailbox@example.net>`: {Name: "First Last", Address: "mailbox@example.net"},
`"<Firšt; Låšt>" <x@example.net>`: {Name: `<Firšt; Låšt>`, Address: `x@example.net`},
"dot.in.local.part@example.com": {Address: "dot.in.local.part@example.com"},
"Name <dot.in.local.part@example.com>": {Name: "Name", Address: "dot.in.local.part@example.com"},
"tagged+tag@example.com": {Address: "tagged+tag@example.com"},
"Name <tagged+tag@example.com>": {Name: "Name", Address: "tagged+tag@example.com"},
"dashed-dash@ex-ample.com": {Address: "dashed-dash@ex-ample.com"},
"Name <dashed-dash@ex-ample.com>": {Name: "Name", Address: "dashed-dash@ex-ample.com"},
"example@example.verylongtld": {Address: "example@example.verylongtld"},
`Po "Wiśnasd" <asd@asd-def-24h.zxc>`: {Name: `Po Wiśnasd`, Address: "asd@asd-def-24h.zxc"},
`Uni العَرَبِية Cøde <x@example.net>`: {Name: "Uni العَرَبِية Cøde", Address: "x@example.net"},
// TODO: Quoting the local part isn't supported (yet).
//`"quoted"@example.com`: {Address: `"quoted"@example.com`},
//`Name <"quoted"@example.com>`: {Name: "Name", Address: `"quoted"@example.com`},
//`"very.unusual.@.unusual.com"@example.com`: {Address: `"very.unusual.@.unusual.com"@example.com`},
//"/#!$%&'*+-/=?^_`{}|~@example.org": {Address: "/#!$%&'*+-/=?^_`{}|~@example.org"},
//"\" \"@example.org": {Address: "\" \"@example.org"},
//"\"()<>[]:,;@\\\"!#$%&'-/=?^_`{}| ~.a\"@example.org": {
// Address: "\"()<>[]:,;@\\\"!#$%&'-/=?^_`{}| ~.a\"@example.org",
//},
//`"very.(),:;<>[]\".VERY.\"very@\ \"very\".unusual"@strange.example.com`: {
// Address: `"very.(),:;<>[]\".VERY.\"very@\ \"very\".unusual"@strange.example.com`,
//},
// \ is 'invisible', but can escape ".
`"esc \some\ \"quotes\"" <q@example.net>`: {Name: `esc some "quotes"`, Address: `q@example.net`},
// You can stop and start quoting
`"Martin" foo "Tournoij" <martin@example.net>`: {Name: `Martin foo Tournoij`, Address: `martin@example.net`},
`"Martin"foo"Tournoij" <martin@example.net>`: {Name: `MartinfooTournoij`, Address: `martin@example.net`},
`'Martin' foo 'Tournoij' <martin@example.net>`: {Name: `'Martin' foo 'Tournoij'`, Address: `martin@example.net`},
`'Martin foo Tournoij' <martin@example.net>`: {Name: `Martin foo Tournoij`, Address: `martin@example.net`},
// do not support this yet
`'Martin foo's Tournoij' <martin@example.net>`: {Name: `'Martin foo's Tournoij'`, Address: `martin@example.net`},
`'Martin foo's foo's Tournoij' <martin@example.net>`: {Name: `'Martin foo's foo's Tournoij'`, Address: `martin@example.net`},
// One-letter local-part, domain
"a@b.c": {Address: "a@b.c"},
"Name <a@b.c>": {Name: "Name", Address: "a@b.c"},
// We can parse the old deprecated "comment" style; per RFC 5322:
//
// Note: Some legacy implementations used the simple form where the
// addr-spec appears without the angle brackets, but included the name
// of the recipient in parentheses as a comment following the addr-spec.
// Since the meaning of the information in a comment is unspecified,
// implementations SHOULD use the full name-addr form of the mailbox,
// instead of the legacy form, to specify the display name associated
// with a mailbox. Also, because some legacy implementations interpret
// the comment, comments generally SHOULD NOT be used in address fields
// to avoid confusing such implementations.
//
// In spite this being explicitly deprecated for at least 15 years, some
// systems still use this format – mainly cron daemons and (ironically)
// MTAs.
"MAILER-DAEMON@example.org (Mail Delivery System)": {Name: "", Address: "MAILER-DAEMON@example.org"},
"hello (world) <foo@foo.foo>": {Name: "hello (world)", Address: "foo@foo.foo"},
"MAILER-DAEMON@example.org ()": {Name: "", Address: "MAILER-DAEMON@example.org"},
"hello () <foo@foo.foo>": {Name: "hello ()", Address: "foo@foo.foo"},
// Newlines are folded
`Martin
Tournoij
<martin@example.net>`: {Name: `Martin Tournoij`, Address: `martin@example.net`},
// Leading/trailing whitespace
" Martin <martin@example.com> ": {Name: "Martin", Address: "martin@example.com"},
" Martin<martin@example.com> ": {Name: "Martin", Address: "martin@example.com"},
" Martin <martin@example.com> ": {Name: "Martin", Address: "martin@example.com"},
// RFC 2047. We don't need to extensibly test it since we use Go's package,
// and assume that works well.
`=?utf-8?q?=E6=97=A5=E6=9C=AC=D0=BA=D0=B8=E6=AD=A3=E9=AB=94=E0=B8=AD?=` +
`=?utf-8?q?=E0=B8=B1=E0=B8=81=E0=B8=A9=ED=9B=88=EB=AF=BC?= <a@example.net>`: {
Name: `日本ки正體อักษ훈민`, Address: `a@example.net`},
// Non-utf8
"=?iso-8859-2?Q?Bogl=E1rka_Tak=E1cs?= <a@example.com>": {Name: "Boglárka Takács", Address: "a@example.com"},
`=?koi8-r?B?IvfMwcTJzcnSIPPFzcXOz9ci?= <boris@rusky.com>`: {Name: `"Владимир Семенов"`, Address: `boris@rusky.com`},
}
var invalidAddresses = []string{
// Invalid encoding
"=?GB2312?B?us6V08qk?= <secmocu@jshjkj.com>",
"no.at.example.com",
"multiple@at@signs@example.com",
// none of the special characters in this local-part are allowed outside
// quotation marks
`a"b(c)d,e:f;gi[j\k]l@example.com`,
// spaces, quotes, and backslashes may only exist when within quoted strings
// and preceded by a backslash
`this is"not\allowed@example.com`,
// even if escaped (preceded by a backslash), spaces, quotes, and
// backslashes must still be contained by quotes
`this\ still\"not\allowed@example.com`,
// sent from localhost
"example@localhost",
"admin@mailserver1",
// double dot after @ – caveat: Gmail lets this through, Email
// address#Local-part the dots altogether
`john.doe@example..com`,
// Multiple addresses.
"multiple@example.com addresses@example.com",
// Invalid UTF-8
string([]byte{0xed, 0xa0, 0x80}) + " <micro@example.net>",
"\"" + string([]byte{0xed, 0xa0, 0x80}) + "\" <half-surrogate@example.com>",
"\"\\" + string([]byte{0x80}) + "\" <escaped-invalid-unicode@example.net>",
// Don't allow unprintable characters.
"\"\x00\" <null@example.net>",
"\"\\\x00\" <escaped-null@example.net>",
"asd\x06asd <null@example.net>",
// Random data after >
"foo <foo@example.com> huh",
// Various junk data collected.
`roby.bell@comcast.netVortex666!!`,
`martimbault@.qc.aira.com`,
`foo jlzuniga@comcast.net>`,
`noreply@http://www.acadiapinesmotel.com`,
`heartinternet.co.uk NO-REPLY@heartinternet.co.uk`,
`Arthurgrebenuk@gmail`,
`lgomberg@dmh.co.la.ca.usordiglg@earth`,
`smellycats612@aol..com`,
`concretesawing@comcast.net13113602@dwsg`,
`MM522@aol.com315=269-5244`,
`"much.more unusual"@example.com`,
// quoted strings must be dot separated or the only element making up the
// local-part.
//`just"not"right@example.com`,
// Technically valid, but we don't want to accept this.
//TODO: accepted as valid "user@192.168.1.1",
"user@[IPv6:2001:DB8::1]",
}
func TestParseAddress(t *testing.T) {
for test, expected := range validAddresses {
t.Run(test, func(t *testing.T) {
out, err := Parse(test)
if err != nil {
t.Fatal(err)
}
if !cmpaddr(out, expected) {
t.Errorf("\n out: %v\n expected: %v\n",
fmtaddr(out), fmtaddr(expected))
}
})
}
}
func TestParseStringOutput(t *testing.T) {
for test, expected := range validAddresses {
t.Run(test, func(t *testing.T) {
parsed, err := Parse(test)
// TestParseAddress will show an error already.
if err != nil || !cmpaddr(parsed, expected) {
t.Skip()
}
out, err := Parse(parsed.StringEncoded())
if err != nil {
t.Fatal(err)
}
if !cmpaddr(out, expected) {
t.Errorf("\n out: %v\n expected: %v\n",
fmtaddr(out), fmtaddr(expected))
}
})
}
}
func TestParseListAddress(t *testing.T) {
// Mix them up randomly to test ParseList
var keys []string
for k := range validAddresses {
keys = append(keys, k)
}
mixed := make(map[string][]Address)
for test, expected := range validAddresses {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(validAddresses)-3)))
newTest := test
newExpected := []Address{expected}
for i := int64(0); i < n.Int64(); i++ {
rnd, _ := rand.Int(rand.Reader, big.NewInt(int64(len(validAddresses))))
pick := keys[rnd.Int64()]
newTest += ", " + pick
newExpected = append(newExpected, validAddresses[pick])
}
mixed[newTest] = List(newExpected).uniq()
}
for test, expected := range mixed {
t.Run(test, func(t *testing.T) {
out, gotErr := ParseList(test)
if gotErr {
t.Errorf("gotErr is true; errors: %#v", out.Errors())
}
if !cmplist(out, expected) {
t.Fatalf("Error different length:\n out : %#v\n expected: %#v\n",
out, expected)
}
for i := range out {
if !cmpaddr(out[i], expected[i]) {
t.Errorf("Error:\n out : %#v\n expected: %#v\n",
out, expected)
}
}
// Make sure we can re-parse our output.
t.Run("reparse", func(t *testing.T) {
reparsedOut, gotErr := ParseList(out.StringEncoded())
if gotErr {
t.Errorf("gotErr is true; errors: %#v", out.Errors())
}
if !cmplist(reparsedOut, expected) {
t.Fatalf("Error different length:\n out : %#v\n expected: %#v\n",
reparsedOut, expected)
}
for i := range reparsedOut {
if !cmpaddr(reparsedOut[i], expected[i]) {
t.Errorf("Error:\n out : %#v\n expected: %#v\n",
reparsedOut, expected)
}
}
})
})
}
}
func TestString(t *testing.T) {
cases := map[string]string{
`martin@example.net`: `martin@example.net`,
`<martin@example.net>`: `martin@example.net`,
`Martin Tournoij <martin@example.net>`: `"Martin Tournoij" <martin@example.net>`,
`"Martin Tournoij" <martin@example.net>`: `"Martin Tournoij" <martin@example.net>`,
`Martin العَرَبِية Tournoij <martin@example.net>`: `"Martin العَرَبِية Tournoij" <martin@example.net>`,
`"<Martin; Tøurnoij>" <martin@example.net>`: `"<Martin; Tøurnoij>" <martin@example.net>`,
`"Martin \a\ \"Tournoij\"" <martin@example.net>`: `"Martin a \"Tournoij\"" <martin@example.net>`,
`"Martin" foo "Tournoij" <martin@example.net>`: `"Martin foo Tournoij" <martin@example.net>`,
`"Martin"foo"Tournoij" <martin@example.net>`: `"MartinfooTournoij" <martin@example.net>`,
`Martin
Tournoij
<martin@example.net>`: `"Martin Tournoij" <martin@example.net>`,
`=?utf-8?q?=E6=97=A5=E6=9C=AC=D0=BA=D0=B8=E6=AD=A3=E9=AB=94=E0=B8=AD?=` +
`=?utf-8?q?=E0=B8=B1=E0=B8=81=E0=B8=A9=ED=9B=88=EB=AF=BC?= <a@example.net>`: `` +
`"日本ки正體อักษ훈민" <a@example.net>`,
``: ``,
`,`: ``,
`martin@example.com,`: `martin@example.com`,
`,martin@example.com`: `martin@example.com`,
`,martin@example.com,`: `martin@example.com`,
`"martin@example.com FOO" <martin@example.com>`: `"martin@example.com FOO" <martin@example.com>`,
}
for test, expected := range cases {
t.Run(test, func(t *testing.T) {
out, gotErr := ParseList(test)
if gotErr {
t.Error(out.Errors())
}
if out.String() != expected {
t.Errorf("\nout: %v\nexpected: %v\nlist: %#v\n",
out.String(), expected, out)
}
})
}
}
func TestInvalid(t *testing.T) {
for i, test := range invalidAddresses {
t.Run(fmt.Sprintf("%d_%s", i, test), func(t *testing.T) {
out, err := Parse(test)
if out.Valid() {
t.Fatal("out.Valid() said it was valid.")
}
if err == nil {
t.Fatal("err == nil")
}
if out.Error() == nil {
t.Fatal("out.Error == nil")
}
})
}
}
func fmtaddr(a Address) string {
return fmt.Sprintf("Name: %v, Address: %v", a.Name, a.Address)
}
func cmpaddr(a, b Address) bool {
return a.Address == b.Address && a.Name == b.Name
}
func cmplist(a, b List) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if !(a[i].Address == b[i].Address && a[i].Name == b[i].Name) {
return false
}
}
return true
}