Skip to main content
This page is the operator companion to the SMTP architecture page: the ports to open, the [smtp] configuration, the queue you manage, and the workflows around sending domains. The first-boot wizard writes a working baseline for all of it.

Listeners and ports

Inbound (25) and submission (587/465) are separate code paths with different rules — untrusted mail is authenticated and scored, user mail is identity-validated and queued. Never proxy these ports; they bind directly even behind a reverse proxy.

The SMTP session

What every listener does with a connection, whatever its port:
  • A message ends at <CRLF>.<CRLF>, and nowhere else (RFC 5321 §4.1.1.4). The dot line that closes DATA used to be recognised after a bare LF too, so body<LF>.<CRLF>MAIL FROM:… ended the message and the bytes after it were read as a second transaction with a forged envelope (SMTP smuggling). A bare-LF line is still accepted as body text, as Postfix does by default. Every line that begins with a period loses that period (§4.5.2), not only .., and a NUL octet in DATA is refused 554 5.6.0, since the body is text. A line over 1000 octets is still delivered intact.
  • The inbound door answers as RFC 5321 says. VRFY answers 252 and discloses nothing (a bare VRFY is 501), EXPN and HELP 502, NOOP ignores its text, QUIT with an argument is 501; all of them used to be 500. RCPT TO reads its path with the grammar MAIL FROM uses: a malformed path is 501 5.1.3 (it was 550 No such user), a path without angle brackets is refused, <Postmaster> without a domain is accepted, and a needlessly quoted local part is its plain form ("jo"@example.com is jo@example.com). A non-ASCII address without SMTPUTF8 on MAIL FROM is 553 5.6.7 (RFC 6531 §3.5). REQUIRETLS is advertised only once the session is encrypted; asked on a clear session, it is 530 5.7.10 (RFC 8689 §4).
  • postmaster@ and abuse@ reach every mail domain of an organization (RFC 2142), not only its primary domain, without a hand-made alias: on one production box a secondary domain had refused postmaster 531 times in three days. oximail doctor reports role_addresses, a failure naming the domains of an organization that has no account at all, the one case still refused. A read error on the target’s Inbox is now 451, where it was swallowed into a 550.
  • The five-minute limit is an inactivity limit. Every command and every read of the body re-arms it (RFC 5321 §4.5.3.2); it used to cut a session five minutes after it opened, however busy. A silent peer is told 421 4.4.2 before the close, instead of an empty reply. A session lives one hour at most, and the journal says which of the two limits ended it; both count in oximail_smtp_inbound_timeout_total.
  • A 250 after the data means the message survives a power loss. Right before that reply, the server syncs the database’s write-ahead log to disk (fdatasync, outside the write lock, so only the client that asked waits; 2.5 ms measured on a production disk). The database alone survives a crash of the process, but a power cut or a crash of the host could roll its last commits back, and RFC 5321 §6.1 forbids losing a message after the 250. A sync that fails answers 451 instead: the sender retries, which gives at worst a duplicate, never a loss. When an SMTP door is durable, the relay spool syncs its body file and its directory too. See durable_doors below.
  • A restart no longer accepts a connection it then cuts. At the stop, the SMTP listeners (and the IMAP and HTTPS ones) stop accepting, so the kernel refuses what comes next. A session waiting for its next command is told 421 4.3.2; a DATA or BDAT in progress finishes. The drain waits for the workers and the open sessions within one 10-second budget, and when it runs out the journal names what it was still waiting for. Before, the listeners kept accepting during the stop and then cut those connections: a message stored but cut before its 250 could be sent twice.
  • No 40 ms wait per reply. Every accepted connection disables Nagle’s algorithm, on every listener. An IMAP SELECT, STATUS, LIST or FETCH used to wait 40 ms for the client’s delayed acknowledgement (44 ms measured each, on a box with no proxy in front).
  • The journal line of an accepted message says what it was judged on. The message accepted line carries the judged client address (client_ip: the real client behind a backup MX) and the spf, dkim and dmarc verdicts, in the same words as the Authentication-Results stored with the message, beside the queue id, the envelope sender, the delivered and failed counts, the spam score and whether TLS was used.
[storage] durable_doors lists the replies that take the durability barrier: smtp-inbound and smtp-submission (the 250 after the data) and smb (a FLUSH on the file share). All three are on by default; the key exists to take one back. An unknown name refuses the configuration, and oximail_durability_barrier_seconds{door,result} shows what durability costs on the box’s disk.

Sending domains and DKIM keys

Each domain the server sends for has its own signing key, declared as an array entry:
Adding a sending domain is a three-step ritual: generate the key (oximail setup dkim --domain <d>), add the block above, and publish the DNS record — a key without its published TXT record signs mail that receivers reject. oximail check-dns --domain <d> confirms the full record set; the email authentication page explains each record. An internationalized domain is matched in its ASCII form (A-label, xn--…, RFC 8616): a From: written in Unicode is signed by the key configured for that domain, and a key declared with a Unicode domain signs with its A-label as d=; an invalid IDN in [[smtp.dkim_keys]] stops the boot. Such a message used to leave unsigned. A message a user’s rule forwards out of the server is signed with a hosted domain’s key only when this server authenticated its author (a local submission, a server notice, or external mail that passed DMARC on the way in); otherwise it leaves unsigned, so a forged From: of a hosted domain cannot be laundered into a valid signature through a redirect. Submission validates both the envelope MAIL FROM and the body From: against the authenticated account’s identities — users cannot send as addresses they do not own, and additional sendable addresses are managed as aliases (oximail alias, ADR-089). The answers at end of DATA say which rule failed: a From: that is not an identity is 550 5.7.1; a message without a usable From:, or carrying more than one From:, Sender: or Date: field, or several From: mailboxes without a Sender:, is 554 5.6.0 (RFC 5322 §3.6). A message with two From: fields is never signed, whichever way it reaches the outbound worker. What the client left out, the server completes rather than refuses (RFC 6409 §8): a missing Date: or Message-ID: is added, and every submitted message carries a Received: line for its submission hop (RFC 5321 §4.4: the line is always there, it counts the hops). That line names no client by default: [smtp] submission_trace = "private" writes from authenticated-client, so the recipients no longer learn the name and the address of the sender’s workstation; the address stays in the server’s journal. "full" writes the EHLO name and the address, as for any other hop, with an IPv6 client written [IPv6:…] (RFC 5321 §4.1.3). Any other value refuses to boot. The authenticated relay keeps its full trace whatever this says.

Routing outbound: transport maps

By default, outbound mail resolves the destination’s MX and delivers directly. [[smtp.transport_maps]] overrides routing per destination pattern — the mechanism behind relay/smarthost sending:
An authenticated route is TLS-mandatory by construction. Delivery through a route still runs the same extension mediation as direct delivery.

The outbound queue

Every delivered message carries its Return-Path: and a for clause in the Received: line the server adds, so the envelope sender is written where bounces, list unsubscribes, SPF/DMARC replay and a migration can read it; the MAIL command refuses what is not a reverse-path (<a@b@c>, a bare address without brackets) and the parameters it does not implement (BODY=BOGUS, SIZE=abc), per the RFC 5321 §4.1.2 grammar, where it used to answer 250 to all of them. Every submission lands in the persistent delivery queue; a worker retries with exponential backoff (1 min → 24 h) until the 5-day give-up window expires, then bounces with an RFC 3464 DSN. Manage it with:
Mail the queue owes to a sub-address of a local account (name+tag@example.com) is delivered to that account. The queue now resolves an address the way the arrival door does (RFC 5233: the tag dropped first, then accounts, aliases and the domain wildcard), at every point where it decides local or remote, the reports and delay notices returned to a sub-addressed sender included. It used to look the exact address up, find nothing, send the message over SMTP to this very server and meet the loop guard until the queue gave up. When a delivery’s outcome cannot be written back to the sender’s copy (its deliveryStatus), the journal says so: a queue row whose account id does not parse is an error, an Email submitted from a mailbox then destroyed before the attempt a warning. Both used to be dropped without a line. The delivery itself goes on in every case. A queue that grows without draining almost always means a DNS, reverse-DNS, or reputation problem on your side — start with oximail check-dns and the remote server’s reply recorded on the queue entry. Note that an account erasure deliberately refuses to run while that account still has undelivered queue entries; cancelling them via the queue surface unblocks it.

Address family and delivery slots

[smtp] outbound_address_family decides which IP family the server sends from. "auto" (the default) takes IPv4 whenever the destination has an IPv4 address, IPv6 only for a destination that has none, and never as the fallback of a failed IPv4: a sending address earns its reputation per family, and an IPv6 address that has never sent is cold at the large providers however established the IPv4 is. "v4" or "v6" pins one family, and a destination without an address in that family fails the attempt, which the journal records, rather than quietly using the other one. Any other value refuses to boot. [smtp.dispatcher] tunes the outbound workers. Every key is optional, the defaults favour reputation, and a box only sets the table to raise throughput on a well-warmed address or to tighten it.
An unknown key stops the boot.

Rate limits

[rate_limit] smtp_outbound_rcpt_per_hour caps outbound volume per account in recipients per hour (150 by default, 0 unlimited; a message toward N addresses costs N whether it is one envelope or N), the instance value every account inherits, overridable per account (oximail account quota --outbound-rcpt-per-hour, or outboundRcptPerHour on Principal/setAdmin; 0 = no limit for that account); a submission over it is deferred 452 4.7.1, counted per account in oximail_smtp_outbound_budget_deferred_total. The former smtp_outbound_per_hour, which counted envelopes, is a tombstone: a configuration that still carries it refuses to boot and says so, since reinterpreting its value as recipients would tighten every box silently. The [rate_limit.destination_domains] table shapes per-destination behaviour to stay polite with large receivers. The recipient budget is runtime-overridable (configuration); a bulk-sending platform behind an ordinary account meets it, see bulk sending.

Backup MX

A second box with [mode] role = "backup" accepts mail for your domains when the primary is down, in one of two modes (queue or synchronous proxy — see the SMTP layer). The wizard has a dedicated Backup MX flow; publish it as MX priority 20. A backup MX must know the valid recipients or stay in proxy mode — a backup that accepts everything is a backscatter source. Mail held for the primary is third-party mail in transit: since ADR-149 its body is a file in the _backup segment of the relay spool, no longer a column of the database that every backup and snapshot carried for the whole retention, and a backup MX without a configured spool is refused by check-config. The backup role also boots through the same path as every other role, so the listener inventory and the start-up guards are the same. The backup writes its own Received: line before it relays, in both modes, naming the client’s EHLO, its address and whether TLS was used. RFC 5321 §4.4 requires it, and without it the primary had no way to learn who the backup had served. The primary judges relayed mail on the client the backup served (ADR-166). A connection from a recognized relay is judged on the client address and EHLO read from its topmost Received: line: SPF, DMARC, the DNSBL check of the spam pipeline, the source recorded for DMARC aggregate reports, the ARC seal and the spamtrap ban all use that address, and the message gets the score a direct client would get. A recognized relay is an address of a host published as MX of one of the box’s mail domains, other than this server. There is no setting: publishing the MX is the declaration, since whoever controls those records already controls where the domain’s mail goes, and the verdict stays the primary’s. The set is derived before port 25 opens (10 seconds at most), then every 10 minutes, so a backup added while the server runs is recognized within that time. A domain without MX has no backup (its implicit MX is the domain itself, RFC 5321 §5.1) and no longer stops the recognition of the others; an MX host without any address is no relay; only a resolver failure keeps the set derived last. A header never grants trust: a recognized relay is never a trusted peer, even when [security] trusted_ips lists it, and an unreadable trace line, or a loopback, private or link-local origin, has the relay itself judged. A recognized relay is not rate-limited at connect. Before, the only way to let relayed mail pass SPF was to list the backup in trusted_ips, which also skipped the DNSBL and the DMARC reject: nothing a backup relayed was ever checked against a list. Remove such an entry. oximail doctor reports backup_relays: the MX hosts of each mail domain, each one “this server” or “recognized relay” with its addresses, or without an address and so unable to reach the box; a warning when a recognized relay is also in trusted_ips, where it is superfluous for mail and bypasses the rate limiter and fail2ban on every other door. The check is skipped on a backup MX itself. The wizard’s Backup MX flow publishes the backup’s own records only: an MX beside the primary’s, its A and AAAA, each read before it is created. It generates and publishes no DKIM key: a backup signs nothing for the domain, and its configuration names no key. With a Cloudflare token the flow used to delete the TXT at default._domainkey.<domain>, the primary’s public key, and publish an unused key of its own, after which every message the primary signed failed DKIM. It no longer publishes the domain’s client SRV records either (_imaps, _submission): clients are pointed at the primary. Its manual instructions list the same records, and say why there are no others. A backup installed with an earlier version may still hold a DKIM key it generated; it signs nothing, and oximail check-dns run on the primary confirms that default._domainkey still carries the primary’s key. check-dns and doctor ask a backup MX for what it needs: an MX record of the domain that names it, its PTR, DMARC, TLS-RPT and MTA-STS (whose policy must cover every MX, the backup included). They no longer ask it for the domain’s SPF and DKIM, which belong to the primary, nor for client records.

A box without a public entry

A box can send without being reachable: the outbound relay carries its mail out by an authenticated route. Receiving is another matter: an MX, a federated peer and a user outside the office all have to reach the box. Three situations take that away: a carrier that shares one IPv4 address between many subscribers (the CGNAT of mobile and some consumer fibre lines), a hosting policy that refuses inbound connections, and a box closed on purpose. A fixed address is not required to receive: an MX names a host, and a dynamic DNS name follows a changing address. The case that costs nothing: the box has a public IPv6 address. A CGNAT line almost always does. Publish the box as MX priority 10 with its AAAA only, and a backup MX you run elsewhere, dual-stack, at priority 20: senders with IPv6 reach the box directly, senders with IPv4 only fall to the backup, which holds the mail and delivers it to the box over IPv6 ([mode] primary_host). The backup is a recognized relay: the box judges each message on the client the backup served, never on the backup. Federated peers and clients reach the box over IPv6 as well; users outside the office who only have IPv4 come in through a VPN the box’s operator runs, which needs no public door at all. The backup MX reads the mail it holds, as every MX does, hop by hop: when a third party runs it, that party is a processor under your data-protection law, with a contract and a declared location, exactly like a managed outbound relay. The case with no entry at all: a relay in front, and what it must do. When the box has no public address of any kind, a machine that has one can pass the connections through a tunnel the box opens toward it (WireGuard, for example): the world knocks at the relay, the box answers. Four rules, without which the relay is a hole rather than a door:
  1. The relay is blind. It routes a connection by the name the client asks for (SNI) and never terminates TLS: it forwards bytes it cannot read. A relay that decrypts reads your mail and your files; for a medical office, that is a disclosure to a third party.
  2. The relay tells the box who is calling. Through a bare TCP relay every client arrives from the relay’s address: the box can no longer tell a colleague from an attacker, a ban hits everyone, SPF and the DNSBL judge the relay, and an anonymous route refuses a source it cannot identify. The standard answer is the PROXY protocol (version 2), by which the relay prefixes each connection with the client’s real address. OxiMail does not read the PROXY protocol today: until it does, a relay that hides the client’s address is not a supported entry for the port 25, submission, IMAP or HTTPS listeners. When it is read, it will be accepted from the tunnel’s endpoint alone.
  3. The tunnel’s endpoint is never in [security] trusted_ips. Listing it there would hand every client on the Internet the exemption from the rate limiter and fail2ban.
  4. Port 25 never crosses a relay that hides the client. SPF, the DNSBL and greylisting read the peer: without the client’s address the box is an open relay in the senders’ eyes. Keep the MX elsewhere (the backup MX above), and remember that “no public door” is a true sentence only while no MX names the box.
Whoever runs the relay controls the public entry and could obtain a certificate for the box’s name: the relay is a processor, under contract, with a declared location (outside your country, a transfer abroad). It is a production service, with its own capacity, availability and on-call. OxiMail names no relay and installs none, and its wizard never proposes one as a default.

Where the rest lives