Receiving mail
What the server does with a message that arrives on port 25, and which checks you can turn on, off or tighten.
The receiving server (IncomingServer and IncomingMailHandler) listens on port 25 and handles mail from other servers. Authentication is disabled on this port, because senders are other mail servers, not your users.
Which checks run, and what happens when one fails, is set in server/config/mail.config.json. The file reloads when you save it. See Configuration files.
The pipeline
When an email arrives on port 25, it goes through four stages.
1. Connect
- Optional reverse DNS check (
incoming.checks.reverseDns). The client's IP must have a PTR record, and that name must resolve back to the same IP. It is off by default, and when it fails it tags the message. - Clients in
incoming.trustedNetworks(loopback by default) skip the checks and the rate limit.
2. MAIL FROM
- Applies a per-IP rate limit (
incoming.rateLimit). Going over it counts as a strike against the IP. See Blocking abusive IPs. - Rejects
.temp@addresses and known disposable sender addresses (checks.disposableSender). - Rejects the sender when its domain has no MX, A or AAAA record (
checks.senderDomain). It answers with a temporary451if DNS is down, so the sender retries. - Accepts a null sender (
MAIL FROM:<>) without the domain check, because delivery failure notices (bounces) are sent that way.
3. RCPT TO
- Rejects the recipient when its domain is not in
incoming.recipients.acceptedDomains. An empty list accepts any domain. - Rejects the recipient when it is not in
incoming.recipients.knownRecipients. An empty list accepts any user. - Enforces
incoming.maxRecipients. - Accepts a recipient at
BOUNCE_DOMAINwhatever the two lists say, and hands it to the bounce processor.
4. DATA
- Enforces
incoming.maxMessageSizeBytes. - Runs DKIM, SPF and DMARC checks with
mailauth, in a worker thread, against the connecting client's IP address and its HELO name, on the original unmodified message. - Applies the action you set for each result (see Policy actions).
- Removes any
X-AE-*headers and forgedAuthentication-Resultsheaders that the sender supplied, then adds the server's own. - Adds the
incoming.customHeadersafter the checks. - Seals the message with ARC headers when
ARC_PRIVATE_KEY_PATHandARC_SIGNING_DOMAINare set. This keeps authentication working when mail is forwarded. - Encrypts the final message with PGP for storage when the PGP keys are set. See PGP encryption.
Custom headers are added after the checks
The X-AE-* headers are added only after authentication has finished, so they never break a DKIM signature.
Blocking abusive IPs
An IP that sends more than incoming.rateLimit.maxMessagesPerMinute messages in a minute gets a strike. Each strike blocks it for longer, and the last one blocks it for good:
| Strike | Default block | Reply to every connection from that IP |
|---|---|---|
| 1 | 15 minutes | 421 4.7.0 try again in N seconds |
| 2 | 60 minutes | 421 4.7.0 try again in N seconds |
| 3 | permanent | 554 5.7.1 blocked, contact the postmaster |
A temporary block is a 421, so a real mail server only waits and retries. A blocked IP is refused as soon as it connects, before any other check runs.
Strikes are kept in the ip_blocks table of the server's SQLite database, MAIL_DATA_DIR/server.db, so a block survives a restart. An IP that stays clean for strikeResetHours (24 by default) starts again from strike 1. Networks in incoming.trustedNetworks are never limited or blocked.
temporaryBlockMinutes has one entry per temporary block, and the next strike is permanent. [15, 60] gives two temporary blocks, then a permanent one. [] blocks for good on the first strike. With enabled: false, going over the limit only gives short temporary blocks that are kept in memory.
To see and change the list, run the CLI in the server folder, or in the container:
Changes apply at once, without a restart. In Docker the file is on the inbound-data volume.
Policy actions
Each SPF, DKIM, DMARC, reverse DNS and sender-domain result has an action:
| Action | Result |
|---|---|
accept | Ignore the result. |
tag | Accept the message, and add X-AE-Spam-Flag: YES and X-AE-Policy-Flags to it. |
tempfail | Reply 4xx, so the sender retries later. |
reject | Reply 5xx with an enhanced status code from RFC 7372. |
By default the server rejects mail that fails DMARC with p=reject, tags mail that fails SPF, DKIM or reverse DNS, and accepts everything else. A DNS error never causes a hard reject. It becomes a temporary error. The full list of checks, settings and reply codes is in Configuration files.
Delivery reports
A delivery report (a multipart/report message of type delivery-status) sent to a normal mailbox is delivered as usual. The DSN processor also reads it, matches it to the message you sent, and records it. A report that arrives this way never adds an address to the suppression list, because it cannot be verified. See Bounces.
Settings
| Setting | Where | Purpose |
|---|---|---|
INCOMING_MAIL_HOST | server/.env | Hostname the receiving server announces. |
ARC_PRIVATE_KEY_PATH, ARC_SIGNING_DOMAIN, ARC_SELECTOR | server/.env | Optional ARC sealing. |
PGP_* | server/.env | Optional encryption at rest. |
incoming.* | server/config/mail.config.json | Limits, checks, accepted domains, trusted networks and custom headers. |
GREETING, RELAY_DENIED, DMARC_REJECT and others | server/config/smtp-messages.json | The text and code of each reply. |
See Environment variables for defaults.
Components involved
| Component | File | Purpose |
|---|---|---|
IncomingMailHandler | server/src/services/IncomingMailHandler.ts | Handles MAIL FROM, RCPT TO and DATA. |
IncomingPolicy | server/src/services/IncomingPolicy.ts | The reverse DNS, sender, recipient, SPF, DKIM and DMARC decisions. |
MailAuth | server/src/server/config/MailAuth.ts | DKIM verification, SPF, DMARC policy, MTA-STS and ARC sealing. |
MailComposer | server/src/server/config/MailComposer.ts | Builds RFC 5322 headers and MIME messages. |
DNSChecker | server/src/server/config/DnsChecker.ts | Resolves MX, SPF, DKIM, DMARC and TXT records and checks TCP ports. |
SpamFilteration | server/src/server/config/SpamFilteration.ts | Per-IP rate limiting with temporary blocks for abusive clients. |
PGPService | server/src/server/config/encryption/PGPService.ts | Key generation and message encryption. |
Storing the message
When IMAP_LMTP_HOST is set, the checked message (with the authentication and policy headers added above) is handed to the IMAP server over LMTP. It appears in the recipient's mailbox at once, in any mail client that is already open.
- Mail tagged by a check (
X-AE-Spam-Flag: YES) is filed in the recipient's Junk folder instead of the inbox. - If every recipient's mailbox takes the message, the server replies
250. - If the IMAP server is down or busy, or any recipient hits a temporary error, it replies
450for the whole message, so the sender retries later and nothing is lost. Mailboxes that already have the message do not store it again: the IMAP server keeps each delivered Message-ID per user in a SQLite file for 24 hours, and drops it when the user deletes the message. - If some recipients have a mailbox and others do not (unknown user, or IMAP turned off), the message is accepted and delivered to those who do. The refusals are logged, and the sender is not told.
- If no recipient has a mailbox, it replies
550 5.1.1. If every mailbox is full, it replies552.
Without IMAP_LMTP_HOST the server still checks, labels and accepts mail, but does not store it.