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