Strategies
Once a message has been accepted and queued, Stalwart has to deliver it, and delivery involves several separate decisions. The server must decide where the message goes, how often to try again when an attempt fails, how to open the connection to the remote server and how strictly to insist on TLS. Outbound strategies are the settings that make these decisions, and they are the place to change when mail must leave through a relay, follow a different retry schedule or use a particular local address.
Stalwart makes these decisions separately for each recipient of the message. Each decision is made by an expression, a rule evaluated at delivery time that returns the name of a strategy object. Because the expression can test the recipient’s domain, the sender, the kind of message, the number of attempts already made or the error of the last attempt, the delivery policy can vary along any of these. The recipients of a single message can therefore take different paths: some are delivered to local mailboxes, others to the MX hosts of their domains, and others through a relay.
Strategy expressions
Section titled “Strategy expressions”The four expressions are fields of the MtaOutboundStrategy singleton (found in the WebUI under Settings › MTA › Outbound › Strategy). Each one returns the name of an object of a different type, and that object holds the actual settings:
| Field | Returns the name of | Decides | Default |
|---|---|---|---|
route | An MtaRoute | Where the message goes: a local mailbox, the domain’s MX hosts, or a relay (Routing). | 'local' for local domains, otherwise 'mx' |
schedule | An MtaDeliverySchedule | The virtual queue, the retry intervals, the delay notifications and the expiry (Scheduling). | 'local' for local domains, 'dsn' for delivery status notifications, 'report' for reports, otherwise 'remote' |
connection | An MtaConnectionStrategy | The local IP address, the EHLO name and the timeouts (Connection). | 'default' |
tls | An MtaTlsStrategy | Whether STARTTLS, DANE and MTA-STS are required, and certificate validation (TLS). | 'invalid-tls' when the previous attempt failed with a TLS error, otherwise 'default' |
The expressions differ in the variables they can use, because they are evaluated at different moments of a delivery. route and schedule are evaluated for a recipient, with the recipient variables. Among them are rcpt, rcpt_domain, sender, sender_domain, received_via_port, received_from_ip and retry_num. The source variable tells how the message entered the queue: authenticated, unauthenticated, dmarc_pass, dsn, report, autogenerated or unknown. The last_error variable holds the kind of error of the previous attempt: dns, connection, tls, dane, mta-sts, unexpected-reply, rate, concurrency, io or none.
connection and tls are evaluated for a remote host, with the host variables. These add mx, the name of the host being delivered to.
The tls expression is evaluated twice, and the two evaluations decide different things. It is first evaluated once for each recipient domain, before the MX hosts have been looked up, so mx is still empty. That first evaluation decides whether the domain’s MTA-STS policy is fetched and applied. The expression is then evaluated again for each MX host, and this second evaluation decides the STARTTLS, DANE and certificate settings for that host. A strategy that changes the MTA-STS setting must therefore be selected with recipient variables such as rcpt_domain, not with mx, because mx has no value when the MTA-STS decision is made.
A new installation already has every strategy object that the default expressions name:
- The routes
localandmx - The schedules
local,remote,dsnandreport, with virtual queues of the same names - The connection strategy
default - The TLS strategies
defaultandinvalid-tls
The invalid-tls strategy accepts invalid certificates. The default tls expression selects it after an attempt that failed because of the remote server’s certificate, which means that the delivery is retried without validating the certificate.
Missing strategies
Section titled “Missing strategies”An expression can return a name for which no object exists. Delivery does not fail in that case. Stalwart logs the warning smtp.id-not-found (Strategy not found), with the name in question, and delivers under a built-in fallback:
| Strategy | Fallback |
|---|---|
| Route | MX delivery, up to 5 MX hosts and 2 addresses per host. The names local and mx fall back silently to local and MX delivery. |
| Schedule | Retries after 2, 5, 10, 15 and 30 minutes, 1 hour and then every 2 hours; delay notifications after 1 and 3 days; expiry after 5 days; the default virtual queue. |
| Connection | No local IP address or EHLO name of its own; 5 minutes for each step of the SMTP dialogue and 10 minutes for the message data. |
| TLS | STARTTLS, DANE and MTA-STS optional; certificates validated. |
For the schedule, connection and TLS strategies, the name default falls back without a warning. Any other missing name means that the delivery runs under the fallback rather than under the policy the expression intended. A smtp.id-not-found warning therefore points to a configuration error that needs correcting.
Recipes
Section titled “Recipes”The following recipes change the route expression for common delivery arrangements. Each keeps the first branch of the default, which sends mail for local domains to the local route.
Send all outgoing mail through a smart host
Section titled “Send all outgoing mail through a smart host”A smart host is a relay that accepts all outgoing mail and delivers it on the server’s behalf. To use one, create a relay route named smarthost and route every remote recipient to it:
{ "route": { "match": {"0": {"if": "is_local_domain(rcpt_domain)", "then": "'local'"}}, "else": "'smarthost'" }}Route by the port a message was received on
Section titled “Route by the port a message was received on”received_via_port holds the local port on which the message was received, and source tells whether its sender authenticated. The following expression relays mail submitted on port 587 through smarthost and delivers everything else directly:
{ "route": { "match": { "0": {"if": "is_local_domain(rcpt_domain)", "then": "'local'"}, "1": {"if": "received_via_port == 587", "then": "'smarthost'"} }, "else": "'mx'" }}Route by sender domain
Section titled “Route by sender domain”A server that hosts several domains can send the mail of each through a different path. The following expression relays the mail of one hosted domain through a different provider:
{ "route": { "match": { "0": {"if": "is_local_domain(rcpt_domain)", "then": "'local'"}, "1": {"if": "sender_domain == 'example.net'", "then": "'relay-example-net'"} }, "else": "'mx'" }}Fall back to a relay after failed attempts
Section titled “Fall back to a relay after failed attempts”Because the expression can test retry_num, it can change the route once direct delivery has failed. The following expression tries direct delivery twice and then hands the message to a relay named fallback:
{ "route": { "match": { "0": {"if": "is_local_domain(rcpt_domain)", "then": "'local'"}, "1": {"if": "retry_num > 1", "then": "'fallback'"} }, "else": "'mx'" }}Delivering some recipients of a local domain to another server is a different arrangement, called split delivery, and is described in Split delivery.