dmarc

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 6 Imported by: 0

README

dmarc

CI Go Reference

The receiver side of DMARC (RFC 7489) for Go, with zero external dependencies (standard library only): policy lookup and parsing, identifier alignment, and aggregate (rua) report XML generation.

The package holds no storage or scheduling state. A mail pipeline consults the policy/alignment primitives per message and records the outcomes however it likes; a reporter later hands a slice of neutral AggregateRecord values to BuildReport to emit the RFC 7489 aggregate-report document.

  • Lookup / ParsePolicy — fetch the _dmarc.<domain> TXT record and read its published policy (p=). Lookup takes an injectable TXTResolver (its signature matches net.LookupTXT), or nil for system DNS.
  • Aligned — DMARC relaxed identifier alignment (RFC 7489 §3.1) between an authenticated domain (from SPF smtp.mailfrom or a DKIM signature's d=) and the From domain.
  • AggregateRecords / BuildReport / Gzip — group per-message evaluations into report rows and marshal the aggregate-report XML, ready to be gzipped and attached per RFC 7489 §7.2.1.

Install

go get github.com/rest-mail/go-dmarc

Evaluate a message

record, err := dmarc.Lookup("example.com", nil)
if err != nil || record == "" {
	// No DMARC record (or lookup failed): DMARC does not apply.
	return
}
policy := dmarc.ParsePolicy(record) // "none" | "quarantine" | "reject"

// authDomain comes from an SPF smtp.mailfrom or a verified DKIM d=.
aligned := dmarc.Aligned(authDomain, "example.com")
if !aligned && policy == "reject" {
	// reject per published policy
}

Build an aggregate report

meta := dmarc.ReportMetadata{
	OrgName:   "reporter.example",
	Email:     "dmarc-reports@reporter.example",
	ReportID:  "1784700000.example.com@reporter.example",
	DateRange: dmarc.DateRange{Begin: begin, End: end},
}
policy := dmarc.PolicyPublished{Domain: "example.com", ADKIM: "r", ASPF: "r", P: "reject", PCT: 100}

xmlBytes, err := dmarc.BuildReport(meta, policy, records) // records []dmarc.AggregateRecord
if err != nil {
	panic(err)
}
gz, err := dmarc.Gzip(xmlBytes)
if err != nil {
	panic(err)
}
_ = gz // attach as application/gzip

License

MIT © 2026 rest-mail

Documentation

Overview

Package dmarc implements the receiver side of DMARC (RFC 7489): looking up and parsing a domain's published policy, evaluating identifier alignment, and generating aggregate (rua) report XML.

The package is deliberately free of any storage or scheduling concerns. A caller records per-message evaluations however it likes, then hands a slice of neutral AggregateRecord values to BuildReport to produce the RFC 7489 aggregate-report document. Policy discovery (Lookup, ParsePolicy) and alignment (Aligned) are exposed as standalone primitives so a mail pipeline can consult them per message.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Aligned

func Aligned(authDomain, fromDomain string) bool

Aligned reports whether an authenticated domain aligns with the From domain under DMARC relaxed alignment (RFC 7489 §3.1): the organizational domains must match. This uses the simple, registry-free rule that the two are equal or one is a subdomain of the other (case-insensitive).

func BuildReport

func BuildReport(meta ReportMetadata, policy PolicyPublished, records []AggregateRecord) ([]byte, error)

BuildReport assembles an RFC 7489 aggregate report XML document (with the XML declaration prepended).

func Gzip

func Gzip(data []byte) ([]byte, error)

Gzip compresses report bytes for the report attachment (reports are delivered as application/gzip per RFC 7489 §7.2.1).

func Lookup

func Lookup(domain string, resolver TXTResolver) (string, error)

Lookup fetches and returns the raw DMARC record published at _dmarc.<domain>. It returns ("", nil) when the domain publishes no DMARC record (this is not an error: DMARC simply does not apply), and ("", err) when the underlying TXT lookup fails.

func ParsePolicy

func ParsePolicy(record string) string

ParsePolicy extracts the requested policy (the p= tag) from a DMARC record. It returns "none" when no p= tag is present.

Types

type AggregateRecord

type AggregateRecord struct {
	Domain      string // header-From domain (the reported-on domain)
	SourceIP    string
	HeaderFrom  string
	Disposition string // none|quarantine|reject (policy applied)
	DKIMResult  string // pass|fail|none
	DKIMAligned bool
	SPFResult   string // pass|fail|none
	SPFAligned  bool
}

AggregateRecord is one message's DMARC evaluation, the neutral input to AggregateRecords and BuildReport. It carries only what the aggregate report needs, so the package depends on no particular storage model.

type AuthResults

type AuthResults struct {
	DKIM []DKIMResult `xml:"dkim,omitempty"`
	SPF  []SPFResult  `xml:"spf,omitempty"`
}

type DKIMResult

type DKIMResult struct {
	Domain string `xml:"domain"`
	Result string `xml:"result"`
}

type DateRange

type DateRange struct {
	Begin int64 `xml:"begin"`
	End   int64 `xml:"end"`
}

DateRange is the reporting period as UNIX epoch seconds.

type Feedback

type Feedback struct {
	XMLName         xml.Name        `xml:"feedback"`
	Version         string          `xml:"version,omitempty"`
	ReportMetadata  ReportMetadata  `xml:"report_metadata"`
	PolicyPublished PolicyPublished `xml:"policy_published"`
	Records         []ReportRecord  `xml:"record"`
}

Feedback is the root element of an RFC 7489 aggregate report.

type Identifiers

type Identifiers struct {
	HeaderFrom string `xml:"header_from"`
}

type PolicyEvaluated

type PolicyEvaluated struct {
	Disposition string `xml:"disposition"`
	DKIM        string `xml:"dkim"`
	SPF         string `xml:"spf"`
}

type PolicyPublished

type PolicyPublished struct {
	Domain string `xml:"domain"`
	ADKIM  string `xml:"adkim,omitempty"`
	ASPF   string `xml:"aspf,omitempty"`
	P      string `xml:"p"`
	SP     string `xml:"sp,omitempty"`
	PCT    int    `xml:"pct,omitempty"`
}

PolicyPublished is the DMARC record the reported-on domain published.

type ReportMetadata

type ReportMetadata struct {
	OrgName   string    `xml:"org_name"`
	Email     string    `xml:"email"`
	ReportID  string    `xml:"report_id"`
	DateRange DateRange `xml:"date_range"`
}

ReportMetadata identifies the reporting organization and period.

type ReportRecord

type ReportRecord struct {
	Row         Row         `xml:"row"`
	Identifiers Identifiers `xml:"identifiers"`
	AuthResults AuthResults `xml:"auth_results"`
}

ReportRecord is one aggregated row: a source IP + evaluation + counts.

func AggregateRecords

func AggregateRecords(records []AggregateRecord) []ReportRecord

AggregateRecords groups raw per-message evaluations into report rows by (source IP, disposition, evaluated dkim/spf), summing counts.

type Row

type Row struct {
	SourceIP        string          `xml:"source_ip"`
	Count           int             `xml:"count"`
	PolicyEvaluated PolicyEvaluated `xml:"policy_evaluated"`
}

type SPFResult

type SPFResult struct {
	Domain string `xml:"domain"`
	Result string `xml:"result"`
}

type TXTResolver

type TXTResolver func(name string) ([]string, error)

TXTResolver resolves the TXT records for a name. Its signature matches net.LookupTXT, so that (or a fake in tests) can be passed directly. A nil resolver passed to Lookup falls back to net.LookupTXT.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL