9 "github.com/mjl-/mox/dkim"
10 "github.com/mjl-/mox/dns"
11 "github.com/mjl-/mox/mlog"
12 "github.com/mjl-/mox/spf"
15var pkglog = mlog.New("dmarc", nil)
17func TestLookup(t *testing.T) {
18 resolver := dns.MockResolver{
19 TXT: map[string][]string{
20 "_dmarc.simple.example.": {"v=DMARC1; p=none;"},
21 "_dmarc.one.example.": {"v=DMARC1; p=none;", "other"},
22 "_dmarc.temperror.example.": {"v=DMARC1; p=none;"},
23 "_dmarc.multiple.example.": {"v=DMARC1; p=none;", "v=DMARC1; p=none;"},
24 "_dmarc.malformed.example.": {"v=DMARC1; p=none; bogus;"},
25 "_dmarc.example.com.": {"v=DMARC1; p=none;"},
28 "txt _dmarc.temperror.example.",
32 test := func(d string, expStatus Status, expDomain string, expRecord *Record, expErr error) {
35 status, dom, record, _, _, err := Lookup(context.Background(), pkglog.Logger, resolver, dns.Domain{ASCII: d})
36 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
37 t.Fatalf("got err %#v, expected %#v", err, expErr)
39 expd := dns.Domain{ASCII: expDomain}
40 if status != expStatus || dom != expd || !reflect.DeepEqual(record, expRecord) {
41 t.Fatalf("got status %v, dom %v, record %#v, expected %v %v %#v", status, dom, record, expStatus, expDomain, expRecord)
47 test("simple.example", StatusNone, "simple.example", &r, nil)
48 test("one.example", StatusNone, "one.example", &r, nil)
49 test("absent.example", StatusNone, "absent.example", nil, ErrNoRecord)
50 test("multiple.example", StatusNone, "multiple.example", nil, ErrMultipleRecords)
51 test("malformed.example", StatusPermerror, "malformed.example", nil, ErrSyntax)
52 test("temperror.example", StatusTemperror, "temperror.example", nil, ErrDNS)
53 test("sub.example.com", StatusNone, "example.com", &r, nil) // Policy published at organizational domain, public suffix.
56func TestLookupExternalReportsAccepted(t *testing.T) {
57 resolver := dns.MockResolver{
58 TXT: map[string][]string{
59 "example.com._report._dmarc.simple.example.": {"v=DMARC1"},
60 "example.com._report._dmarc.simple2.example.": {"v=DMARC1;"},
61 "example.com._report._dmarc.one.example.": {"v=DMARC1; p=none;", "other"},
62 "example.com._report._dmarc.temperror.example.": {"v=DMARC1; p=none;"},
63 "example.com._report._dmarc.multiple.example.": {"v=DMARC1; p=none;", "v=DMARC1"},
64 "example.com._report._dmarc.malformed.example.": {"v=DMARC1; p=none; bogus;"},
67 "txt example.com._report._dmarc.temperror.example.",
71 test := func(dom, extdom string, expStatus Status, expAccepts bool, expErr error) {
74 accepts, status, _, _, _, err := LookupExternalReportsAccepted(context.Background(), pkglog.Logger, resolver, dns.Domain{ASCII: dom}, dns.Domain{ASCII: extdom})
75 if (err == nil) != (expErr == nil) || err != nil && !errors.Is(err, expErr) {
76 t.Fatalf("got err %#v, expected %#v", err, expErr)
78 if status != expStatus || accepts != expAccepts {
79 t.Fatalf("got status %s, accepts %v, expected %v, %v", status, accepts, expStatus, expAccepts)
85 test("example.com", "simple.example", StatusNone, true, nil)
86 test("example.org", "simple.example", StatusNone, false, ErrNoRecord)
87 test("example.com", "simple2.example", StatusNone, true, nil)
88 test("example.com", "one.example", StatusNone, true, nil)
89 test("example.com", "absent.example", StatusNone, false, ErrNoRecord)
90 test("example.com", "multiple.example", StatusNone, true, nil)
91 test("example.com", "malformed.example", StatusPermerror, false, ErrSyntax)
92 test("example.com", "temperror.example", StatusTemperror, false, ErrDNS)
95func TestVerify(t *testing.T) {
96 resolver := dns.MockResolver{
97 TXT: map[string][]string{
98 "_dmarc.reject.example.": {"v=DMARC1; p=reject"},
99 "_dmarc.strict.example.": {"v=DMARC1; p=reject; adkim=s; aspf=s"},
100 "_dmarc.test.example.": {"v=DMARC1; p=reject; pct=0"},
101 "_dmarc.subnone.example.": {"v=DMARC1; p=reject; sp=none"},
102 "_dmarc.none.example.": {"v=DMARC1; p=none"},
103 "_dmarc.malformed.example.": {"v=DMARC1; p=none; bogus"},
104 "_dmarc.example.com.": {"v=DMARC1; p=reject"},
107 "txt _dmarc.temperror.example.",
111 equalResult := func(got, exp Result) bool {
112 if reflect.DeepEqual(got, exp) {
115 if got.Err != nil && exp.Err != nil && (got.Err == exp.Err || errors.Is(got.Err, exp.Err)) {
118 return reflect.DeepEqual(got, exp)
123 test := func(fromDom string, dkimResults []dkim.Result, spfResult spf.Status, spfIdentity *dns.Domain, expUseResult bool, expResult Result) {
126 from, err := dns.ParseDomain(fromDom)
128 t.Fatalf("parsing domain: %v", err)
130 useResult, result := Verify(context.Background(), pkglog.Logger, resolver, from, dkimResults, spfResult, spfIdentity, true)
131 if useResult != expUseResult || !equalResult(result, expResult) {
132 t.Fatalf("verify: got useResult %v, result %#v, expected %v %#v", useResult, result, expUseResult, expResult)
136 // Basic case, reject policy and no dkim or spf results.
137 reject := DefaultRecord
138 reject.Policy = PolicyReject
139 test("reject.example",
143 true, Result{true, StatusFail, false, false, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
146 // Accept with spf pass.
147 test("reject.example",
150 &dns.Domain{ASCII: "sub.reject.example"},
151 true, Result{false, StatusPass, true, false, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
154 // Accept with dkim pass.
155 test("reject.example",
158 Status: dkim.StatusPass,
159 Sig: &dkim.Sig{ // Just the minimum fields needed.
160 Domain: dns.Domain{ASCII: "sub.reject.example"},
162 Record: &dkim.Record{},
166 &dns.Domain{ASCII: "reject.example"},
167 true, Result{false, StatusPass, false, true, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
170 // Reject due to spf and dkim "strict".
171 strict := DefaultRecord
172 strict.Policy = PolicyReject
173 strict.ADKIM = AlignStrict
174 strict.ASPF = AlignStrict
175 test("strict.example",
178 Status: dkim.StatusPass,
179 Sig: &dkim.Sig{ // Just the minimum fields needed.
180 Domain: dns.Domain{ASCII: "sub.strict.example"},
182 Record: &dkim.Record{},
186 &dns.Domain{ASCII: "sub.strict.example"},
187 true, Result{true, StatusFail, false, false, dns.Domain{ASCII: "strict.example"}, &strict, false, nil},
190 // No dmarc policy, nothing to say.
191 test("absent.example",
195 false, Result{false, StatusNone, false, false, dns.Domain{ASCII: "absent.example"}, nil, false, ErrNoRecord},
198 // No dmarc policy, spf pass does nothing.
199 test("absent.example",
202 &dns.Domain{ASCII: "absent.example"},
203 false, Result{false, StatusNone, false, false, dns.Domain{ASCII: "absent.example"}, nil, false, ErrNoRecord},
206 none := DefaultRecord
207 none.Policy = PolicyNone
208 // Policy none results in no reject.
212 &dns.Domain{ASCII: "none.example"},
213 true, Result{false, StatusPass, true, false, dns.Domain{ASCII: "none.example"}, &none, false, nil},
216 // No actual reject due to pct=0.
217 testr := DefaultRecord
218 testr.Policy = PolicyReject
224 false, Result{true, StatusFail, false, false, dns.Domain{ASCII: "test.example"}, &testr, false, nil},
227 // No reject if subdomain has "none" policy.
229 sub.Policy = PolicyReject
230 sub.SubdomainPolicy = PolicyNone
231 test("sub.subnone.example",
234 &dns.Domain{ASCII: "sub.subnone.example"},
235 true, Result{false, StatusFail, false, false, dns.Domain{ASCII: "subnone.example"}, &sub, false, nil},
238 // No reject if spf temperror and no other pass.
239 test("reject.example",
242 &dns.Domain{ASCII: "mail.reject.example"},
243 true, Result{false, StatusTemperror, false, false, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
246 // No reject if dkim temperror and no other pass.
247 test("reject.example",
250 Status: dkim.StatusTemperror,
251 Sig: &dkim.Sig{ // Just the minimum fields needed.
252 Domain: dns.Domain{ASCII: "sub.reject.example"},
254 Record: &dkim.Record{},
259 true, Result{false, StatusTemperror, false, false, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
262 // No reject if spf temperror but still dkim pass.
263 test("reject.example",
266 Status: dkim.StatusPass,
267 Sig: &dkim.Sig{ // Just the minimum fields needed.
268 Domain: dns.Domain{ASCII: "sub.reject.example"},
270 Record: &dkim.Record{},
274 &dns.Domain{ASCII: "mail.reject.example"},
275 true, Result{false, StatusPass, false, true, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
278 // No reject if dkim temperror but still spf pass.
279 test("reject.example",
282 Status: dkim.StatusTemperror,
283 Sig: &dkim.Sig{ // Just the minimum fields needed.
284 Domain: dns.Domain{ASCII: "sub.reject.example"},
286 Record: &dkim.Record{},
290 &dns.Domain{ASCII: "mail.reject.example"},
291 true, Result{false, StatusPass, true, false, dns.Domain{ASCII: "reject.example"}, &reject, false, nil},
294 // Bad DMARC record results in permerror without reject.
295 test("malformed.example",
299 false, Result{false, StatusPermerror, false, false, dns.Domain{ASCII: "malformed.example"}, nil, false, ErrSyntax},
302 // DKIM domain that is higher-level than organizational can not result in a pass.
../rfc/7489:525
306 Status: dkim.StatusPass,
307 Sig: &dkim.Sig{ // Just the minimum fields needed.
308 Domain: dns.Domain{ASCII: "com"},
310 Record: &dkim.Record{},
315 true, Result{true, StatusFail, false, false, dns.Domain{ASCII: "example.com"}, &reject, false, nil},