DATA stage
The DATA stage is where the message itself arrives, and where most of the work on it is done. The client transfers the message with the DATA command, or with one or more BDAT commands when it uses chunking. When the transfer ends, Stalwart checks, filters and signs the message. It then either queues the message and answers 250, or refuses it. Because the client is still connected, a refusal at this stage reaches the sender at once.
The stage is configured on the MtaStageData singleton (found in the WebUI under Settings › MTA › Session › DATA Stage). Its expressions can use the RCPT TO variables. In these expressions, rcpt and rcpt_domain refer to the last recipient.
Order of processing
Section titled “Order of processing”After the message has been received, the following steps run in order. A step that refuses the message ends the processing, and the client receives that step’s reply:
- Size and header limits: the message limits below.
- Loop detection: a message with more
Receivedheaders thanmaxReceivedHeadersis refused. - DKIM: the signatures of the message are verified.
- ARC: the authentication chain of forwarded messages is verified.
- DKIM2: DKIM2 signatures are verified, and DKIM2-signed delivery status notifications that fail verification are refused.
- DMARC: the DMARC policy of the
Fromdomain is evaluated, using the SPF result ofMAIL FROMand the DKIM results. Messages with several author domains are refused. - Reports: DMARC, TLS and abuse reports addressed to the report addresses are analysed.
- Trace headers: the
Received,Authentication-ResultsandReceived-SPFheaders are prepared (Headers). - Spam filter: the message is scored, and the
X-Spam-ResultandX-Spam-Scoreheaders are added. A message above the reject or discard threshold is refused or dropped here. - Milters, then MTA hooks: external filters can accept, reject or modify the message.
- Sieve script: the DATA-stage system script runs on the message as modified by the milters and hooks.
- Final headers:
Return-Pathis added when configured, andDateandMessage-IDwhen configured and missing. - Queue: the queue quotas are checked, the message is signed with DKIM and DKIM2, and it is queued. The client receives
250 2.0.0 Message queued with id <id>.
The order explains what each step can rely on. The authentication checks run before the spam filter, so their results are among the evidence that the filter scores. Milters, MTA hooks and the Sieve script receive the message as the client sent it, without the headers added in steps 8 and 9. The Sieve script runs after the milters and hooks, so it sees their changes, and it receives the authentication results and the spam score as variables.
Not every step runs for every message. Steps that a setting disables are skipped, such as DMARC on the submission ports or the spam filter for authenticated sessions. Messages submitted by users go through the same steps, but with the default settings DMARC is evaluated only on port 25, and the spam filter runs only for unauthenticated sessions.
Message limits
Section titled “Message limits”The first steps check the size and structure of the message, before any other work is done on it, against the following limits:
| Field | Default | Limit | Reply when exceeded |
|---|---|---|---|
maxMessageSize | 100 MiB | Size of a message, advertised to clients with SIZE. 0 removes the limit and the advertisement. | 552 5.3.4 Message too big for system. |
maxMessages | 10 | Messages per session. | 452 4.4.5 Maximum number of messages per session exceeded. |
maxReceivedHeaders | 50 | Received headers in a message, a sign of a mail loop when exceeded. | 450 4.4.6 Too many Received headers. Possible loop detected. |
maxHeaderSize on Email | 512 KiB | Size of the top-level header section. | 552 5.3.4 Message header too large. |
maxHeaderCount on Email | 5000 | Header fields in the top-level header section. In addition, a message may have at most 65534 header fields across all its parts. | 552 5.3.4 Message header too large. |
Each field of MtaStageData is an expression, a rule evaluated for each message that returns the value to apply. The size limit can therefore differ, for example, between port 25 and the submission ports. The following object body accepts messages up to 50 MiB on port 25 and up to 25 MiB elsewhere:
{ "maxMessageSize": { "match": {"0": {"if": "local_port == 25", "then": "52428800"}}, "else": "26214400" }}The header limits work differently. They are fields of the Email singleton, not of MtaStageData, and they apply to every protocol (Message and mailbox limits). At the DATA stage, they apply to messages received from SMTP clients. An exceeded limit is logged as smtp.message-too-large.
The size limit of the DATA stage applies to messages in transit. The size of a message stored in a mailbox is limited separately, by maxMessageSize on the Email singleton.
Spam filtering
Section titled “Spam filtering”enableSpamFilter decides whether the spam filter runs for a message. The default, is_empty(authenticated_as), filters mail from unauthenticated sessions and skips mail from authenticated users. The filter can also be limited to particular listeners. The following object body filters only the mail received on the listener named smtp:
{ "enableSpamFilter": {"else": "listener == 'smtp'"}}Sieve script
Section titled “Sieve script”A system Sieve script at this stage can act on the complete message, which the earlier stages never see. script selects the script to run on the message. The script sees the following:
- The whole message as the client sent it, with the changes made by milters and MTA hooks, but without the trace and spam filter headers that Stalwart adds
- All the recipients, in
envelope.to - The authentication results and the spam score, as environment variables (Variables)
The script can reject the message or discard it. It can also modify it with addheader, deleteheader, replace and the other editing extensions, and the modified message is the one that is queued.
The same script runs for messages received from other servers and for messages submitted by users. A script meant only for outgoing mail must therefore tell the two apart: it tests env.authenticated_as, or it is selected only for the submission ports. The following expression runs a script named outgoing for authenticated sessions only:
{ "script": { "match": {"0": {"if": "!is_empty(authenticated_as)", "then": "'outgoing'"}}, "else": "false" }}Header rewriting, Content rewriting and Sieve examples give scripts for common tasks.
DKIM signing
Section titled “DKIM signing”Signing a message with DKIM lets receiving servers verify that the signing domain sent it. dkimSignDomain decides which domain signs the message with DKIM and DKIM2, or returns false for no signature. The default signs with the sender’s domain when the session is authenticated and the domain is hosted on the server:
{ "dkimSignDomain": { "match": {"0": {"if": "is_local_domain(sender_domain) && !is_empty(authenticated_as)", "then": "sender_domain"}}, "else": "false" }}As a result, relayed messages from unauthenticated sessions are not signed. DKIM signing describes how to change the expression and how the keys of the domain are selected.
Headers
Section titled “Headers”Stalwart adds trace headers to the messages it accepts. These headers record where a message came from and how it was authenticated. Each of the following fields is an expression, and by default each adds its header only on port 25, where messages arrive from other servers:
| Field | Header |
|---|---|
addReceivedHeader | Received, with the client’s name and address and the TLS version used. |
addAuthResultsHeader | Authentication-Results, with the SPF, DKIM, DKIM2, DMARC and reverse DNS results. |
addReceivedSpfHeader | Received-SPF, with the SPF result for the sender, when SPF was checked. |
addReturnPathHeader | Return-Path, with the envelope sender. |
addDateHeader | Date, when the message has none. |
addMessageIdHeader | Message-ID, when the message has none. |
One header is controlled differently. addDeliveredToHeader is a plain Boolean rather than an expression, and it is enabled by default. It adds a Delivered-To header with the recipient’s address when a message is stored in a local mailbox.
Mail clients normally create the Date and Message-ID headers themselves. Some applications submit messages without them, and for these the two headers can be enabled on the submission ports as well. The following object body adds them for every message:
{ "addDateHeader": {"else": "true"}, "addMessageIdHeader": {"else": "true"}}