package spf import ( "context" "errors" "fmt" "net" "reflect" "testing" "time" "github.com/mjl-/mox/dns" "github.com/mjl-/mox/smtp" ) func TestLookup(t *testing.T) { resolver := dns.MockResolver{ TXT: map[string][]string{ "temperror.example.": {"irrelevant"}, "malformed.example.": {"v=spf1 !"}, "multiple.example.": {"v=spf1", "v=spf1"}, "nonspf.example.": {"something else"}, "ok.example.": {"v=spf1"}, }, Fail: map[dns.Mockreq]struct{}{ {Type: "txt", Name: "temperror.example."}: {}, }, } test := func(domain string, expStatus Status, expRecord *Record, expErr error) { t.Helper() d := dns.Domain{ASCII: domain} status, txt, record, err := Lookup(context.Background(), resolver, d) if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) { t.Fatalf("got err %v, expected err %v", err, expErr) } if err != nil { return } if status != expStatus || txt == "" || !reflect.DeepEqual(record, expRecord) { t.Fatalf("got status %q, txt %q, record %#v, expected %q, ..., %#v", status, txt, record, expStatus, expRecord) } } test("..", StatusNone, nil, ErrName) test("absent.example", StatusNone, nil, ErrNoRecord) test("temperror.example", StatusTemperror, nil, ErrDNS) test("malformed.example", StatusPermerror, nil, ErrRecordSyntax) test("multiple.example", StatusPermerror, nil, ErrMultipleRecords) test("nonspf.example", StatusNone, nil, ErrNoRecord) test("ok.example", StatusNone, &Record{Version: "spf1"}, nil) } func TestExpand(t *testing.T) { defArgs := Args{ senderLocalpart: "strong-bad", senderDomain: dns.Domain{ASCII: "email.example.com"}, domain: dns.Domain{ASCII: "email.example.com"}, MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "mox.example"}, HelloDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "mx.mox.example"}}, LocalIP: net.ParseIP("10.10.10.10"), LocalHostname: dns.Domain{ASCII: "self.example"}, } resolver := dns.MockResolver{ PTR: map[string][]string{ "10.0.0.1": {"other.example.", "sub.mx.mox.example.", "mx.mox.example."}, "10.0.0.2": {"other.example.", "sub.mx.mox.example.", "mx.mox.example."}, "10.0.0.3": {"other.example.", "sub.mx.mox.example.", "mx.mox.example."}, }, A: map[string][]string{ "mx.mox.example.": {"10.0.0.1"}, "sub.mx.mox.example.": {"10.0.0.2"}, "other.example.": {"10.0.0.3"}, }, } mustParseIP := func(s string) net.IP { ip := net.ParseIP(s) if ip == nil { t.Fatalf("bad ip %q", s) } return ip } ctx := context.Background() // Examples from ../rfc/7208:1777 test := func(dns bool, macro, ip, exp string) { t.Helper() args := defArgs args.dnsRequests = new(int) args.voidLookups = new(int) if ip != "" { args.RemoteIP = mustParseIP(ip) } r, err := expandDomainSpec(ctx, resolver, macro, args, dns) if (err == nil) != (exp != "") { t.Fatalf("got err %v, expected expansion %q, for macro %q", err, exp, macro) } if r != exp { t.Fatalf("got expansion %q, expected %q, for macro %q", r, exp, macro) } } testDNS := func(macro, ip, exp string) { t.Helper() test(true, macro, ip, exp) } testExpl := func(macro, ip, exp string) { t.Helper() test(false, macro, ip, exp) } testDNS("%{s}", "", "strong-bad@email.example.com") testDNS("%{o}", "", "email.example.com") testDNS("%{d}", "", "email.example.com") testDNS("%{d4}", "", "email.example.com") testDNS("%{d3}", "", "email.example.com") testDNS("%{d2}", "", "example.com") testDNS("%{d1}", "", "com") testDNS("%{dr}", "", "com.example.email") testDNS("%{d2r}", "", "example.email") testDNS("%{l}", "", "strong-bad") testDNS("%{l-}", "", "strong.bad") testDNS("%{lr}", "", "strong-bad") testDNS("%{lr-}", "", "bad.strong") testDNS("%{l1r-}", "", "strong") testDNS("%", "", "") testDNS("%b", "", "") testDNS("%{", "", "") testDNS("%{s", "", "") testDNS("%{s1", "", "") testDNS("%{s0}", "", "") testDNS("%{s1r", "", "") testDNS("%{s99999999999999999999999999999999999999999999999999999999999999999999999}", "", "") testDNS("%{ir}.%{v}._spf.%{d2}", "192.0.2.3", "3.2.0.192.in-addr._spf.example.com") testDNS("%{lr-}.lp._spf.%{d2}", "192.0.2.3", "bad.strong.lp._spf.example.com") testDNS("%{lr-}.lp.%{ir}.%{v}._spf.%{d2}", "192.0.2.3", "bad.strong.lp.3.2.0.192.in-addr._spf.example.com") testDNS("%{ir}.%{v}.%{l1r-}.lp._spf.%{d2}", "192.0.2.3", "3.2.0.192.in-addr.strong.lp._spf.example.com") testDNS("%{d2}.trusted-domains.example.net", "192.0.2.3", "example.com.trusted-domains.example.net") 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") // Additional. testDNS("%%%-%_", "10.0.0.1", "%%20 ") testDNS("%{p}", "10.0.0.1", "mx.mox.example.") testDNS("%{p}", "10.0.0.2", "sub.mx.mox.example.") testDNS("%{p}", "10.0.0.3", "other.example.") testDNS("%{p}", "10.0.0.4", "unknown") testExpl("%{c}", "10.0.0.1", "10.10.10.10") testExpl("%{r}", "10.0.0.1", "self.example") orig := timeNow now := orig() defer func() { timeNow = orig }() timeNow = func() time.Time { return now } testExpl("%{t}", "10.0.0.1", fmt.Sprintf("%d", now.Unix())) // DNS name can be 253 bytes long, each label can be 63 bytes. xlabel := make([]byte, 62) for i := range xlabel { xlabel[i] = 'a' } label := string(xlabel) name := label + "." + label + "." + label + "." + label // 4*62+3 = 251 testDNS("x."+name, "10.0.0.1", "x."+name) // Still fits. testDNS("xx."+name, "10.0.0.1", name) // Does not fit, "xx." is truncated to make it fit. testDNS("%{p}..", "10.0.0.1", "") testDNS("%{h}", "10.0.0.1", "mx.mox.example") } func TestVerify(t *testing.T) { xip := func(s string) net.IP { ip := net.ParseIP(s) if ip == nil { t.Fatalf("bad ip: %q", s) } return ip } iplist := func(l ...string) []net.IP { r := make([]net.IP, len(l)) for i, s := range l { r[i] = xip(s) } return r } // ../rfc/7208:2975 Appendix A. Extended Examples r := dns.MockResolver{ PTR: map[string][]string{ "192.0.2.10": {"example.com."}, "192.0.2.11": {"example.com."}, "192.0.2.65": {"amy.example.com."}, "192.0.2.66": {"bob.example.com."}, "192.0.2.129": {"mail-a.example.com."}, "192.0.2.130": {"mail-b.example.com."}, "192.0.2.140": {"mail-c.example.org."}, "10.0.0.4": {"bob.example.com."}, }, TXT: map[string][]string{ // Additional from DNSBL, ../rfc/7208:3115 "mobile-users._spf.example.com.": {"v=spf1 exists:%{l1r+}.%{d}"}, "remote-users._spf.example.com.": {"v=spf1 exists:%{ir}.%{l1r+}.%{d}"}, // Additional ../rfc/7208:3171 "ip4._spf.example.com.": {"v=spf1 -ip4:192.0.2.0/24 +all"}, "ptr._spf.example.com.": {"v=spf1 -ptr:example.com +all"}, // ../rfc/7208-eid6216 ../rfc/7208:3172 // Additional tests "_spf.example.com.": {"v=spf1 include:_netblock.example.com -all"}, "_netblock.example.com.": {"v=spf1 ip4:192.0.2.128/28 -all"}, }, A: map[string][]string{ "example.com.": {"192.0.2.10", "192.0.2.11"}, "amy.example.com.": {"192.0.2.65"}, "bob.example.com.": {"192.0.2.66"}, "mail-a.example.com.": {"192.0.2.129"}, "mail-b.example.com.": {"192.0.2.130"}, "mail-c.example.org.": {"192.0.2.140"}, // Additional from DNSBL, ../rfc/7208:3115 "mary.mobile-users._spf.example.com.": {"127.0.0.2"}, "fred.mobile-users._spf.example.com.": {"127.0.0.2"}, "15.15.168.192.joel.remote-users._spf.example.com.": {"127.0.0.2"}, "16.15.168.192.joel.remote-users._spf.example.com.": {"127.0.0.2"}, }, AAAA: map[string][]string{}, MX: map[string][]*net.MX{ "example.com.": { {Host: "mail-a.example.com.", Pref: 10}, {Host: "mail-b.example.com.", Pref: 20}, }, "example.org.": { {Host: "mail-c.example.org.", Pref: 10}, }, }, Fail: map[dns.Mockreq]struct{}{}, } ctx := context.Background() verify := func(ip net.IP, localpart string, status Status) { t.Helper() args := Args{ MailFromLocalpart: smtp.Localpart(localpart), MailFromDomain: dns.Domain{ASCII: "example.com"}, RemoteIP: ip, LocalIP: xip("127.0.0.1"), LocalHostname: dns.Domain{ASCII: "localhost"}, } received, _, _, err := Verify(ctx, r, args) if received.Result != status { t.Fatalf("got status %q, expected %q, for ip %q (err %v)", received.Result, status, ip, err) } if err != nil { t.Fatalf("unexpected error: %s", err) } } test := func(txt string, ips []net.IP, only bool) { r.TXT["example.com."] = []string{txt} seen := map[string]struct{}{} for _, ip := range ips { verify(ip, "", StatusPass) seen[ip.String()] = struct{}{} } if !only { return } for ip := range r.PTR { if _, ok := seen[ip]; ok { continue } verify(xip(ip), "", StatusFail) } } // ../rfc/7208:3031 A.1. Simple Examples test("v=spf1 +all", iplist("192.0.2.129", "1.2.3.4"), false) test("v=spf1 a -all", iplist("192.0.2.10", "192.0.2.11"), true) test("v=spf1 a:example.org -all", iplist(), true) test("v=spf1 mx -all", iplist("192.0.2.129", "192.0.2.130"), true) test("v=spf1 mx:example.org -all", iplist("192.0.2.140"), true) test("v=spf1 mx mx:example.org -all", iplist("192.0.2.129", "192.0.2.130", "192.0.2.140"), true) test("v=spf1 mx/30 mx:example.org/30 -all", iplist("192.0.2.129", "192.0.2.130", "192.0.2.140"), true) 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) test("v=spf1 ip4:192.0.2.128/28 -all", iplist("192.0.2.129", "192.0.2.130", "192.0.2.140"), true) // Additional tests test("v=spf1 redirect=_spf.example.com", iplist("192.0.2.129", "192.0.2.130", "192.0.2.140"), true) // Additional from DNSBL, ../rfc/7208:3115 r.TXT["example.com."] = []string{"v=spf1 mx include:mobile-users._spf.%{d} include:remote-users._spf.%{d} -all"} verify(xip("1.2.3.4"), "mary", StatusPass) verify(xip("1.2.3.4"), "fred", StatusPass) verify(xip("1.2.3.4"), "fred+wildcard", StatusPass) verify(xip("1.2.3.4"), "joel", StatusFail) verify(xip("1.2.3.4"), "other", StatusFail) verify(xip("192.168.15.15"), "joel", StatusPass) verify(xip("192.168.15.16"), "joel", StatusPass) verify(xip("192.168.15.17"), "joel", StatusFail) verify(xip("192.168.15.17"), "other", StatusFail) // Additional ../rfc/7208:3171 r.TXT["example.com."] = []string{"v=spf1 -include:ip4._spf.%{d} -include:ptr._spf.%{d} +all"} r.PTR["192.0.2.1"] = []string{"a.example.com."} r.PTR["192.0.0.1"] = []string{"b.example.com."} r.A["a.example.com."] = []string{"192.0.2.1"} r.A["b.example.com."] = []string{"192.0.0.1"} verify(xip("192.0.2.1"), "", StatusPass) // IP in range and PTR matches. verify(xip("192.0.2.2"), "", StatusFail) // IP in range but no PTR match. verify(xip("192.0.0.1"), "", StatusFail) // PTR match but IP not in range. verify(xip("192.0.0.2"), "", StatusFail) // No PTR match and IP not in range. } // ../rfc/7208:3093 func TestVerifyMultipleDomain(t *testing.T) { resolver := dns.MockResolver{ TXT: map[string][]string{ "example.org.": {"v=spf1 include:example.com include:example.net -all"}, "la.example.org.": {"v=spf1 redirect=example.org"}, "example.com.": {"v=spf1 ip4:10.0.0.1 -all"}, "example.net.": {"v=spf1 ip4:10.0.0.2 -all"}, }, } verify := func(domain, ip string, status Status) { t.Helper() args := Args{ MailFromDomain: dns.Domain{ASCII: domain}, RemoteIP: net.ParseIP(ip), LocalIP: net.ParseIP("127.0.0.1"), LocalHostname: dns.Domain{ASCII: "localhost"}, } received, _, _, err := Verify(context.Background(), resolver, args) if err != nil { t.Fatalf("unexpected error: %s", err) } if received.Result != status { t.Fatalf("got status %q, expected %q, for ip %q", received.Result, status, ip) } } verify("example.com", "10.0.0.1", StatusPass) verify("example.net", "10.0.0.2", StatusPass) verify("example.com", "10.0.0.2", StatusFail) verify("example.net", "10.0.0.1", StatusFail) verify("example.org", "10.0.0.1", StatusPass) verify("example.org", "10.0.0.2", StatusPass) verify("example.org", "10.0.0.3", StatusFail) verify("la.example.org", "10.0.0.1", StatusPass) verify("la.example.org", "10.0.0.2", StatusPass) verify("la.example.org", "10.0.0.3", StatusFail) } func TestVerifyScenarios(t *testing.T) { test := func(resolver dns.Resolver, args Args, expStatus Status, expDomain string, expExpl string, expErr error) { t.Helper() recv, d, expl, err := Verify(context.Background(), resolver, args) if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) { t.Fatalf("got err %v, expected %v", err, expErr) } if expStatus != recv.Result || expDomain != "" && d.ASCII != expDomain || expExpl != "" && expl != expExpl { t.Fatalf("got status %q, domain %q, expl %q, err %v", recv.Result, d, expl, err) } } r := dns.MockResolver{ TXT: map[string][]string{ "mox.example.": {"v=spf1 ip6:2001:db8::0/64 -all"}, "void.example.": {"v=spf1 exists:absent1.example exists:absent2.example ip4:1.2.3.4 exists:absent3.example -all"}, "loop.example.": {"v=spf1 include:loop.example -all"}, "a-unknown.example.": {"v=spf1 a:absent.example"}, "include-bad-expand.example.": {"v=spf1 include:%{c}"}, // macro 'c' only valid while expanding for "exp". "exists-bad-expand.example.": {"v=spf1 exists:%{c}"}, // macro 'c' only valid while expanding for "exp". "redir-bad-expand.example.": {"v=spf1 redirect=%{c}"}, // macro 'c' only valid while expanding for "exp". "a-bad-expand.example.": {"v=spf1 a:%{c}"}, // macro 'c' only valid while expanding for "exp". "mx-bad-expand.example.": {"v=spf1 mx:%{c}"}, // macro 'c' only valid while expanding for "exp". "ptr-bad-expand.example.": {"v=spf1 ptr:%{c}"}, // macro 'c' only valid while expanding for "exp". "include-temperror.example.": {"v=spf1 include:temperror.example"}, "include-none.example.": {"v=spf1 include:absent.example"}, "include-permerror.example.": {"v=spf1 include:permerror.example"}, "permerror.example.": {"v=spf1 a:%%"}, "no-mx.example.": {"v=spf1 mx -all"}, "many-mx.example.": {"v=spf1 mx -all"}, "many-ptr.example.": {"v=spf1 ptr:many-mx.example ~all"}, "expl.example.": {"v=spf1 ip4:10.0.1.1 -ip4:10.0.1.2 ?all exp=details.expl.example"}, "details.expl.example.": {"your ip %{i} is not allowed"}, "expl-multi.example.": {"v=spf1 ip4:10.0.1.1 -ip4:10.0.1.2 ~all exp=details-multi.expl.example"}, "details-multi.expl.example.": {"your ip ", "%{i} is not allowed"}, }, A: map[string][]string{ "mail.mox.example.": {"10.0.0.1"}, "mx1.many-mx.example.": {"10.0.1.1"}, "mx2.many-mx.example.": {"10.0.1.2"}, "mx3.many-mx.example.": {"10.0.1.3"}, "mx4.many-mx.example.": {"10.0.1.4"}, "mx5.many-mx.example.": {"10.0.1.5"}, "mx6.many-mx.example.": {"10.0.1.6"}, "mx7.many-mx.example.": {"10.0.1.7"}, "mx8.many-mx.example.": {"10.0.1.8"}, "mx9.many-mx.example.": {"10.0.1.9"}, "mx10.many-mx.example.": {"10.0.1.10"}, "mx11.many-mx.example.": {"10.0.1.11"}, }, AAAA: map[string][]string{ "mail.mox.example.": {"2001:db8::1"}, }, MX: map[string][]*net.MX{ "no-mx.example.": {{Host: ".", Pref: 10}}, "many-mx.example.": { {Host: "mx1.many-mx.example.", Pref: 1}, {Host: "mx2.many-mx.example.", Pref: 2}, {Host: "mx3.many-mx.example.", Pref: 3}, {Host: "mx4.many-mx.example.", Pref: 4}, {Host: "mx5.many-mx.example.", Pref: 5}, {Host: "mx6.many-mx.example.", Pref: 6}, {Host: "mx7.many-mx.example.", Pref: 7}, {Host: "mx8.many-mx.example.", Pref: 8}, {Host: "mx9.many-mx.example.", Pref: 9}, {Host: "mx10.many-mx.example.", Pref: 10}, {Host: "mx11.many-mx.example.", Pref: 11}, }, }, PTR: map[string][]string{ "2001:db8::1": {"mail.mox.example."}, "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."}, }, Fail: map[dns.Mockreq]struct{}{ {Type: "txt", Name: "temperror.example."}: {}, }, } // IPv6 remote IP. test(r, Args{RemoteIP: net.ParseIP("2001:db8::1"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "mox.example"}}, StatusPass, "", "", nil) test(r, Args{RemoteIP: net.ParseIP("2001:fa11::1"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "mox.example"}}, StatusFail, "", "", nil) // Use EHLO identity. test(r, Args{RemoteIP: net.ParseIP("2001:db8::1"), HelloDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "mox.example"}}}, StatusPass, "", "", nil) test(r, Args{RemoteIP: net.ParseIP("2001:db8::1"), HelloDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "mail.mox.example"}}}, StatusNone, "", "", ErrNoRecord) // Too many void lookups. 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. 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. // Too many DNS requests. 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. // a:other where other does not exist. test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "a-unknown.example"}}, StatusNeutral, "", "", nil) // Expand with an invalid macro. test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "include-bad-expand.example"}}, StatusPermerror, "", "", ErrMacroSyntax) test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "exists-bad-expand.example"}}, StatusPermerror, "", "", ErrMacroSyntax) test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "redir-bad-expand.example"}}, StatusPermerror, "", "", ErrMacroSyntax) // Expand with invalid character (because macros are not expanded). test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "a-bad-expand.example"}}, StatusPermerror, "", "", ErrName) test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "mx-bad-expand.example"}}, StatusPermerror, "", "", ErrName) test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "ptr-bad-expand.example"}}, StatusPermerror, "", "", ErrName) // Include with varying results. test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "include-temperror.example"}}, StatusTemperror, "", "", ErrDNS) test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "include-none.example"}}, StatusPermerror, "", "", ErrNoRecord) test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "include-permerror.example"}}, StatusPermerror, "", "", ErrName) // MX with explicit "." for "no mail". test(r, Args{RemoteIP: net.ParseIP("1.2.3.4"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "no-mx.example"}}, StatusFail, "", "", nil) // MX names beyond 10th entry result in Permerror. test(r, Args{RemoteIP: net.ParseIP("10.0.1.1"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "many-mx.example"}}, StatusPass, "", "", nil) test(r, Args{RemoteIP: net.ParseIP("10.0.1.10"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "many-mx.example"}}, StatusPass, "", "", nil) test(r, Args{RemoteIP: net.ParseIP("10.0.1.11"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "many-mx.example"}}, StatusPermerror, "", "", ErrTooManyDNSRequests) test(r, Args{RemoteIP: net.ParseIP("10.0.1.254"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "many-mx.example"}}, StatusPermerror, "", "", ErrTooManyDNSRequests) // PTR names beyond 10th entry are ignored. test(r, Args{RemoteIP: net.ParseIP("10.0.1.1"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "many-ptr.example"}}, StatusPass, "", "", nil) test(r, Args{RemoteIP: net.ParseIP("10.0.1.2"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "many-ptr.example"}}, StatusSoftfail, "", "", nil) // Explanation from txt records. test(r, Args{RemoteIP: net.ParseIP("10.0.1.1"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "expl.example"}}, StatusPass, "", "", nil) 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) test(r, Args{RemoteIP: net.ParseIP("10.0.1.3"), MailFromLocalpart: "x", MailFromDomain: dns.Domain{ASCII: "expl.example"}}, StatusNeutral, "", "", nil) 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) // Verify with IP EHLO. test(r, Args{RemoteIP: net.ParseIP("2001:db8::1"), HelloDomain: dns.IPDomain{IP: net.ParseIP("::1")}}, StatusNone, "", "", nil) } func TestEvaluate(t *testing.T) { record := &Record{} resolver := dns.MockResolver{} args := Args{} status, _, _, _ := Evaluate(context.Background(), record, resolver, args) if status != StatusNone { t.Fatalf("got status %q, expected none", status) } args = Args{ HelloDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "test.example"}}, } status, mechanism, _, err := Evaluate(context.Background(), record, resolver, args) if status != StatusNeutral || mechanism != "default" || err != nil { t.Fatalf("got status %q, mechanism %q, err %v, expected neutral, default, no error", status, mechanism, err) } }