1package spf
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net"
8 "reflect"
9 "testing"
10 "time"
11
12 "github.com/mjl-/mox/dns"
13 "github.com/mjl-/mox/smtp"
14)
15
16func TestLookup(t *testing.T) {
17 resolver := dns.MockResolver{
18 TXT: map[string][]string{
19 "temperror.example.": {"irrelevant"},
20 "malformed.example.": {"v=spf1 !"},
21 "multiple.example.": {"v=spf1", "v=spf1"},
22 "nonspf.example.": {"something else"},
23 "ok.example.": {"v=spf1"},
24 },
25 Fail: map[dns.Mockreq]struct{}{
26 {Type: "txt", Name: "temperror.example."}: {},
27 },
28 }
29
30 test := func(domain string, expStatus Status, expRecord *Record, expErr error) {
31 t.Helper()
32
33 d := dns.Domain{ASCII: domain}
34 status, txt, record, err := Lookup(context.Background(), resolver, d)
35 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
36 t.Fatalf("got err %v, expected err %v", err, expErr)
37 }
38 if err != nil {
39 return
40 }
41 if status != expStatus || txt == "" || !reflect.DeepEqual(record, expRecord) {
42 t.Fatalf("got status %q, txt %q, record %#v, expected %q, ..., %#v", status, txt, record, expStatus, expRecord)
43 }
44 }
45
46 test("..", StatusNone, nil, ErrName)
47 test("absent.example", StatusNone, nil, ErrNoRecord)
48 test("temperror.example", StatusTemperror, nil, ErrDNS)
49 test("malformed.example", StatusPermerror, nil, ErrRecordSyntax)
50 test("multiple.example", StatusPermerror, nil, ErrMultipleRecords)
51 test("nonspf.example", StatusNone, nil, ErrNoRecord)
52 test("ok.example", StatusNone, &Record{Version: "spf1"}, nil)
53}
54
55func TestExpand(t *testing.T) {
56 defArgs := Args{
57 senderLocalpart: "strong-bad",
58 senderDomain: dns.Domain{ASCII: "email.example.com"},
59 domain: dns.Domain{ASCII: "email.example.com"},
60
61 MailFromLocalpart: "x",
62 MailFromDomain: dns.Domain{ASCII: "mox.example"},
63 HelloDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "mx.mox.example"}},
64 LocalIP: net.ParseIP("10.10.10.10"),
65 LocalHostname: dns.Domain{ASCII: "self.example"},
66 }
67
68 resolver := dns.MockResolver{
69 PTR: map[string][]string{
70 "10.0.0.1": {"other.example.", "sub.mx.mox.example.", "mx.mox.example."},
71 "10.0.0.2": {"other.example.", "sub.mx.mox.example.", "mx.mox.example."},
72 "10.0.0.3": {"other.example.", "sub.mx.mox.example.", "mx.mox.example."},
73 },
74 A: map[string][]string{
75 "mx.mox.example.": {"10.0.0.1"},
76 "sub.mx.mox.example.": {"10.0.0.2"},
77 "other.example.": {"10.0.0.3"},
78 },
79 }
80
81 mustParseIP := func(s string) net.IP {
82 ip := net.ParseIP(s)
83 if ip == nil {
84 t.Fatalf("bad ip %q", s)
85 }
86 return ip
87 }
88
89 ctx := context.Background()
90
91 // Examples from ../rfc/7208:1777
92 test := func(dns bool, macro, ip, exp string) {
93 t.Helper()
94
95 args := defArgs
96 args.dnsRequests = new(int)
97 args.voidLookups = new(int)
98 if ip != "" {
99 args.RemoteIP = mustParseIP(ip)
100 }
101
102 r, err := expandDomainSpec(ctx, resolver, macro, args, dns)
103 if (err == nil) != (exp != "") {
104 t.Fatalf("got err %v, expected expansion %q, for macro %q", err, exp, macro)
105 }
106 if r != exp {
107 t.Fatalf("got expansion %q, expected %q, for macro %q", r, exp, macro)
108 }
109 }
110
111 testDNS := func(macro, ip, exp string) {
112 t.Helper()
113 test(true, macro, ip, exp)
114 }
115
116 testExpl := func(macro, ip, exp string) {
117 t.Helper()
118 test(false, macro, ip, exp)
119 }
120
121 testDNS("%{s}", "", "strong-bad@email.example.com")
122 testDNS("%{o}", "", "email.example.com")
123 testDNS("%{d}", "", "email.example.com")
124 testDNS("%{d4}", "", "email.example.com")
125 testDNS("%{d3}", "", "email.example.com")
126 testDNS("%{d2}", "", "example.com")
127 testDNS("%{d1}", "", "com")
128 testDNS("%{dr}", "", "com.example.email")
129 testDNS("%{d2r}", "", "example.email")
130 testDNS("%{l}", "", "strong-bad")
131 testDNS("%{l-}", "", "strong.bad")
132 testDNS("%{lr}", "", "strong-bad")
133 testDNS("%{lr-}", "", "bad.strong")
134 testDNS("%{l1r-}", "", "strong")
135
136 testDNS("%", "", "")
137 testDNS("%b", "", "")
138 testDNS("%{", "", "")
139 testDNS("%{s", "", "")
140 testDNS("%{s1", "", "")
141 testDNS("%{s0}", "", "")
142 testDNS("%{s1r", "", "")
143 testDNS("%{s99999999999999999999999999999999999999999999999999999999999999999999999}", "", "")
144
145 testDNS("%{ir}.%{v}._spf.%{d2}", "192.0.2.3", "3.2.0.192.in-addr._spf.example.com")
146 testDNS("%{lr-}.lp._spf.%{d2}", "192.0.2.3", "bad.strong.lp._spf.example.com")
147 testDNS("%{lr-}.lp.%{ir}.%{v}._spf.%{d2}", "192.0.2.3", "bad.strong.lp.3.2.0.192.in-addr._spf.example.com")
148 testDNS("%{ir}.%{v}.%{l1r-}.lp._spf.%{d2}", "192.0.2.3", "3.2.0.192.in-addr.strong.lp._spf.example.com")
149 testDNS("%{d2}.trusted-domains.example.net", "192.0.2.3", "example.com.trusted-domains.example.net")
150
151 testDNS("%{ir}.%{v}._spf.%{d2}", "2001:db8::cb01", "1.0.b.c.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6._spf.example.com")
152
153 // Additional.
154 testDNS("%%%-%_", "10.0.0.1", "%%20 ")
155 testDNS("%{p}", "10.0.0.1", "mx.mox.example.")
156 testDNS("%{p}", "10.0.0.2", "sub.mx.mox.example.")
157 testDNS("%{p}", "10.0.0.3", "other.example.")
158 testDNS("%{p}", "10.0.0.4", "unknown")
159 testExpl("%{c}", "10.0.0.1", "10.10.10.10")
160 testExpl("%{r}", "10.0.0.1", "self.example")
161 orig := timeNow
162 now := orig()
163 defer func() {
164 timeNow = orig
165 }()
166 timeNow = func() time.Time {
167 return now
168 }
169 testExpl("%{t}", "10.0.0.1", fmt.Sprintf("%d", now.Unix()))
170 // DNS name can be 253 bytes long, each label can be 63 bytes.
171 xlabel := make([]byte, 62)
172 for i := range xlabel {
173 xlabel[i] = 'a'
174 }
175 label := string(xlabel)
176 name := label + "." + label + "." + label + "." + label // 4*62+3 = 251
177 testDNS("x."+name, "10.0.0.1", "x."+name) // Still fits.
178 testDNS("xx."+name, "10.0.0.1", name) // Does not fit, "xx." is truncated to make it fit.
179 testDNS("%{p}..", "10.0.0.1", "")
180 testDNS("%{h}", "10.0.0.1", "mx.mox.example")
181}
182
183func TestVerify(t *testing.T) {
184 xip := func(s string) net.IP {
185 ip := net.ParseIP(s)
186 if ip == nil {
187 t.Fatalf("bad ip: %q", s)
188 }
189 return ip
190 }
191 iplist := func(l ...string) []net.IP {
192 r := make([]net.IP, len(l))
193 for i, s := range l {
194 r[i] = xip(s)
195 }
196 return r
197 }
198
199 // ../rfc/7208:2975 Appendix A. Extended Examples
200 r := dns.MockResolver{
201 PTR: map[string][]string{
202 "192.0.2.10": {"example.com."},
203 "192.0.2.11": {"example.com."},
204 "192.0.2.65": {"amy.example.com."},
205 "192.0.2.66": {"bob.example.com."},
206 "192.0.2.129": {"mail-a.example.com."},
207 "192.0.2.130": {"mail-b.example.com."},
208 "192.0.2.140": {"mail-c.example.org."},
209 "10.0.0.4": {"bob.example.com."},
210 },
211 TXT: map[string][]string{
212 // Additional from DNSBL, ../rfc/7208:3115
213 "mobile-users._spf.example.com.": {"v=spf1 exists:%{l1r+}.%{d}"},
214 "remote-users._spf.example.com.": {"v=spf1 exists:%{ir}.%{l1r+}.%{d}"},
215
216 // Additional ../rfc/7208:3171
217 "ip4._spf.example.com.": {"v=spf1 -ip4:192.0.2.0/24 +all"},
218 "ptr._spf.example.com.": {"v=spf1 -ptr:example.com +all"}, // ../rfc/7208-eid6216 ../rfc/7208:3172
219
220 // Additional tests
221 "_spf.example.com.": {"v=spf1 include:_netblock.example.com -all"},
222 "_netblock.example.com.": {"v=spf1 ip4:192.0.2.128/28 -all"},
223 },
224 A: map[string][]string{
225 "example.com.": {"192.0.2.10", "192.0.2.11"},
226 "amy.example.com.": {"192.0.2.65"},
227 "bob.example.com.": {"192.0.2.66"},
228 "mail-a.example.com.": {"192.0.2.129"},
229 "mail-b.example.com.": {"192.0.2.130"},
230 "mail-c.example.org.": {"192.0.2.140"},
231
232 // Additional from DNSBL, ../rfc/7208:3115
233 "mary.mobile-users._spf.example.com.": {"127.0.0.2"},
234 "fred.mobile-users._spf.example.com.": {"127.0.0.2"},
235 "15.15.168.192.joel.remote-users._spf.example.com.": {"127.0.0.2"},
236 "16.15.168.192.joel.remote-users._spf.example.com.": {"127.0.0.2"},
237 },
238 AAAA: map[string][]string{},
239 MX: map[string][]*net.MX{
240 "example.com.": {
241 {Host: "mail-a.example.com.", Pref: 10},
242 {Host: "mail-b.example.com.", Pref: 20},
243 },
244 "example.org.": {
245 {Host: "mail-c.example.org.", Pref: 10},
246 },
247 },
248 Fail: map[dns.Mockreq]struct{}{},
249 }
250
251 ctx := context.Background()
252
253 verify := func(ip net.IP, localpart string, status Status) {
254 t.Helper()
255
256 args := Args{
257 MailFromLocalpart: smtp.Localpart(localpart),
258 MailFromDomain: dns.Domain{ASCII: "example.com"},
259 RemoteIP: ip,
260 LocalIP: xip("127.0.0.1"),
261 LocalHostname: dns.Domain{ASCII: "localhost"},
262 }
263 received, _, _, err := Verify(ctx, r, args)
264 if received.Result != status {
265 t.Fatalf("got status %q, expected %q, for ip %q (err %v)", received.Result, status, ip, err)
266 }
267 if err != nil {
268 t.Fatalf("unexpected error: %s", err)
269 }
270 }
271
272 test := func(txt string, ips []net.IP, only bool) {
273 r.TXT["example.com."] = []string{txt}
274 seen := map[string]struct{}{}
275 for _, ip := range ips {
276 verify(ip, "", StatusPass)
277 seen[ip.String()] = struct{}{}
278 }
279 if !only {
280 return
281 }
282 for ip := range r.PTR {
283 if _, ok := seen[ip]; ok {
284 continue
285 }
286 verify(xip(ip), "", StatusFail)
287 }
288 }
289
290 // ../rfc/7208:3031 A.1. Simple Examples
291 test("v=spf1 +all", iplist("192.0.2.129", "1.2.3.4"), false)
292 test("v=spf1 a -all", iplist("192.0.2.10", "192.0.2.11"), true)
293 test("v=spf1 a:example.org -all", iplist(), true)
294 test("v=spf1 mx -all", iplist("192.0.2.129", "192.0.2.130"), true)
295 test("v=spf1 mx:example.org -all", iplist("192.0.2.140"), true)
296 test("v=spf1 mx mx:example.org -all", iplist("192.0.2.129", "192.0.2.130", "192.0.2.140"), true)
297 test("v=spf1 mx/30 mx:example.org/30 -all", iplist("192.0.2.129", "192.0.2.130", "192.0.2.140"), true)
298 test("v=spf1 ptr -all", iplist("192.0.2.10", "192.0.2.11", "192.0.2.65", "192.0.2.66", "192.0.2.129", "192.0.2.130"), true)
299 test("v=spf1 ip4:192.0.2.128/28 -all", iplist("192.0.2.129", "192.0.2.130", "192.0.2.140"), true)
300
301 // Additional tests
302 test("v=spf1 redirect=_spf.example.com", iplist("192.0.2.129", "192.0.2.130", "192.0.2.140"), true)
303
304 // Additional from DNSBL, ../rfc/7208:3115
305 r.TXT["example.com."] = []string{"v=spf1 mx include:mobile-users._spf.%{d} include:remote-users._spf.%{d} -all"}
306 verify(xip("1.2.3.4"), "mary", StatusPass)
307 verify(xip("1.2.3.4"), "fred", StatusPass)
308 verify(xip("1.2.3.4"), "fred+wildcard", StatusPass)
309 verify(xip("1.2.3.4"), "joel", StatusFail)
310 verify(xip("1.2.3.4"), "other", StatusFail)
311 verify(xip("192.168.15.15"), "joel", StatusPass)
312 verify(xip("192.168.15.16"), "joel", StatusPass)
313 verify(xip("192.168.15.17"), "joel", StatusFail)
314 verify(xip("192.168.15.17"), "other", StatusFail)
315
316 // Additional ../rfc/7208:3171
317 r.TXT["example.com."] = []string{"v=spf1 -include:ip4._spf.%{d} -include:ptr._spf.%{d} +all"}
318 r.PTR["192.0.2.1"] = []string{"a.example.com."}
319 r.PTR["192.0.0.1"] = []string{"b.example.com."}
320 r.A["a.example.com."] = []string{"192.0.2.1"}
321 r.A["b.example.com."] = []string{"192.0.0.1"}
322
323 verify(xip("192.0.2.1"), "", StatusPass) // IP in range and PTR matches.
324 verify(xip("192.0.2.2"), "", StatusFail) // IP in range but no PTR match.
325 verify(xip("192.0.0.1"), "", StatusFail) // PTR match but IP not in range.
326 verify(xip("192.0.0.2"), "", StatusFail) // No PTR match and IP not in range.
327}
328
329// ../rfc/7208:3093
330func TestVerifyMultipleDomain(t *testing.T) {
331 resolver := dns.MockResolver{
332 TXT: map[string][]string{
333 "example.org.": {"v=spf1 include:example.com include:example.net -all"},
334 "la.example.org.": {"v=spf1 redirect=example.org"},
335 "example.com.": {"v=spf1 ip4:10.0.0.1 -all"},
336 "example.net.": {"v=spf1 ip4:10.0.0.2 -all"},
337 },
338 }
339
340 verify := func(domain, ip string, status Status) {
341 t.Helper()
342
343 args := Args{
344 MailFromDomain: dns.Domain{ASCII: domain},
345 RemoteIP: net.ParseIP(ip),
346 LocalIP: net.ParseIP("127.0.0.1"),
347 LocalHostname: dns.Domain{ASCII: "localhost"},
348 }
349 received, _, _, err := Verify(context.Background(), resolver, args)
350 if err != nil {
351 t.Fatalf("unexpected error: %s", err)
352 }
353 if received.Result != status {
354 t.Fatalf("got status %q, expected %q, for ip %q", received.Result, status, ip)
355 }
356 }
357
358 verify("example.com", "10.0.0.1", StatusPass)
359 verify("example.net", "10.0.0.2", StatusPass)
360 verify("example.com", "10.0.0.2", StatusFail)
361 verify("example.net", "10.0.0.1", StatusFail)
362 verify("example.org", "10.0.0.1", StatusPass)
363 verify("example.org", "10.0.0.2", StatusPass)
364 verify("example.org", "10.0.0.3", StatusFail)
365 verify("la.example.org", "10.0.0.1", StatusPass)
366 verify("la.example.org", "10.0.0.2", StatusPass)
367 verify("la.example.org", "10.0.0.3", StatusFail)
368}
369
370func TestVerifyScenarios(t *testing.T) {
371 test := func(resolver dns.Resolver, args Args, expStatus Status, expDomain string, expExpl string, expErr error) {
372 t.Helper()
373
374 recv, d, expl, err := Verify(context.Background(), resolver, args)
375 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
376 t.Fatalf("got err %v, expected %v", err, expErr)
377 }
378 if expStatus != recv.Result || expDomain != "" && d.ASCII != expDomain || expExpl != "" && expl != expExpl {
379 t.Fatalf("got status %q, domain %q, expl %q, err %v", recv.Result, d, expl, err)
380 }
381 }
382
383 r := dns.MockResolver{
384 TXT: map[string][]string{
385 "mox.example.": {"v=spf1 ip6:2001:db8::0/64 -all"},
386 "void.example.": {"v=spf1 exists:absent1.example exists:absent2.example ip4:1.2.3.4 exists:absent3.example -all"},
387 "loop.example.": {"v=spf1 include:loop.example -all"},
388 "a-unknown.example.": {"v=spf1 a:absent.example"},
389 "include-bad-expand.example.": {"v=spf1 include:%{c}"}, // macro 'c' only valid while expanding for "exp".
390 "exists-bad-expand.example.": {"v=spf1 exists:%{c}"}, // macro 'c' only valid while expanding for "exp".
391 "redir-bad-expand.example.": {"v=spf1 redirect=%{c}"}, // macro 'c' only valid while expanding for "exp".
392 "a-bad-expand.example.": {"v=spf1 a:%{c}"}, // macro 'c' only valid while expanding for "exp".
393 "mx-bad-expand.example.": {"v=spf1 mx:%{c}"}, // macro 'c' only valid while expanding for "exp".
394 "ptr-bad-expand.example.": {"v=spf1 ptr:%{c}"}, // macro 'c' only valid while expanding for "exp".
395 "include-temperror.example.": {"v=spf1 include:temperror.example"},
396 "include-none.example.": {"v=spf1 include:absent.example"},
397 "include-permerror.example.": {"v=spf1 include:permerror.example"},
398 "permerror.example.": {"v=spf1 a:%%"},
399 "no-mx.example.": {"v=spf1 mx -all"},
400 "many-mx.example.": {"v=spf1 mx -all"},
401 "many-ptr.example.": {"v=spf1 ptr:many-mx.example ~all"},
402 "expl.example.": {"v=spf1 ip4:10.0.1.1 -ip4:10.0.1.2 ?all exp=details.expl.example"},
403 "details.expl.example.": {"your ip %{i} is not allowed"},
404 "expl-multi.example.": {"v=spf1 ip4:10.0.1.1 -ip4:10.0.1.2 ~all exp=details-multi.expl.example"},
405 "details-multi.expl.example.": {"your ip ", "%{i} is not allowed"},
406 },
407 A: map[string][]string{
408 "mail.mox.example.": {"10.0.0.1"},
409 "mx1.many-mx.example.": {"10.0.1.1"},
410 "mx2.many-mx.example.": {"10.0.1.2"},
411 "mx3.many-mx.example.": {"10.0.1.3"},
412 "mx4.many-mx.example.": {"10.0.1.4"},
413 "mx5.many-mx.example.": {"10.0.1.5"},
414 "mx6.many-mx.example.": {"10.0.1.6"},
415 "mx7.many-mx.example.": {"10.0.1.7"},
416 "mx8.many-mx.example.": {"10.0.1.8"},
417 "mx9.many-mx.example.": {"10.0.1.9"},
418 "mx10.many-mx.example.": {"10.0.1.10"},
419 "mx11.many-mx.example.": {"10.0.1.11"},
420 },
421 AAAA: map[string][]string{
422 "mail.mox.example.": {"2001:db8::1"},
423 },
424 MX: map[string][]*net.MX{
425 "no-mx.example.": {{Host: ".", Pref: 10}},
426 "many-mx.example.": {
427 {Host: "mx1.many-mx.example.", Pref: 1},
428 {Host: "mx2.many-mx.example.", Pref: 2},
429 {Host: "mx3.many-mx.example.", Pref: 3},
430 {Host: "mx4.many-mx.example.", Pref: 4},
431 {Host: "mx5.many-mx.example.", Pref: 5},
432 {Host: "mx6.many-mx.example.", Pref: 6},
433 {Host: "mx7.many-mx.example.", Pref: 7},
434 {Host: "mx8.many-mx.example.", Pref: 8},
435 {Host: "mx9.many-mx.example.", Pref: 9},
436 {Host: "mx10.many-mx.example.", Pref: 10},
437 {Host: "mx11.many-mx.example.", Pref: 11},
438 },
439 },
440 PTR: map[string][]string{
441 "2001:db8::1": {"mail.mox.example."},
442 "10.0.1.1": {"mx1.many-mx.example.", "mx2.many-mx.example.", "mx3.many-mx.example.", "mx4.many-mx.example.", "mx5.many-mx.example.", "mx6.many-mx.example.", "mx7.many-mx.example.", "mx8.many-mx.example.", "mx9.many-mx.example.", "mx10.many-mx.example.", "mx11.many-mx.example."},
443 },
444 Fail: map[dns.Mockreq]struct{}{
445 {Type: "txt", Name: "temperror.example."}: {},
446 },
447 }
448
449 // IPv6 remote IP.
450 test(r, Args{RemoteIP: net.ParseIP("2001:db8::1"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "mox.example"}}, StatusPass, "", "", nil)
451 test(r, Args{RemoteIP: net.ParseIP("2001:fa11::1"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "mox.example"}}, StatusFail, "", "", nil)
452
453 // Use EHLO identity.
454 test(r, Args{RemoteIP: net.ParseIP("2001:db8::1"), HelloDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "mox.example"}}}, StatusPass, "", "", nil)
455 test(r, Args{RemoteIP: net.ParseIP("2001:db8::1"), HelloDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "mail.mox.example"}}}, StatusNone, "", "", ErrNoRecord)
456
457 // Too many void lookups.
458 test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "void.example"}}, StatusPass, "", "", nil) // IP found after 2 void lookups, but before 3rd.
459 test(r, Args{RemoteIP: net.ParseIP("1.1.1.1"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "void.example"}}, StatusPermerror, "", "", ErrTooManyVoidLookups) // IP not found, not doing 3rd lookup.
460
461 // Too many DNS requests.
462 test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "loop.example"}}, StatusPermerror, "", "", ErrTooManyDNSRequests) // Self-referencing record, will abort after 10 includes.
463
464 // a:other where other does not exist.
465 test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "a-unknown.example"}}, StatusNeutral, "", "", nil)
466
467 // Expand with an invalid macro.
468 test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "include-bad-expand.example"}}, StatusPermerror, "", "", ErrMacroSyntax)
469 test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "exists-bad-expand.example"}}, StatusPermerror, "", "", ErrMacroSyntax)
470 test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "redir-bad-expand.example"}}, StatusPermerror, "", "", ErrMacroSyntax)
471
472 // Expand with invalid character (because macros are not expanded).
473 test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "a-bad-expand.example"}}, StatusPermerror, "", "", ErrName)
474 test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "mx-bad-expand.example"}}, StatusPermerror, "", "", ErrName)
475 test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "ptr-bad-expand.example"}}, StatusPermerror, "", "", ErrName)
476
477 // Include with varying results.
478 test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "include-temperror.example"}}, StatusTemperror, "", "", ErrDNS)
479 test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "include-none.example"}}, StatusPermerror, "", "", ErrNoRecord)
480 test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "include-permerror.example"}}, StatusPermerror, "", "", ErrName)
481
482 // MX with explicit "." for "no mail".
483 test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "no-mx.example"}}, StatusFail, "", "", nil)
484
485 // MX names beyond 10th entry result in Permerror.
486 test(r, Args{RemoteIP: net.ParseIP("10.0.1.1"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "many-mx.example"}}, StatusPass, "", "", nil)
487 test(r, Args{RemoteIP: net.ParseIP("10.0.1.10"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "many-mx.example"}}, StatusPass, "", "", nil)
488 test(r, Args{RemoteIP: net.ParseIP("10.0.1.11"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "many-mx.example"}}, StatusPermerror, "", "", ErrTooManyDNSRequests)
489 test(r, Args{RemoteIP: net.ParseIP("10.0.1.254"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "many-mx.example"}}, StatusPermerror, "", "", ErrTooManyDNSRequests)
490
491 // PTR names beyond 10th entry are ignored.
492 test(r, Args{RemoteIP: net.ParseIP("10.0.1.1"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "many-ptr.example"}}, StatusPass, "", "", nil)
493 test(r, Args{RemoteIP: net.ParseIP("10.0.1.2"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "many-ptr.example"}}, StatusSoftfail, "", "", nil)
494
495 // Explanation from txt records.
496 test(r, Args{RemoteIP: net.ParseIP("10.0.1.1"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "expl.example"}}, StatusPass, "", "", nil)
497 test(r, Args{RemoteIP: net.ParseIP("10.0.1.2"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "expl.example"}}, StatusFail, "", "your ip 10.0.1.2 is not allowed", nil)
498 test(r, Args{RemoteIP: net.ParseIP("10.0.1.3"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "expl.example"}}, StatusNeutral, "", "", nil)
499 test(r, Args{RemoteIP: net.ParseIP("10.0.1.2"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "expl-multi.example"}}, StatusFail, "", "your ip 10.0.1.2 is not allowed", nil)
500
501 // Verify with IP EHLO.
502 test(r, Args{RemoteIP: net.ParseIP("2001:db8::1"), HelloDomain: dns.IPDomain{IP: net.ParseIP("::1")}}, StatusNone, "", "", nil)
503}
504
505func TestEvaluate(t *testing.T) {
506 record := &Record{}
507 resolver := dns.MockResolver{}
508 args := Args{}
509 status, _, _, _ := Evaluate(context.Background(), record, resolver, args)
510 if status != StatusNone {
511 t.Fatalf("got status %q, expected none", status)
512 }
513
514 args = Args{
515 HelloDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "test.example"}},
516 }
517 status, mechanism, _, err := Evaluate(context.Background(), record, resolver, args)
518 if status != StatusNeutral || mechanism != "default" || err != nil {
519 t.Fatalf("got status %q, mechanism %q, err %v, expected neutral, default, no error", status, mechanism, err)
520 }
521}
522