Sovite.Core.Delivery (sovite v0.6.0)

Copy Markdown View Source

Delivers one job (a message to a group of recipients with the same destination). Runs in a task started by Sovite.Core.QueueManager.

The destination's transport (see Sovite.Core.Router) decides how:

  • :smtp - to another server, described below.
  • :lmtp - to a mailbox server such as Dovecot over LMTP (RFC 2033), on a Unix socket or a host. Each recipient gets its own status code after the data. LMTP connections are reused like SMTP ones.
  • :local and :mailbox - into a Maildir folder, from the maildir.local or maildir.mailbox template.
  • :pipe - to the command of a [pipe.<name>] config section, once per recipient, through Sovite.Pipe.

These four are final deliveries. A recipient that a Delivered-To: field of the message already names is a mail loop and fails with 5.4.6 (RFC 9228). Maildir and pipe deliveries get Return-Path: and Delivered-To: fields at the top (RFC 5321 §4.4); an LMTP server adds its own.

SMTP

For each SMTP job it:

  1. Finds the destination's addresses: MX hosts in preference order (Sovite.DNS.MX), then each host's addresses in the configured IP version order, at most delivery.max_addresses in total.
  2. Tries them in turn until one accepts a connection and completes the EHLO. A failure to connect, a rejected greeting, or a connection lost before the end of the data moves on to the next address (RFC 5321 §4.5.4.1).
  3. Runs the transaction and turns each recipient's reply into a result: 2xx delivered, 4xx deferred, 5xx failed.

A connection that ends in a clean state is handed back with the result, and the queue manager may give the worker another job for the same destination to send over it.

A server that greets with this server's own host name is a mail loop, and the job fails with 5.4.6.

TLS

Each destination has a TLS level, from delivery.tls_policy or else delivery.tls:

  • :none - never use TLS.
  • :may - opportunistic TLS (RFC 7435): STARTTLS when offered, without checking the certificate. If the handshake fails, the address is tried again without TLS.
  • :encrypt - TLS is required; the certificate is not checked.
  • :verify - TLS is required, and the certificate must be valid for the MX (or relay) host name, from a trusted CA.
  • :dane (the default) - DANE (RFC 7672): when DNSSEC authenticated the host's MX and address records and it has TLSA records, TLS is required and the certificate must match them; otherwise as :may. A failed TLSA lookup skips the host.

With :may and :dane, mail to a domain's MX hosts also follows the domain's MTA-STS policy (RFC 8461, see Sovite.Core.MTASTS), for hosts where DANE does not apply: DANE wins (RFC 8461 §2). In enforce mode, only MX hosts the policy lists are used, with TLS and a certificate valid for the host name from a trusted CA. In testing mode, delivery is opportunistic and problems are only reported.

When TLS is required and fails, the address is skipped with 4.7.4 (not offered) or 4.7.5 (handshake or certificate failure). Relay hosts on port 465 get implicit TLS.

A message received with REQUIRETLS (RFC 8689) is only sent over TLS verified with DANE or an MTA-STS policy (testing mode counts as enforce), or for relay hosts with a trusted certificate, and only to servers that offer REQUIRETLS; otherwise it fails with 5.7.30. A message with TLS-Required: No ignores the DANE and MTA-STS policies.

Extensions

With delivery.chunking (the default), messages go to servers that offer CHUNKING in BDAT chunks (RFC 3030). A binary message (BINARYMIME) can only go to servers that offer BINARYMIME; to others it fails with 5.6.3, as it is not converted. The message's DSN parameters (RFC 3461) go to servers that offer DSN; a recipient delivered to another server is then reported to the sender as "relayed" when it asked for success notifications, except where that server got the parameters and reports itself.

The MT-PRIORITY (RFC 6710) of a message goes to servers that offer it. So does a DELIVERBY deadline (RFC 2852), with the time left; a message that must be returned when it passes (R) only goes to such servers, and fails with 5.4.7 when no MX host offers it, while one with N that goes to another server is reported as "relayed" (also with T, trace). A recipient with RRVS (RFC 7293) goes with it to servers that offer it; to others, it fails with 5.7.19 (R) or goes without it (C).

With tls_rpt.reports, each session to an MX host is recorded for TLS-RPT (RFC 8460): the policy that applied and whether TLS worked.

When the destination has credentials (see Sovite.Core.Router), the client authenticates to the next hop, but only over TLS. With a source address for the address family, the connection is made from it.

Summary

Types

An open connection and the host it goes to, for reuse.

Work for one delivery: built by the queue manager.

Worker options

Functions

Runs job, reusing connection if given. Returns one result per recipient and the connection, if it can carry another message.

Types

connection()

@type connection() :: {Sovite.SMTP.Client.t(), String.t()}

An open connection and the host it goes to, for reuse.

job()

@type job() :: %{
  queue_id: String.t(),
  destination: Sovite.Core.Router.destination(),
  recipients: [String.t(), ...],
  sender: String.t(),
  body_type: :"7bit" | :"8bitmime" | :binarymime | nil,
  path: Path.t(),
  message_offset: non_neg_integer(),
  message_size: non_neg_integer(),
  prefix: binary(),
  requiretls: boolean(),
  smtputf8: boolean(),
  xforward: Sovite.SMTP.Server.Session.xforward() | nil,
  dsn: Sovite.SMTP.Client.dsn() | nil,
  chunking: boolean(),
  priority: -9..9,
  deliver_by: Sovite.Queue.Envelope.deliver_by() | nil,
  rrvs: %{required(String.t()) => Sovite.SMTP.Params.rrvs()}
}

Work for one delivery: built by the queue manager.

opts()

@type opts() :: %{
  hostname: String.t(),
  resolver: Sovite.DNS.resolver(),
  port: :inet.port_number(),
  families: [:a | :aaaa, ...],
  max_addresses: pos_integer(),
  client: keyword(),
  tls: %{
    default: tls_level(),
    policy: %{required(String.t()) => tls_level()},
    cacerts: [binary()] | nil
  },
  maildir: %{optional(:local | :mailbox) => String.t() | nil},
  pipes: %{required(String.t()) => map()},
  delimiter: String.t(),
  tmp_dir: Path.t(),
  mta_sts: GenServer.server() | nil,
  tls_reports: Sovite.Core.Repo.t() | nil,
  ip_pools: atom() | nil
}

Worker options:

  • :maildir - %{local: template, mailbox: template}, Maildir path templates with {user}, {domain}, and {address}, or nil.

  • :pipes - pipe commands by name, from the [pipe] config section.

  • :delimiter - the address extension delimiter.

  • :tmp_dir - where pipe deliveries write the message for the command to read.

  • :hostname - this server's name, for EHLO and loop detection.

  • :resolver - a Sovite.DNS resolver.

  • :port - SMTP port for MX deliveries. 25 except in tests.

  • :families - [:aaaa, :a] and similar, see Sovite.DNS.MX.resolve/3.

  • :max_addresses - addresses to try per job.

  • :client - options for Sovite.SMTP.Client.connect/3.

  • :tls - %{default, policy, cacerts}: the default level, a map of destination (domain, relay host, or address literal) to level, and the CAs for :verify (nil for the system's).

  • :mta_sts - the Sovite.Core.MTASTS server, or nil to ignore MTA-STS policies.

  • :tls_reports - the Sovite.Core.Repo reference to record TLS-RPT outcomes in, or nil.

result()

tls_level()

@type tls_level() :: :none | :may | :encrypt | :verify | :dane

Functions

run(job, connection, opts)

@spec run(job(), connection() | nil, opts()) :: {[result()], connection() | nil}

Runs job, reusing connection if given. Returns one result per recipient and the connection, if it can carry another message.