DMARC (RFC 7489): checks that the domain in a message's From: field
is the one SPF or DKIM authenticated, and finds what its owner asks
receivers to do with mail that fails.
{:ok, domain} = Sovite.DMARC.from_domain(fields)
result =
Sovite.DMARC.check(resolver, domain,
spf: {:pass, "bounces.example.com"},
dkim: [{:pass, "example.com"}]
)
result.result #=> :pass
result.disposition #=> :nonePolicies are found with the DMARCbis DNS tree walk instead of the
Public Suffix List (discover/2): _dmarc.<domain> is queried for
the From domain and then for its parents. The same walk gives the
Organizational Domain used for relaxed alignment (org_domain/2).
Aggregate reports are built by Sovite.DMARC.Report.
Summary
Functions
Returns whether domains a and b are aligned (RFC 7489 §3.1):
:strict needs the same domain, :relaxed the same Organizational
Domain. Case and a trailing dot do not matter.
Evaluates DMARC for a message from from_domain (RFC 7489 §6.6).
Finds the DMARC policy for domain (DMARCbis DNS tree walk).
Returns the RFC5322.From domain of a message, from its header fields
as parsed by Sovite.Message.Headers.parse/1.
Finds the Organizational Domain of domain (DMARCbis §4.10.2), with
the same tree walk as discover/2
Returns whether rua_domain accepts reports about policy_domain
(RFC 7489 §7.1): always if both have the same Organizational Domain,
and otherwise if <policy_domain>._report._dmarc.<rua_domain> has a
TXT record starting with v=DMARC1.
Functions
@spec aligned?(Sovite.DNS.resolver(), :strict | :relaxed, String.t(), String.t()) :: {:ok, boolean()} | {:error, :temperror}
Returns whether domains a and b are aligned (RFC 7489 §3.1):
:strict needs the same domain, :relaxed the same Organizational
Domain. Case and a trailing dot do not matter.
@spec check(Sovite.DNS.resolver(), String.t(), keyword()) :: Sovite.DMARC.Result.t()
Evaluates DMARC for a message from from_domain (RFC 7489 §6.6).
SPF counts if it passed for a domain aligned with from_domain under
the policy's aspf, and DKIM if any passing signature's d= is
aligned under adkim. Either one makes the result :pass.
When the result is :fail, the disposition comes from np if the
policy was found at a parent and from_domain does not exist (its A,
AAAA, and MX lookups all return NXDOMAIN), from sp if the policy
was found at a parent, and from p otherwise. Unless the message is
sampled (see Sovite.DMARC.Result), the disposition is lowered one
step: :reject to :quarantine, :quarantine to :none.
Options
:spf-{result, domain}: the SPF result (:pass,:fail, ...) and the domain it was checked for (MAIL FROM, or HELO for the null sender), ornil. Defaults to{:none, nil}.:dkim- the DKIM results,[{result, d_domain}]. Only:passcounts. Defaults to[].:random- a function returning a float in[0, 1), used forpct=sampling. Defaults to:rand.uniform/0.
@spec discover(Sovite.DNS.resolver(), String.t()) :: {:ok, Sovite.DMARC.Policy.t()} | {:none, org_domain :: String.t()} | {:error, :temperror}
Finds the DMARC policy for domain (DMARCbis DNS tree walk).
_dmarc.<domain> is queried first. Without a single valid record
there, the walk continues with the parents of domain, removing one
label at a time, but jumps straight to the last 7 labels of a domain
with more than 8, and stops before the top-level domain. Records that
do not start with v=DMARC1 are ignored, and a level with more than
one DMARC record counts as having none. The first record found is the
policy.
Returns {:none, org_domain} without a policy, and
{:error, :temperror} if a query fails with a DNS error other than
NXDOMAIN.
@spec from_domain([Sovite.Message.Headers.field()]) :: {:ok, String.t()} | {:error, :missing | :multiple | :invalid}
Returns the RFC5322.From domain of a message, from its header fields
as parsed by Sovite.Message.Headers.parse/1.
Fails with :missing without a From: field, with :multiple if
there are several From: fields or the mailboxes in it have different
domains (several mailboxes with the same domain are fine, as DMARCbis
allows), and with :invalid if no domain can be read from it.
@spec org_domain(Sovite.DNS.resolver(), String.t()) :: {:ok, String.t()} | {:error, :temperror}
Finds the Organizational Domain of domain (DMARCbis §4.10.2), with
the same tree walk as discover/2:
- A record with
psd=nmakes the domain it is at the Organizational Domain. - A record with
psd=ymarks a public suffix: the Organizational Domain is one label longer (ordomainitself, if the record is atdomain). - Otherwise it is the domain with the fewest labels that has a
record, or
domainitself if none does.
@spec report_authorized?(Sovite.DNS.resolver(), String.t(), String.t()) :: {:ok, boolean()} | {:error, :temperror}
Returns whether rua_domain accepts reports about policy_domain
(RFC 7489 §7.1): always if both have the same Organizational Domain,
and otherwise if <policy_domain>._report._dmarc.<rua_domain> has a
TXT record starting with v=DMARC1.