mox/dmarcrpt/parse_test.go
Mechiel Lukkien e7699708ef
implement outgoing dmarc aggregate reporting
in smtpserver, we store dmarc evaluations (under the right conditions).
in dmarcdb, we periodically (hourly) send dmarc reports if there are
evaluations. for failed deliveries, we deliver the dsn quietly to a submailbox
of the postmaster mailbox.

this is on by default, but can be disabled in mox.conf.
2023-11-02 09:12:30 +01:00

186 lines
4.4 KiB
Go

package dmarcrpt
import (
"encoding/xml"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/mjl-/mox/mlog"
)
var xlog = mlog.New("dmarcrpt")
const reportExample = `<?xml version="1.0" encoding="UTF-8" ?>
<feedback>
<report_metadata>
<org_name>google.com</org_name>
<email>noreply-dmarc-support@google.com</email>
<extra_contact_info>https://support.google.com/a/answer/2466580</extra_contact_info>
<report_id>10051505501689795560</report_id>
<date_range>
<begin>1596412800</begin>
<end>1596499199</end>
</date_range>
</report_metadata>
<policy_published>
<domain>example.org</domain>
<adkim>r</adkim>
<aspf>r</aspf>
<p>reject</p>
<sp>reject</sp>
<pct>100</pct>
</policy_published>
<record>
<row>
<source_ip>127.0.0.1</source_ip>
<count>1</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>pass</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.org</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>example.org</domain>
<result>pass</result>
<selector>example</selector>
</dkim>
<spf>
<domain>example.org</domain>
<result>pass</result>
</spf>
</auth_results>
</record>
</feedback>
`
func TestParseReport(t *testing.T) {
var expect = &Feedback{
XMLName: xml.Name{Local: "feedback"},
ReportMetadata: ReportMetadata{
OrgName: "google.com",
Email: "noreply-dmarc-support@google.com",
ExtraContactInfo: "https://support.google.com/a/answer/2466580",
ReportID: "10051505501689795560",
DateRange: DateRange{
Begin: 1596412800,
End: 1596499199,
},
},
PolicyPublished: PolicyPublished{
Domain: "example.org",
ADKIM: "r",
ASPF: "r",
Policy: "reject",
SubdomainPolicy: "reject",
Percentage: 100,
},
Records: []ReportRecord{
{
Row: Row{
SourceIP: "127.0.0.1",
Count: 1,
PolicyEvaluated: PolicyEvaluated{
Disposition: DispositionNone,
DKIM: DMARCPass,
SPF: DMARCPass,
},
},
Identifiers: Identifiers{
HeaderFrom: "example.org",
},
AuthResults: AuthResults{
DKIM: []DKIMAuthResult{
{
Domain: "example.org",
Result: DKIMPass,
Selector: "example",
},
},
SPF: []SPFAuthResult{
{
Domain: "example.org",
Result: SPFPass,
},
},
},
},
},
}
feedback, err := ParseReport(strings.NewReader(reportExample))
if err != nil {
t.Fatalf("parsing report: %s", err)
}
if !reflect.DeepEqual(expect, feedback) {
t.Fatalf("expected:\n%#v\ngot:\n%#v", expect, feedback)
}
}
func TestParseMessageReport(t *testing.T) {
dir := filepath.FromSlash("../testdata/dmarc-reports")
files, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("listing dmarc aggregate report emails: %s", err)
}
for _, file := range files {
p := filepath.Join(dir, file.Name())
f, err := os.Open(p)
if err != nil {
t.Fatalf("open %q: %s", p, err)
}
_, err = ParseMessageReport(xlog, f)
if err != nil {
t.Fatalf("ParseMessageReport: %q: %s", p, err)
}
f.Close()
}
// No report in a non-multipart message.
_, err = ParseMessageReport(xlog, strings.NewReader("From: <mjl@mox.example>\r\n\r\nNo report.\r\n"))
if err != ErrNoReport {
t.Fatalf("message without report, got err %#v, expected ErrNoreport", err)
}
// No report in a multipart message.
var multipartNoreport = strings.ReplaceAll(`From: <mjl@mox.example>
To: <mjl@mox.example>
Subject: Report Domain: mox.example Submitter: mail.mox.example
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="===============5735553800636657282=="
--===============5735553800636657282==
Content-Type: text/plain
MIME-Version: 1.0
test
--===============5735553800636657282==
Content-Type: text/html
MIME-Version: 1.0
<html></html>
--===============5735553800636657282==--
`, "\n", "\r\n")
_, err = ParseMessageReport(xlog, strings.NewReader(multipartNoreport))
if err != ErrNoReport {
t.Fatalf("message without report, got err %#v, expected ErrNoreport", err)
}
}
func FuzzParseReport(f *testing.F) {
f.Add("")
f.Add(reportExample)
f.Fuzz(func(t *testing.T, s string) {
ParseReport(strings.NewReader(s))
})
}