mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 17:44:35 +03:00
add more documentation, examples with tests to illustrate reusable components
This commit is contained in:
@ -58,7 +58,8 @@ const (
|
||||
// Result is a DMARC policy evaluation.
|
||||
type Result struct {
|
||||
// Whether to reject the message based on policies. If false, the message should
|
||||
// not necessarily be accepted, e.g. due to reputation or content-based analysis.
|
||||
// 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".
|
||||
@ -86,12 +87,12 @@ type Result struct {
|
||||
// domain is the domain with the DMARC record.
|
||||
//
|
||||
// rauthentic indicates if the DNS results were DNSSEC-verified.
|
||||
func Lookup(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, from dns.Domain) (status Status, domain dns.Domain, record *Record, txt string, rauthentic bool, rerr error) {
|
||||
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) {
|
||||
log := mlog.New("dmarc", elog)
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
log.Debugx("dmarc lookup result", rerr,
|
||||
slog.Any("fromdomain", from),
|
||||
slog.Any("fromdomain", msgFrom),
|
||||
slog.Any("status", status),
|
||||
slog.Any("domain", domain),
|
||||
slog.Any("record", record),
|
||||
@ -99,15 +100,15 @@ func Lookup(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, from
|
||||
}()
|
||||
|
||||
// ../rfc/7489:859 ../rfc/7489:1370
|
||||
domain = from
|
||||
domain = msgFrom
|
||||
status, record, txt, authentic, err := lookupRecord(ctx, resolver, domain)
|
||||
if status != StatusNone {
|
||||
return status, domain, record, txt, authentic, err
|
||||
}
|
||||
if record == nil {
|
||||
// ../rfc/7489:761 ../rfc/7489:1377
|
||||
domain = publicsuffix.Lookup(ctx, log.Logger, from)
|
||||
if domain == from {
|
||||
domain = publicsuffix.Lookup(ctx, log.Logger, msgFrom)
|
||||
if domain == msgFrom {
|
||||
return StatusNone, domain, nil, txt, authentic, err
|
||||
}
|
||||
|
||||
@ -222,8 +223,9 @@ func LookupExternalReportsAccepted(ctx context.Context, elog *slog.Logger, resol
|
||||
// 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.
|
||||
func Verify(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, from dns.Domain, dkimResults []dkim.Result, spfResult spf.Status, spfIdentity *dns.Domain, applyRandomPercentage bool) (useResult bool, result Result) {
|
||||
// useResult indicates if the result should be applied in a policy decision,
|
||||
// based on the "pct" field in the DMARC record.
|
||||
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) {
|
||||
log := mlog.New("dmarc", elog)
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
@ -237,7 +239,7 @@ func Verify(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, from
|
||||
}
|
||||
MetricVerify.ObserveLabels(float64(time.Since(start))/float64(time.Second), string(result.Status), reject, use)
|
||||
log.Debugx("dmarc verify result", result.Err,
|
||||
slog.Any("fromdomain", from),
|
||||
slog.Any("fromdomain", msgFrom),
|
||||
slog.Any("dkimresults", dkimResults),
|
||||
slog.Any("spfresult", spfResult),
|
||||
slog.Any("status", result.Status),
|
||||
@ -246,7 +248,7 @@ func Verify(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, from
|
||||
slog.Duration("duration", time.Since(start)))
|
||||
}()
|
||||
|
||||
status, recordDomain, record, _, authentic, err := Lookup(ctx, log.Logger, resolver, from)
|
||||
status, recordDomain, record, _, authentic, err := Lookup(ctx, log.Logger, resolver, msgFrom)
|
||||
if record == nil {
|
||||
return false, Result{false, status, false, false, recordDomain, record, authentic, err}
|
||||
}
|
||||
@ -261,7 +263,7 @@ func Verify(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, from
|
||||
// We treat "quarantine" and "reject" the same. Thus, we also don't "downgrade"
|
||||
// from reject to quarantine if this message was sampled out.
|
||||
// ../rfc/7489:1446 ../rfc/7489:1024
|
||||
if recordDomain != from && record.SubdomainPolicy != PolicyEmpty {
|
||||
if recordDomain != msgFrom && record.SubdomainPolicy != PolicyEmpty {
|
||||
result.Reject = record.SubdomainPolicy != PolicyNone
|
||||
} else {
|
||||
result.Reject = record.Policy != PolicyNone
|
||||
@ -288,7 +290,7 @@ func Verify(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, from
|
||||
|
||||
// ../rfc/7489:1319
|
||||
// ../rfc/7489:544
|
||||
if spfResult == spf.StatusPass && spfIdentity != nil && (*spfIdentity == from || result.Record.ASPF == "r" && pubsuffix(from) == pubsuffix(*spfIdentity)) {
|
||||
if spfResult == spf.StatusPass && spfIdentity != nil && (*spfIdentity == msgFrom || result.Record.ASPF == "r" && pubsuffix(msgFrom) == pubsuffix(*spfIdentity)) {
|
||||
result.AlignedSPFPass = true
|
||||
}
|
||||
|
||||
@ -299,7 +301,7 @@ func Verify(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, from
|
||||
continue
|
||||
}
|
||||
// ../rfc/7489:511
|
||||
if dkimResult.Status == dkim.StatusPass && dkimResult.Sig != nil && (dkimResult.Sig.Domain == from || result.Record.ADKIM == "r" && pubsuffix(from) == pubsuffix(dkimResult.Sig.Domain)) {
|
||||
if dkimResult.Status == dkim.StatusPass && dkimResult.Sig != nil && (dkimResult.Sig.Domain == msgFrom || result.Record.ADKIM == "r" && pubsuffix(msgFrom) == pubsuffix(dkimResult.Sig.Domain)) {
|
||||
// ../rfc/7489:535
|
||||
result.AlignedDKIMPass = true
|
||||
break
|
||||
|
86
dmarc/examples_test.go
Normal file
86
dmarc/examples_test.go
Normal file
@ -0,0 +1,86 @@
|
||||
package dmarc_test
|
||||
|
||||
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 ExampleLookup() {
|
||||
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)
|
||||
}
|
||||
|
||||
func ExampleVerify() {
|
||||
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)
|
||||
}
|
||||
|
||||
func ExampleParseRecord() {
|
||||
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)
|
||||
}
|
@ -19,12 +19,17 @@ func (e parseErr) Error() string {
|
||||
// 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).
|
||||
func ParseRecord(s string) (record *Record, isdmarc bool, rerr error) {
|
||||
return parseRecord(s, true)
|
||||
}
|
||||
|
||||
// ParseRecordNoRequired is like ParseRecord, but don't check for required fields
|
||||
// for regular DMARC records. Useful for checking the _report._dmarc record.
|
||||
// for regular DMARC records. Useful for checking the _report._dmarc record,
|
||||
// used for opting into receiving reports for other domains.
|
||||
func ParseRecordNoRequired(s string) (record *Record, isdmarc bool, rerr error) {
|
||||
return parseRecord(s, false)
|
||||
}
|
||||
|
16
dmarc/txt.go
16
dmarc/txt.go
@ -55,17 +55,17 @@ const (
|
||||
//
|
||||
// v=DMARC1; p=reject; rua=mailto:postmaster@mox.example
|
||||
type Record struct {
|
||||
Version string // "v=DMARC1"
|
||||
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=".
|
||||
FailureReportAddresses []URI // Optional, for "ruf="
|
||||
ADKIM Align // "r" (default) for relaxed or "s" for simple. For "adkim=".
|
||||
ASPF Align // "r" (default) for relaxed or "s" for simple. For "aspf=".
|
||||
AggregateReportingInterval int // Default 86400. For "ri="
|
||||
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). Ffor "rf=".
|
||||
Percentage int // Between 0 and 100, default 100. For "pct=".
|
||||
ReportingFormat []string // "afrf" (default). For "rf=".
|
||||
Percentage int // Between 0 and 100, default 100. For "pct=". Policy applies randomly to this percentage of messages.
|
||||
}
|
||||
|
||||
// DefaultRecord holds the defaults for a DMARC record.
|
||||
|
Reference in New Issue
Block a user