9 "github.com/mjl-/mox/dkim"
10 "github.com/mjl-/mox/dns"
11 "github.com/mjl-/mox/spf"
14func TestLookup(t *testing.T) {
15 resolver := dns.MockResolver{
16 TXT: map[string][]string{
17 "_dmarc.simple.example.": {"v=DMARC1; p=none;"},
18 "_dmarc.one.example.": {"v=DMARC1; p=none;", "other"},
19 "_dmarc.temperror.example.": {"v=DMARC1; p=none;"},
20 "_dmarc.multiple.example.": {"v=DMARC1; p=none;", "v=DMARC1; p=none;"},
21 "_dmarc.malformed.example.": {"v=DMARC1; p=none; bogus;"},
22 "_dmarc.example.com.": {"v=DMARC1; p=none;"},
25 "txt _dmarc.temperror.example.",
29 test := func(d string, expStatus Status, expDomain string, expRecord *Record, expErr error) {
32 status, dom, record, _, _, err := Lookup(context.Background(), resolver, dns.Domain{ASCII: d})
33 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
34 t.Fatalf("got err %#v, expected %#v", err, expErr)
36 expd := dns.Domain{ASCII: expDomain}
37 if status != expStatus || dom != expd || !reflect.DeepEqual(record, expRecord) {
38 t.Fatalf("got status %v, dom %v, record %#v, expected %v %v %#v", status, dom, record, expStatus, expDomain, expRecord)
44 test("simple.example", StatusNone, "simple.example", &r, nil)
45 test("one.example", StatusNone, "one.example", &r, nil)
46 test("absent.example", StatusNone, "absent.example", nil, ErrNoRecord)
47 test("multiple.example", StatusNone, "multiple.example", nil, ErrMultipleRecords)
48 test("malformed.example", StatusPermerror, "malformed.example", nil, ErrSyntax)
49 test("temperror.example", StatusTemperror, "temperror.example", nil, ErrDNS)
50 test("sub.example.com", StatusNone, "example.com", &r, nil) // Policy published at organizational domain, public suffix.
53func TestLookupExternalReportsAccepted(t *testing.T) {
54 resolver := dns.MockResolver{
55 TXT: map[string][]string{
56 "example.com._report._dmarc.simple.example.": {"v=DMARC1"},
57 "example.com._report._dmarc.simple2.example.": {"v=DMARC1;"},
58 "example.com._report._dmarc.one.example.": {"v=DMARC1; p=none;", "other"},
59 "example.com._report._dmarc.temperror.example.": {"v=DMARC1; p=none;"},
60 "example.com._report._dmarc.multiple.example.": {"v=DMARC1; p=none;", "v=DMARC1"},
61 "example.com._report._dmarc.malformed.example.": {"v=DMARC1; p=none; bogus;"},
64 "txt example.com._report._dmarc.temperror.example.",
68 test := func(dom, extdom string, expStatus Status, expAccepts bool, expErr error) {
71 accepts, status, _, _, _, err := LookupExternalReportsAccepted(context.Background(), resolver, dns.Domain{ASCII: dom}, dns.Domain{ASCII: extdom})
72 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
73 t.Fatalf("got err %#v, expected %#v", err, expErr)
75 if status != expStatus || accepts != expAccepts {
76 t.Fatalf("got status %s, accepts %v, expected %v, %v", status, accepts, expStatus, expAccepts)
82 test("example.com", "simple.example", StatusNone, true, nil)
83 test("example.org", "simple.example", StatusNone, false, ErrNoRecord)
84 test("example.com", "simple2.example", StatusNone, true, nil)
85 test("example.com", "one.example", StatusNone, true, nil)
86 test("example.com", "absent.example", StatusNone, false, ErrNoRecord)
87 test("example.com", "multiple.example", StatusNone, true, nil)
88 test("example.com", "malformed.example", StatusPermerror, false, ErrSyntax)
89 test("example.com", "temperror.example", StatusTemperror, false, ErrDNS)
92func TestVerify(t *testing.T) {
93 resolver := dns.MockResolver{
94 TXT: map[string][]string{
95 "_dmarc.reject.example.": {"v=DMARC1; p=reject"},
96 "_dmarc.strict.example.": {"v=DMARC1; p=reject; adkim=s; aspf=s"},
97 "_dmarc.test.example.": {"v=DMARC1; p=reject; pct=0"},
98 "_dmarc.subnone.example.": {"v=DMARC1; p=reject; sp=none"},
99 "_dmarc.none.example.": {"v=DMARC1; p=none"},
100 "_dmarc.malformed.example.": {"v=DMARC1; p=none; bogus"},
101 "_dmarc.example.com.": {"v=DMARC1; p=reject"},
104 "txt _dmarc.temperror.example.",
108 equalResult := func(got, exp Result) bool {
109 if reflect.DeepEqual(got, exp) {
112 if got.Err != nil && exp.Err != nil && (got.Err == exp.Err || errors.Is(got.Err, exp.Err)) {
115 return reflect.DeepEqual(got, exp)
120 test := func(fromDom string, dkimResults []dkim.Result, spfResult spf.Status, spfIdentity *dns.Domain, expUseResult bool, expResult Result) {
123 from, err := dns.ParseDomain(fromDom)
125 t.Fatalf("parsing domain: %v", err)
127 useResult, result := Verify(context.Background(), resolver, from, dkimResults, spfResult, spfIdentity, true)
128 if useResult != expUseResult || !equalResult(result, expResult) {
129 t.Fatalf("verify: got useResult %v, result %#v, expected %v %#v", useResult, result, expUseResult, expResult)
133 // Basic case, reject policy and no dkim or spf results.
134 reject := DefaultRecord
135 reject.Policy = PolicyReject
136 test("reject.example",
140 true, Result{true, StatusFail, false, false, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
143 // Accept with spf pass.
144 test("reject.example",
147 &dns.Domain{ASCII: "sub.reject.example"},
148 true, Result{false, StatusPass, true, false, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
151 // Accept with dkim pass.
152 test("reject.example",
155 Status: dkim.StatusPass,
156 Sig: &dkim.Sig{ // Just the minimum fields needed.
157 Domain: dns.Domain{ASCII: "sub.reject.example"},
159 Record: &dkim.Record{},
163 &dns.Domain{ASCII: "reject.example"},
164 true, Result{false, StatusPass, false, true, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
167 // Reject due to spf and dkim "strict".
168 strict := DefaultRecord
169 strict.Policy = PolicyReject
170 strict.ADKIM = AlignStrict
171 strict.ASPF = AlignStrict
172 test("strict.example",
175 Status: dkim.StatusPass,
176 Sig: &dkim.Sig{ // Just the minimum fields needed.
177 Domain: dns.Domain{ASCII: "sub.strict.example"},
179 Record: &dkim.Record{},
183 &dns.Domain{ASCII: "sub.strict.example"},
184 true, Result{true, StatusFail, false, false, dns.Domain{ASCII: "strict.example"}, &strict, false, nil},
187 // No dmarc policy, nothing to say.
188 test("absent.example",
192 false, Result{false, StatusNone, false, false, dns.Domain{ASCII: "absent.example"}, nil, false, ErrNoRecord},
195 // No dmarc policy, spf pass does nothing.
196 test("absent.example",
199 &dns.Domain{ASCII: "absent.example"},
200 false, Result{false, StatusNone, false, false, dns.Domain{ASCII: "absent.example"}, nil, false, ErrNoRecord},
203 none := DefaultRecord
204 none.Policy = PolicyNone
205 // Policy none results in no reject.
209 &dns.Domain{ASCII: "none.example"},
210 true, Result{false, StatusPass, true, false, dns.Domain{ASCII: "none.example"}, &none, false, nil},
213 // No actual reject due to pct=0.
214 testr := DefaultRecord
215 testr.Policy = PolicyReject
221 false, Result{true, StatusFail, false, false, dns.Domain{ASCII: "test.example"}, &testr, false, nil},
224 // No reject if subdomain has "none" policy.
226 sub.Policy = PolicyReject
227 sub.SubdomainPolicy = PolicyNone
228 test("sub.subnone.example",
231 &dns.Domain{ASCII: "sub.subnone.example"},
232 true, Result{false, StatusFail, false, false, dns.Domain{ASCII: "subnone.example"}, &sub, false, nil},
235 // No reject if spf temperror and no other pass.
236 test("reject.example",
239 &dns.Domain{ASCII: "mail.reject.example"},
240 true, Result{false, StatusTemperror, false, false, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
243 // No reject if dkim temperror and no other pass.
244 test("reject.example",
247 Status: dkim.StatusTemperror,
248 Sig: &dkim.Sig{ // Just the minimum fields needed.
249 Domain: dns.Domain{ASCII: "sub.reject.example"},
251 Record: &dkim.Record{},
256 true, Result{false, StatusTemperror, false, false, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
259 // No reject if spf temperror but still dkim pass.
260 test("reject.example",
263 Status: dkim.StatusPass,
264 Sig: &dkim.Sig{ // Just the minimum fields needed.
265 Domain: dns.Domain{ASCII: "sub.reject.example"},
267 Record: &dkim.Record{},
271 &dns.Domain{ASCII: "mail.reject.example"},
272 true, Result{false, StatusPass, false, true, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
275 // No reject if dkim temperror but still spf pass.
276 test("reject.example",
279 Status: dkim.StatusTemperror,
280 Sig: &dkim.Sig{ // Just the minimum fields needed.
281 Domain: dns.Domain{ASCII: "sub.reject.example"},
283 Record: &dkim.Record{},
287 &dns.Domain{ASCII: "mail.reject.example"},
288 true, Result{false, StatusPass, true, false, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
291 // Bad DMARC record results in permerror without reject.
292 test("malformed.example",
296 false, Result{false, StatusPermerror, false, false, dns.Domain{ASCII: "malformed.example"}, nil, false, ErrSyntax},
299 // DKIM domain that is higher-level than organizational can not result in a pass.
../rfc/7489:525
303 Status: dkim.StatusPass,
304 Sig: &dkim.Sig{ // Just the minimum fields needed.
305 Domain: dns.Domain{ASCII: "com"},
307 Record: &dkim.Record{},
312 true, Result{true, StatusFail, false, false, dns.Domain{ASCII: "example.com"}, &reject, false, nil},