Documentation ¶
Overview ¶
Package dmarc implements DMARC (Domain-based Message Authentication, Reporting, and Conformance; RFC 7489) verification.
DMARC is a mechanism for verifying ("authenticating") the address in the "From" message header, since users will look at that header to identify the sender of a message. DMARC compares the "From"-(sub)domain against the SPF and/or DKIM-validated domains, based on the DMARC policy that a domain has published in DNS as TXT record under "_dmarc.<domain>". A DMARC policy can also ask for feedback about evaluations by other email servers, for monitoring/debugging problems with email delivery.
Index ¶
- Variables
- func Lookup(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, ...) (status Status, domain dns.Domain, record *Record, txt string, rauthentic bool, ...)
- func LookupExternalReportsAccepted(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, ...) (accepts bool, status Status, records []*Record, txts []string, authentic bool, ...)
- type Align
- type DMARCPolicy
- type Record
- type Result
- type Status
- type URI
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrNoRecord = errors.New("dmarc: no dmarc dns record") ErrMultipleRecords = errors.New("dmarc: multiple dmarc dns records") // Must also be treated as if domain does not implement DMARC. ErrDNS = errors.New("dmarc: dns lookup") ErrSyntax = errors.New("dmarc: malformed dmarc dns record") )
Lookup errors.
var DefaultRecord = Record{ Version: "DMARC1", ADKIM: "r", ASPF: "r", AggregateReportingInterval: 86400, FailureReportingOptions: []string{"0"}, ReportingFormat: []string{"afrf"}, Percentage: 100, }
DefaultRecord holds the defaults for a DMARC record.
var (
MetricVerify stub.HistogramVec = stub.HistogramVecIgnore{}
)
Functions ¶
func Lookup ¶
func Lookup(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, msgFrom dns.Domain) (status Status, domain dns.Domain, record *Record, txt string, rauthentic bool, rerr error)
Lookup looks up the DMARC TXT record at "_dmarc.<domain>" for the domain in the "From"-header of a message.
If no DMARC record is found for the "From"-domain, another lookup is done at the organizational domain of the domain (if different). The organizational domain is determined using the public suffix list. E.g. for "sub.example.com", the organizational domain is "example.com". The returned domain is the domain with the DMARC record.
rauthentic indicates if the DNS results were DNSSEC-verified.
Example ¶
package main import ( "context" "log" "golang.org/x/exp/slog" "github.com/mjl-/mox/dmarc" "github.com/mjl-/mox/dns" ) func main() { ctx := context.Background() resolver := dns.StrictResolver{} msgFrom, err := dns.ParseDomain("sub.example.com") if err != nil { log.Fatalf("parsing from domain: %v", err) } // Lookup DMARC DNS record for domain. status, domain, record, txt, authentic, err := dmarc.Lookup(ctx, slog.Default(), resolver, msgFrom) if err != nil { log.Fatalf("dmarc lookup: %v", err) } log.Printf("status %s, domain %s, record %v, txt %q, dnssec %v", status, domain, record, txt, authentic) }
Output:
func LookupExternalReportsAccepted ¶ added in v0.0.7
func LookupExternalReportsAccepted(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, dmarcDomain dns.Domain, extDestDomain dns.Domain) (accepts bool, status Status, records []*Record, txts []string, authentic bool, rerr error)
LookupExternalReportsAccepted returns whether the extDestDomain has opted in to receiving dmarc reports for dmarcDomain (where the dmarc record was found), through a "._report._dmarc." DNS TXT DMARC record.
accepts is true if the external domain has opted in. If a temporary error occurred, the returned status is StatusTemperror, and a later retry may give an authoritative result. The returned error is ErrNoRecord if no opt-in DNS record exists, which is not a failure condition.
The normally invalid "v=DMARC1" record is accepted since it is used as example in RFC 7489.
authentic indicates if the DNS results were DNSSEC-verified.
Types ¶
type DMARCPolicy ¶
type DMARCPolicy string
Policy as used in DMARC DNS record for "p=" or "sp=".
const ( PolicyEmpty DMARCPolicy = "" // Only for the optional Record.SubdomainPolicy. PolicyNone DMARCPolicy = "none" PolicyQuarantine DMARCPolicy = "quarantine" PolicyReject DMARCPolicy = "reject" )
type Record ¶
type Record struct { Version string // "v=DMARC1", fixed. Policy DMARCPolicy // Required, for "p=". SubdomainPolicy DMARCPolicy // Like policy but for subdomains. Optional, for "sp=". AggregateReportAddresses []URI // Optional, for "rua=". Destination addresses for aggregate reports. FailureReportAddresses []URI // Optional, for "ruf=". Destination addresses for failure reports. ADKIM Align // Alignment: "r" (default) for relaxed or "s" for simple. For "adkim=". ASPF Align // Alignment: "r" (default) for relaxed or "s" for simple. For "aspf=". AggregateReportingInterval int // In seconds, default 86400. For "ri=" FailureReportingOptions []string // "0" (default), "1", "d", "s". For "fo=". ReportingFormat []string // "afrf" (default). For "rf=". Percentage int // Between 0 and 100, default 100. For "pct=". Policy applies randomly to this percentage of messages. }
Record is a DNS policy or reporting record.
Example:
v=DMARC1; p=reject; rua=mailto:postmaster@mox.example
func ParseRecord ¶
ParseRecord parses a DMARC TXT record.
Fields and values are are case-insensitive in DMARC are returned in lower case for easy comparison.
DefaultRecord provides default values for tags not present in s.
isdmarc indicates if the record starts tag "v" with value "DMARC1", and should be treated as a valid DMARC record. Used to detect possibly multiple DMARC records (invalid) for a domain with multiple TXT record (quite common).
Example ¶
package main import ( "log" "github.com/mjl-/mox/dmarc" ) func main() { txt := "v=DMARC1; p=reject; rua=mailto:postmaster@mox.example" record, isdmarc, err := dmarc.ParseRecord(txt) if err != nil { log.Fatalf("parsing dmarc record: %v (isdmarc: %v)", err, isdmarc) } log.Printf("parsed record: %v", record) }
Output:
func ParseRecordNoRequired ¶ added in v0.0.7
ParseRecordNoRequired is like ParseRecord, but don't check for required fields for regular DMARC records. Useful for checking the _report._dmarc record, used for opting into receiving reports for other domains.
type Result ¶
type Result struct { // Whether to reject the message based on policies. If false, the message should // not necessarily be accepted: other checks such as reputation-based and // content-based analysis may lead to reject the message. Reject bool // Result of DMARC validation. A message can fail validation, but still // not be rejected, e.g. if the policy is "none". Status Status AlignedSPFPass bool AlignedDKIMPass bool // Domain with the DMARC DNS record. May be the organizational domain instead of // the domain in the From-header. Domain dns.Domain // Parsed DMARC record. Record *Record // Whether DMARC DNS response was DNSSEC-signed, regardless of whether SPF/DKIM records were DNSSEC-signed. RecordAuthentic bool // Details about possible error condition, e.g. when parsing the DMARC record failed. Err error }
Result is a DMARC policy evaluation.
func Verify ¶
func Verify(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, msgFrom dns.Domain, dkimResults []dkim.Result, spfResult spf.Status, spfIdentity *dns.Domain, applyRandomPercentage bool) (useResult bool, result Result)
Verify evaluates the DMARC policy for the domain in the From-header of a message given the DKIM and SPF evaluation results.
applyRandomPercentage determines whether the records "pct" is honored. This field specifies the percentage of messages the DMARC policy is applied to. It is used for slow rollout of DMARC policies and should be honored during normal email processing
Verify always returns the result of verifying the DMARC policy against the message (for inclusion in Authentication-Result headers).
useResult indicates if the result should be applied in a policy decision, based on the "pct" field in the DMARC record.
Example ¶
package main import ( "context" "log" "net" "strings" "golang.org/x/exp/slog" "github.com/mjl-/mox/dkim" "github.com/mjl-/mox/dmarc" "github.com/mjl-/mox/dns" "github.com/mjl-/mox/message" "github.com/mjl-/mox/spf" ) func main() { ctx := context.Background() resolver := dns.StrictResolver{} // Message to verify. msg := strings.NewReader("From: <sender@example.com>\r\nMore: headers\r\n\r\nBody\r\n") msgFrom, _, _, err := message.From(slog.Default(), true, msg) if err != nil { log.Fatalf("parsing message for from header: %v", err) } // Verify SPF, for use with DMARC. args := spf.Args{ RemoteIP: net.ParseIP("10.11.12.13"), MailFromDomain: dns.Domain{ASCII: "sub.example.com"}, } spfReceived, spfDomain, _, _, err := spf.Verify(ctx, slog.Default(), resolver, args) if err != nil { log.Printf("verifying spf: %v", err) } // Verify DKIM-Signature headers, for use with DMARC. smtputf8 := false ignoreTestMode := false dkimResults, err := dkim.Verify(ctx, slog.Default(), resolver, smtputf8, dkim.DefaultPolicy, msg, ignoreTestMode) if err != nil { log.Printf("verifying dkim: %v", err) } // Verify DMARC, based on DKIM and SPF results. applyRandomPercentage := true useResult, result := dmarc.Verify(ctx, slog.Default(), resolver, msgFrom.Domain, dkimResults, spfReceived.Result, &spfDomain, applyRandomPercentage) // Print results. log.Printf("dmarc status: %s", result.Status) log.Printf("use result: %v", useResult) if useResult && result.Reject { log.Printf("should reject message") } log.Printf("result: %#v", result) }
Output:
type Status ¶
type Status string
Status is the result of DMARC policy evaluation, for use in an Authentication-Results header.
const ( StatusNone Status = "none" // No DMARC TXT DNS record found. StatusPass Status = "pass" // SPF and/or DKIM pass with identifier alignment. StatusFail Status = "fail" // Either both SPF and DKIM failed or identifier did not align with a pass. StatusTemperror Status = "temperror" // Typically a DNS lookup. A later attempt may results in a conclusion. StatusPermerror Status = "permerror" // Typically a malformed DMARC DNS record. )