Email authentication for Sovite.Core.SMTPHandler: SPF, DKIM, ARC,
and DMARC on mail from outside, DKIM signing on mail from users, and
ARC sealing on mail this server forwards.
Mail from outside is mail from clients that are neither in
smtp.trusted_networks nor authenticated, except over LMTP, where the
client is an MTA that has checked the mail already. For it:
- SPF checks the
MAIL FROMidentity (and theHELOidentity, withspf.helo) when the client sendsMAIL. Withspf.reject_fail, afailis refused with550 5.7.23. - DKIM signatures and the ARC chain are verified while the body is
received, and DMARC is evaluated at the end of the data. With
dmarc.policy = "enforce", a failing message whose policy isrejectis refused with550 5.7.26, and one whose policy isquarantineis put on hold. An unbroken ARC chain sealed last by one ofarc.trusted_sealersoverrides the policy, so mailing lists and forwarders that seal keep working. - The results go into an
Authentication-Results:field at the top of the message (RFC 8601). - When the message is forwarded to another domain, it is ARC sealed
with
arc.seal.
Fields claiming to be Authentication-Results: of this server
(server.authserv_id) are removed from every message, so users never
see forged results (RFC 8601 §5).
Mail from users (trusted or authenticated) is DKIM signed with every
signing key of the From: domain, or of its closest parent domain that
has one, so a domain with RSA and Ed25519 keys gets both signatures.
All headers this module adds are passed to Sovite.Queue.Spool.commit/2
as the message prefix: they are computed only once the whole message
has been received.
Telemetry
[:sovite, :smtp, :message, :authenticated]-%{},%{session_id, queue_id, spf, dkim, arc, dmarc, disposition}, once per message from outside.
Summary
Types
The SMTP transaction the message came in: the client address and
HELO name, the MAIL FROM address, the authentication identity and
whether the client is trusted, whether some recipient is forwarded to
another domain, and the message's tenant (see Sovite.Core.Tenants),
which may be left out.
SPF results from MAIL: the MAIL FROM identity and, if checked, HELO.
Functions
Checks SPF for the MAIL FROM address sender (and the HELO name,
with spf.helo). Returns {:ok, results}, {:ok, nil} when SPF is
off, or {:reject, reply, results} for a fail with
spf.reject_fail.
Finishes the checks at the end of the data. Returns the header fields
to add at the top of the message and what to do with it: :accept,
{:hold, reason}, or {:reject, reply}.
Options from the running configuration.
DKIM signs a message Sovite writes itself, such as a report, with the
keys of the domain of from (or of its closest parent with keys).
Returns the DKIM-Signature: fields to put at the top, for the prefix
of Sovite.Queue.Spool.commit/2.
Plans the work for a message once its header section is complete.
received are the header fields as they came in, fields as they will
be stored (after submission fixes and rewriting). Returns nil when
there is nothing to do.
Feeds body data.
Types
@type context() :: %{ session_id: String.t(), queue_id: String.t() | nil, ip: :inet.ip_address(), helo: String.t() | nil, sender: String.t(), recipients: [String.t()], inbound: boolean(), forwarded: boolean(), spf: spf() | nil, tenant: String.t() | nil }
The SMTP transaction the message came in: the client address and
HELO name, the MAIL FROM address, the authentication identity and
whether the client is trusted, whether some recipient is forwarded to
another domain, and the message's tenant (see Sovite.Core.Tenants),
which may be left out.
@type opts() :: map()
Options from the configuration, see opts/3.
@type spf() :: %{mail_from: Sovite.SPF.Result.t(), helo: Sovite.SPF.Result.t() | nil}
SPF results from MAIL: the MAIL FROM identity and, if checked, HELO.
Functions
@spec check_spf(opts(), :inet.ip_address(), String.t() | nil, String.t()) :: {:ok, spf() | nil} | {:reject, Sovite.SMTP.Reply.t(), spf()}
Checks SPF for the MAIL FROM address sender (and the HELO name,
with spf.helo). Returns {:ok, results}, {:ok, nil} when SPF is
off, or {:reject, reply, results} for a fail with
spf.reject_fail.
@spec finish(map() | nil, context()) :: {[String.t()], :accept | {:hold, String.t()} | {:reject, Sovite.SMTP.Reply.t()}}
Finishes the checks at the end of the data. Returns the header fields
to add at the top of the message and what to do with it: :accept,
{:hold, reason}, or {:reject, reply}.
@spec opts(Sovite.Core.Config.t(), Sovite.Core.Repo.t() | nil, Sovite.DNS.resolver()) :: opts()
Options from the running configuration.
DKIM signs a message Sovite writes itself, such as a report, with the
keys of the domain of from (or of its closest parent with keys).
Returns the DKIM-Signature: fields to put at the top, for the prefix
of Sovite.Queue.Spool.commit/2.
@spec start( opts(), [Sovite.Message.Headers.field()], [Sovite.Message.Headers.field()], context() ) :: map() | nil
Plans the work for a message once its header section is complete.
received are the header fields as they came in, fields as they will
be stored (after submission fixes and rewriting). Returns nil when
there is nothing to do.
Feeds body data.