Sovite.Core.MailAuth (sovite v0.6.0)

Copy Markdown View Source

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 FROM identity (and the HELO identity, with spf.helo) when the client sends MAIL. With spf.reject_fail, a fail is refused with 550 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 is reject is refused with 550 5.7.26, and one whose policy is quarantine is put on hold. An unbroken ARC chain sealed last by one of arc.trusted_sealers overrides 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.

Options from the configuration, see opts/3.

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

context()

@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.

opts()

@type opts() :: map()

Options from the configuration, see opts/3.

spf()

@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

check_spf(opts, ip, helo, sender)

@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.

finish(work, context)

@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}.

opts(config, repo, resolver)

Options from the running configuration.

sign_message(opts, from, message)

@spec sign_message(opts(), String.t(), iodata()) :: [String.t()]

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.

start(opts, received, fields, context)

@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.

update(work, data)

@spec update(map() | nil, iodata()) :: map() | nil

Feeds body data.