> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oximail.ch/llms.txt
> Use this file to discover all available pages before exploring further.

# oximail-smtp

> The SMTP layer: separate inbound (port 25) and authenticated submission (port 587) code paths, the inbound auth + spam pipeline, the outbound delivery queue, DSN generation, and the backup-MX path.

`oximail-smtp` is the SMTP layer. It owns the wire protocol on both sides of the mail boundary: receiving mail from the outside world on port 25, accepting authenticated mail from a user's client on port 587, delivering queued mail to remote servers, generating bounces when delivery fails, and running as a backup MX for a primary that is down. It is the only crate that speaks raw SMTP; everything it accepts converges on the single ingestion path in [oximail-mail](/architecture/oximail-mail), and everything it sends comes off the persistent delivery queue.

## Overview

A message reaches this crate one of two ways, and the two ways are **deliberately distinct code paths**. An external server connecting to port 25 to deliver mail to a local recipient runs `process_inbound_data`. An authenticated user's client connecting to port 587 to send mail runs `process_submission_data`. These are not two branches of one function with a flag — they are separate methods with separate rules, because port 25 (inbound) and port 587 (submission) are fundamentally different protocols. Inbound mail is untrusted and must be authenticated, scored, and sealed before delivery; submission mail is already authenticated and must instead be validated against the sending user's own identities before it is queued for the outside world.

The crate sits above [oximail-store](/architecture/oximail-store) and [oximail-mail](/architecture/oximail-mail) and depends on the in-house `oximail-auth` crate for every email-authentication primitive (DKIM, SPF, DMARC, ARC, MTA-STS, DANE, SRS). The content-scoring side of spam is **orchestrated** here but lives in the `oximail-spam` crate: this crate decides *where the verdict sends the message*, not *how the message scores*.

Every accepted message obeys ADR-017: an email that received a `250 OK` from SMTP is always delivered somewhere — the recipient's inbox, the Junk folder, or quarantine — and never silently dropped.

## Features

* **Two separate code paths** — `process_inbound_data` (port 25) and `process_submission_data` (port 587), routed by `process_data` on whether the session is an authenticated submission. Inbound verifies and scores; submission validates the sender's identity and queues.
* **Inbound auth + spam pipeline** — DKIM / SPF / DMARC / ARC verification (via `oximail-auth`), ARC sealing of the chain, an `Authentication-Results` header (RFC 8601), then the spam pipeline orchestrated from the `oximail-spam` scorer, then delivery through the ingestion path.
* **Submission validation** — the authenticated user's envelope `MAIL FROM` *and* the message body's `From:` header are both checked against the account's identities (own address plus JMAP aliases). A mismatch is refused; the server does not relay forged senders even for an authenticated user.
* **Rejection replies with a postmaster help URL** — every synchronous reject and asynchronous bounce carries a stable `https://oximail.ch/postmaster?p=<Reason>` URL so the *sending* postmaster can self-diagnose.
* **Persistent delivery queue** — a SQLite-backed outbound queue with exponential-backoff retry and an RFC 5321 give-up window, polled by a background worker that resolves MX, DKIM-signs at delivery time, and enforces transport policy (MTA-STS / DANE).
* **DSN bounce generation** (RFC 3464) — a structured bounce to the original sender on permanent failure, partial delivery failure, or queue expiry. Multiline remote diagnostics are flattened before they reach a DSN header field, so a verbose remote reply can never break bounce generation (the full text still reaches the human-readable body).
* **Backup-MX path** (ADR-052) — store-and-forward queue mode (default) or synchronous proxy mode.
* **SMTP AUTH** — SASL `PLAIN` and `LOGIN` on the submission port, with per-session brute-force tracking.
* **Transport extensions** — `BDAT` chunking (RFC 3030), `REQUIRETLS` (RFC 8689), and `SRS` envelope rewriting on forward.
* **Outbound extension mediation** — every outbound hop derives its ESMTP requirements from the message content (SMTPUTF8, 8BITMIME, SIZE) and checks them against the destination's advertised capabilities. An unbridgeable gap bounces permanently instead of delivering a mis-encoded message.
* **Compliance journaling hooks** — inbound and outbound mail can be silently copied to a journal mailbox or an external archiver per tenant rules; see [compliance](/operator/compliance). Off by default.

## How it works

### Two paths, one routing point

Each TCP connection gets an `SmtpSession` that tracks the protocol state machine (`Connected` → `Greeted` → `MailFrom` → `RcptTo` → `Data` / `Bdat`) and whether the connection arrived on a submission port. When `DATA` completes, `process_data` makes the one decision that splits the two worlds:

```text theme={null}
if session.is_submission && session.authenticated.is_some()
    → process_submission_data   (port 587)
else
    → process_inbound_data      (port 25)
```

The two handlers share nothing but the raw bytes. This is a hard design rule, not an implementation detail: an authenticated submission must never accidentally fall into the inbound spam pipeline, and untrusted inbound mail must never reach the outbound queue.

### The inbound path

`process_inbound_data` runs, in order:

1. **Trust check** (ADR-031). Loopback is always trusted; `[security] trusted_ips` extends the set. A trusted peer bypasses SPF / DNSBL / DMARC-policy reject — but content analysis still runs, because a compromised local app can still spam.
2. **Authentication.** `oximail-auth` verifies SPF, DKIM, DMARC, and the ARC chain. The crate then ARC-seals the chain (RFC 8617) and prepends an `Authentication-Results` header (RFC 8601) so downstream consumers see the verdict without re-running verification.
3. **Spam pipeline.** The scored pipeline (orchestrated here, scored in `oximail-spam`) returns a decision. `Reject` → a `550` reject reply; `Defer` → a `451`; the delivered verdicts (`Junk` / suspected / bulk / clean) route to folder placement and an `X-Spam-Status` tag through a single disposition function (ADR-080) — only `Junk` files to the Junk folder, suspected and bulk deliver to the Inbox *with a tag* and are never junked.
4. **Delivery.** Each recipient is delivered through the ingestion path. If ingestion fails after the message is accepted, ADR-017 takes over: the message is still delivered (with the `$junk` keyword) rather than dropped. If *every* recipient fails, the reply is a `451` so the sender retries; a *partial* failure generates a DSN bounce for the failed recipients.

<Note>
  The spam **content scoring** is not implemented in this crate. `oximail-smtp` builds the `FilterContext`, calls the scorer, and acts on the returned `SpamDecision` — folder, tag, reject, or defer. The scoring model itself (heuristics, reputation, Bayesian, content analysis) lives in `oximail-spam`. This page documents the orchestration and the delivery consequences, not the scoring.
</Note>

### The submission path

`process_submission_data` requires an authenticated session and does **no** auth verification, **no** spam scoring, **no** ARC, and **no** `Authentication-Results` — that work belongs to inbound mail, not to mail the user is sending. Instead it enforces sender legitimacy twice:

* The envelope `MAIL FROM` must match the authenticated account's email or one of its JMAP identities (aliases, plus-tags). On a lookup error the check fails closed (ADR-027): a transient database hiccup rejects rather than relays.
* The message body's `From:` header is checked against the *same* identity set. An authenticated user who passes the envelope check can still forge a body `From:`; this second check refuses that.

A submission that passes is stored as a tenant-encrypted blob (ADR-020 — if the tenant key is unavailable the submission is rejected with `451`, never stored in cleartext) and enqueued for outbound delivery. DKIM signing happens later, in the outbound worker, at delivery time.

When the recipient is **local** (a user sends to a colleague on the same server), the outbound worker delivers without a network hop — and since #693 that local delivery applies the recipient's filtering Rules and vacation auto-reply, exactly as an externally received message would. ADR-117 ratifies the longer-term shape: one shared per-recipient delivery pipeline used by every ingestion entry point, so a path that skips recipient policy becomes unrepresentable.

### Rejection replies and the postmaster convention

Every rejection and bounce carries a stable help URL — `https://oximail.ch/postmaster?p=<Reason>` — mirroring the de-facto industry standard (Google's `support.google.com/mail/?p=<Reason>`). The `?p=` value is a per-**reason** slug, not a per-message reference, so the URL is stable and cacheable and maps to a named anchor on the public `/postmaster` page. `postmaster.rs` is the single source of truth for the slugs; the SMTP layer and the help page share it.

The enhanced status code is chosen by *why* the message was refused, not by convenience:

| Reason                                          | Reply | Enhanced status | Standard      |
| ----------------------------------------------- | ----- | --------------- | ------------- |
| DMARC `p=reject` (failing SPF + DKIM alignment) | `550` | `5.7.26`        | RFC 7372 §3.3 |
| Refused by the spam content pipeline            | `550` | `5.7.1`         | RFC 5321      |
| Recipient mailbox does not exist                | `550` | `5.1.1`         | RFC 5321      |
| Backup MX does not relay for this domain        | `550` | `5.1.1`         | RFC 5321      |
| Permanent delivery failure (returned to sender) | DSN   | —               | RFC 3464      |

The DMARC reject is honest about what it is: `5.7.26` ("multiple authentication checks failed", RFC 7372) is an authentication decision, not a content score — it is the code Gmail and Microsoft return for a DMARC reject. The previous code cited an arbitrary content reason under a `5.7.1`, producing a self-contradictory bounce; the three DMARC-reject sites (scored pipeline, backup-MX, no-pipeline fallback) now route through one shared `smtp_dmarc_reject_reply` helper so they stay in lockstep.

### Outbound delivery

A submitted or relayed message lands in the SQLite delivery queue. A background worker polls for due entries and delivers each one: it resolves the destination MX, applies transport policy (MTA-STS, RFC 8461; DANE, RFC 7672), DKIM-signs the message at delivery time, and sends it over an opportunistic-or-enforced TLS connection.

A temporary failure schedules a retry on an exponential backoff (1 min → 5 min → 30 min → 2 h → 8 h → 24 h). A message that cannot be delivered before the give-up window expires — **5 days** by default, per the RFC 5321 §4.5.4.1 guidance (the same default as Postfix) — generates an RFC 3464 DSN bounce to the original sender and leaves the queue.

The delivery hop **mediates ESMTP extensions** instead of silently bridging a gap. The requirements are derived deterministically from the message itself, never from a parameter the submitter declared: a non-ASCII octet in an envelope address or the header section requires SMTPUTF8 (RFC 6531), a high-bit body octet requires 8BITMIME (RFC 6152), and the transmitted length is declared as `SIZE=` when the peer advertises it (RFC 1870). Supported requirements are emitted onward; a destination that cannot satisfy one bounces the message permanently (`550 5.6.7` / `554 5.6.3` / `552 5.3.4`) rather than delivering it mis-encoded. This runs at the single outbound chokepoint, so direct-MX delivery and the [relay path](/operator/outbound-relay) behave identically. For the same honesty reason, BINARYMIME is not advertised on the inbound EHLO — the outbound client is line-oriented and could not carry binary content onward without corruption — and a `BODY=BINARYMIME` parameter is rejected `555 5.5.4`.

DKIM signing at delivery time **oversigns** the display-critical headers: the signature's `h=` list names `From`, `To`, and `Subject` one extra time, so a downstream actor cannot inject a second `From:`/`To:`/`Subject:` without breaking the signature (RFC 6376 §8.15 header-injection replay).

### The backup-MX path

When the server runs with `[mode] role = "backup"`, the inbound handler branches at RCPT TO and DATA into one of two modes (ADR-052):

* **Queue mode** (default) — store-and-forward. The backup accepts the mail with `250 OK`, then a retry worker relays it to the primary in the background. If the primary refuses with a `5xx`, a DSN bounce goes to the original sender. RFC-conformant, but it opens a window between sender acceptance and the bounce in which a forged sender can consider its spam delivered.
* **Proxy mode** (opt-in, ADR-052) — the backup proxies the SMTP session to the primary in real time: MAIL FROM, RCPT TO, DATA, body, and replies all flow through synchronously, so a primary `5xx` is propagated to the sender as a `5xx` with no backscatter and no delayed DSN. If the primary is unreachable at session start, the backup transparently falls back to queue acceptance for that mail.

### Invariants

* **Never fail silently.** An accepted message (`250 OK`) is always delivered somewhere (ADR-017); a permanent delivery failure always produces a DSN to the sender; an identity-lookup error on submission fails closed, not open.
* **Inbound and submission are separate paths.** Submission validates `MAIL FROM` and body `From:` against the authenticated identities; inbound does not validate sender identity but does authenticate, score, and seal. Neither path can fall into the other.
* **No cleartext blobs.** A submission whose tenant encryption key is unavailable is rejected with `451`, never stored in cleartext (ADR-020).
* **One reply per reason.** Each reject reason maps to one enhanced status code and one postmaster help slug, defined once in `postmaster.rs`.

## Configuration

`oximail-smtp` does not read configuration files directly; the binary resolves the values and constructs the server. The operator-facing knobs that shape the SMTP surface — the inbound and submission bind addresses and ports, the hostname used in `Received:` and SRS, the rate limits, the `[security] trusted_ips` and `[server] trusted_proxies` sets, the backup-MX `[mode]` block, and the outbound max-lifetime override — are documented in the [configuration reference](/operator/configuration). The at-rest encryption model behind the queued blobs (the LUKS-style scheme, not end-to-end) is covered in [encryption at rest](/operator/encryption-at-rest).

## SMTP listeners

`oximail-smtp` is **not** a JMAP crate; it serves no JMAP methods and no HTTP routes. Its surface is the SMTP wire protocol on these listeners:

| Listener   | Port | Path                      | Purpose                                                                                                                                                                        |
| ---------- | ---- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Inbound MX | 25   | `process_inbound_data`    | Receive mail from external servers for local recipients. Verifies SPF / DKIM / DMARC / ARC, runs the spam pipeline, delivers through ingestion. No sender-identity validation. |
| Submission | 587  | `process_submission_data` | Accept authenticated mail from a user's client. Requires SMTP AUTH; validates `MAIL FROM` and body `From:` against the account's identities; queues for outbound delivery.     |
| Backup MX  | 25   | backup branch (ADR-052)   | When `role = "backup"`: store-and-forward (queue) or synchronous proxy to the primary.                                                                                         |

The submission port supports SASL `PLAIN` and `LOGIN`, `STARTTLS`, `BDAT` chunking (RFC 3030), and `REQUIRETLS` (RFC 8689). The JMAP-side send surface (`Email/send`, `EmailSubmission/set`) lives in [oximail-mail](/architecture/oximail-mail); this crate implements the `SubmissionSender` contract that the delivery queue fulfils for it.

## Design decisions

* **ADR-017 (accepted email must deliver)** — any message that got a `250 OK` ends up in the inbox, Junk, or quarantine; ingestion failure delivers with `$junk` rather than dropping. For inbound, ADR-017 wins over encryption (deliver with `$junk`); for outbound submission, encryption wins (reject with `451` before any `250 OK`).
* **ADR-052 (backup-MX proxy mode)** — the backup MX runs in queue mode (store-and-forward, background relay, delayed DSN on primary `5xx`) or proxy mode (synchronous session proxy that propagates the primary's reply with no backscatter).
* **ADR-049 (MTA-STS coverage extension)** — transport-security policy (MTA-STS, DANE) is honored on the backup-MX relay path, not only on direct outbound.
* **ADR-031 (trusted-network policy)** — loopback and `[security] trusted_ips` bypass SPF / DNSBL / DMARC reject, but never the content pipeline. Its greylist clause is superseded by **ADR-128**, which retired RCPT-time greylisting altogether ([anti-spam](../operator/anti-spam#no-greylisting)).
* **ADR-080 (spam disposition spine)** — one disposition function maps every delivered spam verdict to folder placement plus the `X-Spam-Status` tag, so suspected and bulk deliver to the Inbox with a tag and only `Junk` files to Junk.
* **ADR-077 (unified spam classification model)** — a message to a spamtrap recipient is captured as ground-truth spam corpus (forced `$junk` delivery) rather than rejected or deferred, so the trap collects exactly the bulk spam it exists to study.
* **ADR-027 (fail-silent audit)** — the submission identity checks fail closed on a lookup error: a transient database failure rejects the send rather than relaying it.
* **ADR-076 (parser and auth stay AGPL, no crates.io)** — the email-authentication primitives come from the in-house AGPL `oximail-auth` crate, a workspace path-dependency.
* **ADR-116 (outbound smarthost relay)** — the authenticated relay routes, the ephemeral relay retention, and the §11 extension-mediation amendment documented above; the operator view is on the [outbound relay page](/operator/outbound-relay).
* **ADR-117 (unified local delivery pipeline)** — locality is a transport detail, not a semantic one: a local recipient gets the same recipient policy (Rules, vacation, trace) as an external delivery. Recipient Rules already run at local delivery; the full shared-pipeline extraction is the engraved target shape.

ADRs live in the OxiMail server repository; see the [ADR index](/reference/adr-index).

## Standards

* [RFC 5321](https://www.rfc-editor.org/rfc/rfc5321) — Simple Mail Transfer Protocol. The wire protocol of both the inbound and submission paths, the `Received:` trace header (§4.4), and the give-up-window guidance (§4.5.4.1).
* [RFC 7372](https://www.rfc-editor.org/rfc/rfc7372) — Email Authentication Status Codes. The `5.7.26` enhanced status used for a DMARC `p=reject` rejection (§3.3).
* [RFC 3464](https://www.rfc-editor.org/rfc/rfc3464) — Delivery Status Notifications. The structured bounce generated on permanent or partial delivery failure and on queue expiry.
* [RFC 8601](https://www.rfc-editor.org/rfc/rfc8601) — Authentication-Results header, prepended to inbound mail with the verdict.
* [RFC 3030](https://www.rfc-editor.org/rfc/rfc3030) — `BDAT` / `CHUNKING` for the inbound data phase.
* [RFC 8689](https://www.rfc-editor.org/rfc/rfc8689) — `REQUIRETLS`, propagated onto bounce and Sieve-generated outbound envelopes, and re-emitted on the outbound wire.
* [RFC 6531](https://www.rfc-editor.org/rfc/rfc6531) (SMTPUTF8), [RFC 6152](https://www.rfc-editor.org/rfc/rfc6152) (8BITMIME), [RFC 1870](https://www.rfc-editor.org/rfc/rfc1870) (SIZE) — the extensions the outbound mediation derives from message content and negotiates per destination.

The email-authentication RFCs themselves — [RFC 6376](https://www.rfc-editor.org/rfc/rfc6376) (DKIM), [RFC 7208](https://www.rfc-editor.org/rfc/rfc7208) (SPF), [RFC 7489](https://www.rfc-editor.org/rfc/rfc7489) (DMARC), [RFC 8617](https://www.rfc-editor.org/rfc/rfc8617) (ARC), [RFC 8461](https://www.rfc-editor.org/rfc/rfc8461) (MTA-STS), [RFC 7672](https://www.rfc-editor.org/rfc/rfc7672) (DANE) — are implemented in the in-house `oximail-auth` crate (which will have its own reference page) and consumed by `oximail-smtp`.

## Source map & tests

| File (within the crate)                                                                              | What it holds                                                                                                                                                                                                                       |
| ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/inbound.rs`                                                                                     | The `SmtpServer`, the command loop, `process_data` (the path split), `process_inbound_data`, `process_submission_data`, `deliver_to_recipient`, and the reject-reply helpers (`smtp_dmarc_reject_reply`, `smtp_spam_reject_reply`). |
| `src/session.rs`                                                                                     | `SmtpSession` and the `SessionState` machine.                                                                                                                                                                                       |
| `src/auth.rs`                                                                                        | The `oximail-auth` verification wrapper (`AuthResolver`, `verify_message`, `should_reject`, `should_flag_junk`).                                                                                                                    |
| `src/outbound.rs`                                                                                    | The outbound worker: MX resolution, transport policy, DKIM signing at delivery, TLS.                                                                                                                                                |
| `src/queue.rs`                                                                                       | `DeliveryQueue` — the persistent outbound queue, retry backoff, and the give-up window.                                                                                                                                             |
| `src/dsn.rs`                                                                                         | RFC 3464 bounce generation.                                                                                                                                                                                                         |
| `src/postmaster.rs`                                                                                  | `PostmasterReason` — the single source of truth for the `?p=` reject slugs and help URLs.                                                                                                                                           |
| `src/backup.rs`, `src/backup_proxy.rs`                                                               | The backup-MX queue mode and synchronous proxy mode (ADR-052).                                                                                                                                                                      |
| `src/srs.rs`                                                                                         | SRS envelope rewriting on forward.                                                                                                                                                                                                  |
| `src/mta_sts.rs`, `src/dane.rs`                                                                      | Outbound transport-policy checks (consume `oximail-auth` primitives).                                                                                                                                                               |
| `src/bayes.rs`, `src/spam_store.rs`                                                                  | The local spam-corpus storage the scorer reads.                                                                                                                                                                                     |
| `src/header_mutate.rs`, `src/sanitize.rs`, `src/transport.rs`, `src/rate_limit.rs`, `src/trusted.rs` | Header mutation, content sanitization, transport helpers, rate limiting, and the trusted-peer set.                                                                                                                                  |

`postmaster.rs` carries a test asserting every reason's help URL is well-formed and stable; the reject-reply helpers carry tests pinning the DMARC reject to `5.7.26` and the content reject to `5.7.1`, both with the postmaster URL. The session state machine has its own transition test.

## Status

Implemented and in production at **v0.30.0**: the two separate inbound and submission code paths, the full inbound auth + ARC-seal + spam-orchestration + delivery pipeline, the submission envelope-and-header identity validation, the persistent delivery queue with backoff and the RFC 5321 give-up window, RFC 3464 DSN generation, the postmaster help-URL reject convention, both backup-MX modes (queue and proxy, ADR-052), the outbound ESMTP extension mediation, DKIM oversigning of From/To/Subject, recipient Rules at local delivery, and the per-tenant compliance journaling hooks.

The spam **content scoring** that the inbound path orchestrates is implemented in the `oximail-spam` crate; on the community (AGPL) build the scorer runs the heuristic and authentication signals, and the spamtrap-corpus capture (ADR-077) is live in production. The email-authentication primitives are provided by the in-house `oximail-auth` crate, which will have its own reference page.
