Parse DMARC aggregate reports; model deliverability findings and delivery outcomes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6663b268c8
commit
5c1737bad3
3 files changed
+483
No files matched your search
@@ -0,0 +1,157 @@
|
||||
package dmarc
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const sample = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<feedback>
|
||||
<report_metadata>
|
||||
<org_name>google.com</org_name>
|
||||
<email>noreply-dmarc-support@google.com</email>
|
||||
<report_id>12345</report_id>
|
||||
<date_range><begin>1700000000</begin><end>1700086399</end></date_range>
|
||||
</report_metadata>
|
||||
<policy_published>
|
||||
<domain>Example.COM</domain>
|
||||
<adkim>r</adkim><aspf>r</aspf><p>none</p><sp>none</sp><pct>100</pct>
|
||||
</policy_published>
|
||||
<record>
|
||||
<row>
|
||||
<source_ip>203.0.113.7</source_ip><count>12</count>
|
||||
<policy_evaluated><disposition>none</disposition><dkim>pass</dkim><spf>fail</spf></policy_evaluated>
|
||||
</row>
|
||||
<identifiers><header_from>example.com</header_from><envelope_from>bounce.example.com</envelope_from></identifiers>
|
||||
<auth_results>
|
||||
<dkim><domain>example.com</domain><selector>s1</selector><result>pass</result></dkim>
|
||||
<spf><domain>bounce.example.com</domain><result>pass</result></spf>
|
||||
</auth_results>
|
||||
</record>
|
||||
<record>
|
||||
<row>
|
||||
<source_ip>198.51.100.9</source_ip><count>3</count>
|
||||
<policy_evaluated><disposition>quarantine</disposition><dkim>fail</dkim><spf>fail</spf></policy_evaluated>
|
||||
</row>
|
||||
<identifiers><header_from>example.com</header_from></identifiers>
|
||||
<auth_results>
|
||||
<spf><domain>example.com</domain><result>softfail</result></spf>
|
||||
</auth_results>
|
||||
</record>
|
||||
</feedback>`
|
||||
|
||||
func check(t *testing.T, r *Report) {
|
||||
t.Helper()
|
||||
if r.Org != "google.com" || r.ReportID != "12345" || r.Domain != "example.com" {
|
||||
t.Fatalf("metadata: %+v", r)
|
||||
}
|
||||
if r.Begin.Unix() != 1700000000 || r.End.Unix() != 1700086399 {
|
||||
t.Fatalf("range: %v %v", r.Begin, r.End)
|
||||
}
|
||||
if r.Policy.P != "none" || r.Policy.Pct != 100 || r.Policy.ADKIM != "r" {
|
||||
t.Fatalf("policy: %+v", r.Policy)
|
||||
}
|
||||
if len(r.Records) != 2 {
|
||||
t.Fatalf("records: %d", len(r.Records))
|
||||
}
|
||||
a, b := r.Records[0], r.Records[1]
|
||||
if a.SourceIP != "203.0.113.7" || a.Count != 12 || !a.Passed() || a.EnvelopeFrom != "bounce.example.com" {
|
||||
t.Fatalf("first record: %+v", a)
|
||||
}
|
||||
if len(a.DKIMAuth) != 1 || a.DKIMAuth[0].Selector != "s1" || len(a.SPFAuth) != 1 {
|
||||
t.Fatalf("auth results: %+v", a)
|
||||
}
|
||||
if b.Passed() || b.Disposition != "quarantine" || b.SPFAuth[0].Result != "softfail" {
|
||||
t.Fatalf("second record: %+v", b)
|
||||
}
|
||||
}
|
||||
|
||||
func gz(t *testing.T, s string) []byte {
|
||||
var buf bytes.Buffer
|
||||
w := gzip.NewWriter(&buf)
|
||||
w.Write([]byte(s))
|
||||
w.Close()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func zipped(t *testing.T, s string) []byte {
|
||||
var buf bytes.Buffer
|
||||
w := zip.NewWriter(&buf)
|
||||
f, _ := w.Create("report.xml")
|
||||
f.Write([]byte(s))
|
||||
w.Close()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func email(contentType, encoding string, body []byte) []byte {
|
||||
b64 := base64.StdEncoding.EncodeToString(body)
|
||||
var wrapped strings.Builder
|
||||
for len(b64) > 76 {
|
||||
wrapped.WriteString(b64[:76] + "\r\n")
|
||||
b64 = b64[76:]
|
||||
}
|
||||
wrapped.WriteString(b64 + "\r\n")
|
||||
return []byte("From: noreply@google.com\r\nSubject: Report Domain: example.com\r\nMIME-Version: 1.0\r\n" +
|
||||
"Content-Type: multipart/mixed; boundary=\"XX\"\r\n\r\n--XX\r\nContent-Type: text/plain\r\n\r\nSee attached.\r\n" +
|
||||
"--XX\r\nContent-Type: " + contentType + "\r\nContent-Transfer-Encoding: " + encoding + "\r\n\r\n" + wrapped.String() + "--XX--\r\n")
|
||||
}
|
||||
|
||||
func TestParseForms(t *testing.T) {
|
||||
forms := map[string][]byte{
|
||||
"xml": []byte("\n" + sample),
|
||||
"gzip": gz(t, sample),
|
||||
"zip": zipped(t, sample),
|
||||
"mail+gzip": email("application/gzip", "base64", gz(t, sample)),
|
||||
"mail+zip": email("application/zip; name=r.zip", "base64", zipped(t, sample)),
|
||||
"mail+xml": email("text/xml", "base64", []byte(sample)),
|
||||
"mail+octet": email("application/octet-stream", "base64", gz(t, sample)),
|
||||
}
|
||||
for name, data := range forms {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
r, err := Parse(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check(t, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRejects(t *testing.T) {
|
||||
for name, data := range map[string][]byte{
|
||||
"empty": nil,
|
||||
"text": []byte("hello, this is not a report"),
|
||||
"other xml": []byte("<html><body/></html>"),
|
||||
"no id": []byte(strings.Replace(sample, "<report_id>12345</report_id>", "", 1)),
|
||||
"mail no att": []byte("Subject: x\r\n\r\njust text\r\n"),
|
||||
"bad gzip": {0x1f, 0x8b, 0, 0},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if r, err := Parse(data); err == nil {
|
||||
t.Fatalf("expected an error, got %+v", r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBombIsRefused(t *testing.T) {
|
||||
big := "<feedback>" + strings.Repeat("x", maxXML+10) + "</feedback>"
|
||||
if _, err := Parse(gz(t, big)); err == nil || !strings.Contains(err.Error(), "too large") {
|
||||
t.Fatalf("want too large, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPctDefaultsAndClamps(t *testing.T) {
|
||||
r, err := Parse([]byte(strings.Replace(sample, "<pct>100</pct>", "<pct>25</pct>", 1)))
|
||||
if err != nil || r.Policy.Pct != 25 {
|
||||
t.Fatalf("pct 25: %v %+v", err, r)
|
||||
}
|
||||
r, err = Parse([]byte(strings.Replace(sample, "<pct>100</pct>", "", 1)))
|
||||
if err != nil || r.Policy.Pct != 100 {
|
||||
t.Fatalf("missing pct: %v %+v", err, r)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user