RCPT TO stage
With the RCPT TO command, the client names one recipient of the message. It repeats the command for each further recipient, and the server decides about each one separately. A recipient can be accepted in two ways. As a local address, it is delivered to a mailbox on this server. As a remote address, it is relayed to another server. A recipient that is neither is refused. This stage therefore decides which mail the server takes responsibility for, and in particular whether it can be used to send mail to other domains.
The stage is configured on the MtaStageRcpt singleton (found in the WebUI under Settings › MTA › Session › RCPT TO Stage). How each domain handles its addresses is configured separately, on its Domain object (found in the WebUI under Management › Domains › Domains). The expressions of MtaStageRcpt can use the RCPT TO variables, which add rcpt, rcpt_domain, recipients, priority and helo_domain to the MAIL FROM variables.
Resolving a recipient
Section titled “Resolving a recipient”For a recipient in a domain hosted on the server, Stalwart must find out which mailbox, if any, the address belongs to. It tries the following steps in order, and the first step that matches decides:
- With sub-addressing, the part of the local part after
+is removed. - A masked address of an account, when it is enabled and not expired (Enterprise edition).
- For a domain whose accounts are kept in an LDAP or SQL directory, the directory is asked whether the address exists. OpenID Connect directories cannot be queried for recipients, so in such a domain only accounts that already exist on the server are accepted (OpenID Connect directory).
- The address of an account or group, or one of their aliases, is accepted and delivered to that mailbox. In a domain served by an LDAP or SQL directory, only the directory decides for accounts. The address of a mailing list is replaced by the list’s recipients.
- The domain’s catch-all address, when set, receives the message.
- When the domain allows relaying of unknown recipients, the recipient is accepted for relaying.
When no step matches, the recipient is refused with 550 5.1.1 Mailbox does not exist. Addresses, aliases and catch-all explains how these addresses are defined.
Recipients found in an LDAP or SQL directory are remembered in the directory recipient cache, so that the directory is not queried for every message. The size of the cache is set by directoryRecipients on the Cache singleton. Each entry is kept for directoryRecipientsTtl, 10 minutes by default.
The cache has a visible consequence. An account removed from the directory is still accepted at RCPT TO for up to that time, while a new account is found at once. When the directory cannot be reached, the recipient is deferred with 451 4.4.3 Unable to verify address at this time.
Relaying
Section titled “Relaying”Relaying means accepting a recipient in a domain that is not hosted on the server, and delivering the message to that domain’s mail servers. Users need it to send mail to other domains. allowRelaying decides whether the session may relay. The default allows relaying only for authenticated sessions:
{ "allowRelaying": {"else": "!is_empty(authenticated_as)"}}A remote recipient that the session may not relay to is refused with 550 5.7.1 Relay not allowed. Allowing unauthenticated relaying turns the server into an open relay, which spammers use to send mail through it. Some systems, such as an application server, need to send mail without authenticating. For them, allow relaying only from their address, for example with !is_empty(authenticated_as) || remote_ip == '192.0.2.10'.
Split delivery
Section titled “Split delivery”In split delivery, the mailboxes of one domain are spread over two servers: Stalwart hosts some of them and another server hosts the rest. Stalwart then has to accept addresses of the domain that it does not know, and pass them on. Setting allowRelaying to true on the Domain object does this: Stalwart accepts recipients of that domain that do not exist locally, and relays them. The outbound routing then sends them to the other server.
The two settings named allowRelaying answer different questions. The one on the Domain object concerns unknown recipients in a local domain. The expression of MtaStageRcpt concerns recipients in other domains.
Sub-addressing
Section titled “Sub-addressing”Sub-addressing, also called plus addressing, lets users receive mail at variants of their address. A message to [email protected], for example, is delivered to [email protected]. Users can hand out a different variant to each correspondent, and then sort or filter their mail by it.
The subAddressing field of the Domain object decides how a domain treats such addresses. It has three variants:
Enabled: the part of the local part from the first+is removed.Disabled: the local part is used as given, sojane.doe+newslettersmust exist as an address of its own.Custom: thecustomRuleexpression computes the local part to deliver to, from the original local part inrcpt.
The Custom variant allows other conventions than +. The following rule delivers [email protected] to [email protected]:
{ "subAddressing": { "@type": "Custom", "customRule": { "match": {"0": {"if": "matches('^([^.]+)\\.([^.]+)$', rcpt)", "then": "$2"}}, "else": "rcpt" } }}The backslash is doubled because the rule is written in a JSON document. In the expression field of the WebUI, it is written once, as \. (escaping backslashes).
When the recipient is accepted, it is rewritten to the address that receives the message, and the original address is kept as the original recipient of the message. The envelope recipient therefore no longer carries the sub-address. A user’s Sieve script that sorts by sub-address tests the To header instead (Sieve examples).
Catch-all addresses
Section titled “Catch-all addresses”A catch-all address receives mail for addresses of a domain that do not exist. Without one, unknown recipients are refused. A domain that names one in catchAllAddress accepts every recipient address, and delivers the mail for unknown recipients to it. This has effects on spam volume and on senders, which Catch-all address describes.
Rewriting recipients
Section titled “Rewriting recipients”A recipient address can also be replaced before it is resolved, for example to map one address convention to another. rewrite does this. When the expression returns an address, that address is used. Any other result, such as false, leaves the recipient unchanged.
The rewrite is applied after the Sieve script, milters and MTA hooks of this stage, so these see the address as the client gave it. The following expression delivers first.last@ addresses of local domains to first+last@:
{ "rewrite": { "match": {"0": {"if": "is_local_domain(rcpt_domain) && matches('^([^.]+)\\.([^.]+)@(.+)$', rcpt)", "then": "$1 + '+' + $2 + '@' + $3"}}, "else": "false" }}Address rewriting describes rewriting in general.
Sieve script
Section titled “Sieve script”script selects a system Sieve script to run for each recipient. The script runs before the recipient is rewritten and resolved. As a consequence, it also runs for recipients that are later refused as unknown. The script sees the recipient being added as envelope.to. It can refuse that recipient without affecting the other recipients of the message. The following script refuses recipients whose local part starts with noreply:
require ["envelope", "reject"];
if envelope :localpart :matches "to" "noreply*" { reject "550 5.1.1 This address does not accept mail.";}The following expression runs the script, installed under the name rcpt-filter, for unauthenticated sessions only:
{ "script": { "match": {"0": {"if": "is_empty(authenticated_as)", "then": "'rcpt-filter'"}}, "else": "false" }}On other servers, greylisting is often implemented in a script at this stage. Stalwart has it built in (Greylisting), so no script is needed for it.
Limits
Section titled “Limits”Three limits apply to recipients:
| Field | Default | Limit |
|---|---|---|
maxRecipients | 100 | Recipients per message. Further recipients are refused with 455 4.5.3 Too many recipients., and the client may send the message to them in a new transaction. |
maxFailures | 5 | Refused recipients per session. When reached, the session is closed with 451 4.3.0 Too many errors, disconnecting. |
waitOnFail | 5 seconds | Delay before answering a refused recipient. |
The delay and the failure limit work together against clients that try addresses to find out which ones exist: each wrong guess costs time, and too many wrong guesses end the session. Like the other settings, the limits are expressions. The following object body lowers the recipient limit for unauthenticated sessions:
{ "maxRecipients": { "match": {"0": {"if": "is_empty(authenticated_as)", "then": "25"}}, "else": "100" }}