1package dmarcrpt
2
3import (
4 "encoding/xml"
5 "os"
6 "path/filepath"
7 "reflect"
8 "strings"
9 "testing"
10
11 "github.com/mjl-/mox/mlog"
12)
13
14var pkglog = mlog.New("dmarcrpt", nil)
15
16const reportExample = `<?xml version="1.0" encoding="UTF-8" ?>
17<feedback>
18 <report_metadata>
19 <org_name>google.com</org_name>
20 <email>noreply-dmarc-support@google.com</email>
21 <extra_contact_info>https://support.google.com/a/answer/2466580</extra_contact_info>
22 <report_id>10051505501689795560</report_id>
23 <date_range>
24 <begin>1596412800</begin>
25 <end>1596499199</end>
26 </date_range>
27 </report_metadata>
28 <policy_published>
29 <domain>example.org</domain>
30 <adkim>r</adkim>
31 <aspf>r</aspf>
32 <p>reject</p>
33 <sp>reject</sp>
34 <pct>100</pct>
35 </policy_published>
36 <record>
37 <row>
38 <source_ip>127.0.0.1</source_ip>
39 <count>1</count>
40 <policy_evaluated>
41 <disposition>none</disposition>
42 <dkim>pass</dkim>
43 <spf>pass</spf>
44 </policy_evaluated>
45 </row>
46 <identifiers>
47 <header_from>example.org</header_from>
48 </identifiers>
49 <auth_results>
50 <dkim>
51 <domain>example.org</domain>
52 <result>pass</result>
53 <selector>example</selector>
54 </dkim>
55 <spf>
56 <domain>example.org</domain>
57 <result>pass</result>
58 </spf>
59 </auth_results>
60 </record>
61</feedback>
62`
63
64func TestParseReport(t *testing.T) {
65 var expect = &Feedback{
66 XMLName: xml.Name{Local: "feedback"},
67 ReportMetadata: ReportMetadata{
68 OrgName: "google.com",
69 Email: "noreply-dmarc-support@google.com",
70 ExtraContactInfo: "https://support.google.com/a/answer/2466580",
71 ReportID: "10051505501689795560",
72 DateRange: DateRange{
73 Begin: 1596412800,
74 End: 1596499199,
75 },
76 },
77 PolicyPublished: PolicyPublished{
78 Domain: "example.org",
79 ADKIM: "r",
80 ASPF: "r",
81 Policy: "reject",
82 SubdomainPolicy: "reject",
83 Percentage: 100,
84 },
85 Records: []ReportRecord{
86 {
87 Row: Row{
88 SourceIP: "127.0.0.1",
89 Count: 1,
90 PolicyEvaluated: PolicyEvaluated{
91 Disposition: DispositionNone,
92 DKIM: DMARCPass,
93 SPF: DMARCPass,
94 },
95 },
96 Identifiers: Identifiers{
97 HeaderFrom: "example.org",
98 },
99 AuthResults: AuthResults{
100 DKIM: []DKIMAuthResult{
101 {
102 Domain: "example.org",
103 Result: DKIMPass,
104 Selector: "example",
105 },
106 },
107 SPF: []SPFAuthResult{
108 {
109 Domain: "example.org",
110 Result: SPFPass,
111 },
112 },
113 },
114 },
115 },
116 }
117
118 feedback, err := ParseReport(strings.NewReader(reportExample))
119 if err != nil {
120 t.Fatalf("parsing report: %s", err)
121 }
122 if !reflect.DeepEqual(expect, feedback) {
123 t.Fatalf("expected:\n%#v\ngot:\n%#v", expect, feedback)
124 }
125}
126
127func TestParseMessageReport(t *testing.T) {
128 dir := filepath.FromSlash("../testdata/dmarc-reports")
129 files, err := os.ReadDir(dir)
130 if err != nil {
131 t.Fatalf("listing dmarc aggregate report emails: %s", err)
132 }
133
134 for _, file := range files {
135 p := filepath.Join(dir, file.Name())
136 f, err := os.Open(p)
137 if err != nil {
138 t.Fatalf("open %q: %s", p, err)
139 }
140 _, err = ParseMessageReport(pkglog.Logger, f)
141 if err != nil {
142 t.Fatalf("ParseMessageReport: %q: %s", p, err)
143 }
144 f.Close()
145 }
146
147 // No report in a non-multipart message.
148 _, err = ParseMessageReport(pkglog.Logger, strings.NewReader("From: <mjl@mox.example>\r\n\r\nNo report.\r\n"))
149 if err != ErrNoReport {
150 t.Fatalf("message without report, got err %#v, expected ErrNoreport", err)
151 }
152
153 // No report in a multipart message.
154 var multipartNoreport = strings.ReplaceAll(`From: <mjl@mox.example>
155To: <mjl@mox.example>
156Subject: Report Domain: mox.example Submitter: mail.mox.example
157MIME-Version: 1.0
158Content-Type: multipart/alternative; boundary="===============5735553800636657282=="
159
160--===============5735553800636657282==
161Content-Type: text/plain
162MIME-Version: 1.0
163
164test
165
166--===============5735553800636657282==
167Content-Type: text/html
168MIME-Version: 1.0
169
170<html></html>
171
172--===============5735553800636657282==--
173`, "\n", "\r\n")
174 _, err = ParseMessageReport(pkglog.Logger, strings.NewReader(multipartNoreport))
175 if err != ErrNoReport {
176 t.Fatalf("message without report, got err %#v, expected ErrNoreport", err)
177 }
178}
179
180func FuzzParseReport(f *testing.F) {
181 f.Add("")
182 f.Add(reportExample)
183 f.Fuzz(func(t *testing.T, s string) {
184 ParseReport(strings.NewReader(s))
185 })
186}
187