A gateway bridging a smolmail account and an SMTP mailbox: smolmail messages carrying an `Email-To` frontmatter field are relayed to that address over SMTP, and mail sent to `<username>@<email domain>` is relayed as a smolmail message to `<username>` if that user is registered on the configured smolmail server. See `src/main.rs` and `src/config.rs` for the full environment variable list, `docker-compose.yml`/`docker/postfix/` for a Postfix-fronted container deployment, and `flake.nix` for a NixOS deployment with no containers.
The implementation is Rust on [fumi-core](../fumi) — the smolmail client library that also powers the `fumi` CLI — reached as a Cargo path dependency on a sibling checkout (`../fumi/core`, the same convention the nix flake and container build spell out). The email side is self-contained: a hand-rolled LMTP listener, a minimal outbound SMTP client (STARTTLS and AUTH LOGIN included, rustls underneath), and a `mail-parser` based body extractor. The gateway's own smolmail state (`gateway.db`) is a fumi store, so every fumi CLI command works against it for bootstrap and manual review.
This assumes a smolmail account already exists, and that the gateway's smolmail address (e.g. `gateway@smol.example.org`) and the email domain it answers to (e.g. `example.com`) are known — the operator can provide both. Everything below uses `fumi`, the same way it's used for anything else.
A few things that aren't obvious from the outside:
- **No `Email-To`, no relay.** A message without it is never forwarded — it just sits there for the operator to notice, never bounced back to the sender.
- **The recipient won't see the sender's smolmail identity.** The email arrives `From:` the gateway's own fixed address, the same for every user of that gateway, not something tied to whoever sent it. A sender who wants to be identified needs to say so in the body.
- **By default, a sender has to be accepted first.** Most gateways only relay mail from senders they've explicitly accepted (SPEC.md §5.8) — the sender needs to ask the operator to accept their address. One wrinkle worth knowing about: being accepted isn't quite enough on its own. The accept token that makes the sender's *next* message count only arrives once the gateway has sent something back, so if a message still doesn't go through right after being accepted, the operator needs to send a quick reply first. (Some gateways are run deliberately open with this requirement turned off — in that case none of this applies and anyone can use it.)
### Receiving email through the gateway
Mail sent to `<username>@example.com` reaches that registered user as a smolmail message, but by default only once the user has accepted the gateway:
Both steps matter, not just the first. Accepting the gateway only changes the user's own server's tiering for mail from it; the gateway has no way to know that happened until it sees it reflected in a message from the user (same §5.8 mechanism as above), which means sending it at least one message. Until both are done, inbound email addressed to that user is rejected outright — not queued, not quietly demoted to some low-priority folder, just refused before it's even accepted.
Once that's done, the smolmail messages received this way won't say who originally sent the email — that's deliberate, since an email's claimed sender is never something the gateway can verify, so it doesn't forward a claim it can't back up. If the original sender identified themselves, that'll only show up in whatever text they wrote, not in a field that can be trusted on its own.
(Again: some gateways run with this requirement turned off, relaying to anyone registered regardless of acceptance — it depends on how that gateway is configured.)
`cargo test` (or `devbox run test`) — 32 passing, offline (stubs plus fumi's in-memory store). Covers address validation including the CR/LF/NUL/unicode-whitespace injection battery and the CR/LF header-value backstop (`smtp_client`), frontmatter sanitization, MIME body extraction including the malformed-MIME battery (empty, binary, 50-deep nesting, declared-huge-missing-body) (`email_to_smol`), the accepted-recipient token gate including default-port normalization against a real fumi store (`email_to_smol`), per-recipient LMTP reply assembly, the accepted-recipient RCPT reject, command parsing and dot-unstuffing (`smtp_server`), the Postfix `tcp_table(5)` lookup protocol including the same gate and `%XX` quoting round-trips (`lookup_server`), and failure isolation plus the accepted-sender tier selection in the smolmail → email poll loop (`smol_to_email`).
Concurrent load against pneumatube directly, and through the real Postfix container rig (`devbox run stress-postfix`). The throwaway server is `bunshin`; the load-driving sender and recipient accounts use the reference `smolmail.py` client in-process, which doubles as an interop check of the Python reference client against the Rust gateway and Rust server. Needs `bunshin` and `fumi` binaries on PATH and a `smolmail` checkout next to this repo.
- **Direct target, default concurrency 20 (200 messages per phase)**: both phases correct (133 ok / 67 rejected exactly as constructed; 200/200 relayed to the outbound sink, no loss or duplication). Email → smolmail sustains ~88 msg/s — each accepted recipient paying a live connect + Noise handshake + RESOLVE at RCPT time — and smolmail → email SENDs land at ~335 msg/s.
- **No serialization point left**: the Python implementation serialized every smolmail operation onto one worker thread (sqlite thread-affinity of the vendored client). fumi's store is `Send + Sync` (mutex + WAL) and every dial is its own Noise session, so the Rust port's concurrency is capped only by the smolmail server on the other end. The old concurrency ≈30 collapse against the reference `smolmaild.py` (ThreadingTCPServer backlog of 5) does not reproduce against `bunshin`: concurrency 30 completes both phases with zero errors and full drain.
- The poll loop relays one message per SMTP conversation, sequentially by design; the harness's drain deadline is sized for that (~150 ms per relay against a local sink).
Live checks against a throwaway rig (same fixtures as the stress test): 3 PASS (defense verified), 3 RISK (confirmed, accepted design tradeoffs — not bugs, but worth knowing they're real), 0 BUG. The unit-style checks the Python version ran in this script live in `cargo test` now (see Unit tests above).
- The smolmail → email direction relays no first-contact sender by default: 0/15 never-before-seen senders with `Email-To` fields reached the outbound sink with `PNEUMATUBE_REQUIRE_ACCEPTED_SENDER` at its default.
- The email → smolmail direction rejects a registered-but-never-accepted recipient live at RCPT TO (outcome=rejected, not queued-then-bounced) with `PNEUMATUBE_REQUIRE_ACCEPTED_RECIPIENT` at its default.
- A forged `MAIL FROM` (`security-team@trusted-bank.example`) leaves no trace in the relayed message's frontmatter — the email → smolmail direction carries no `Email-From` field at all, since `MAIL FROM` is never authenticated.
- The Postfix lookup service unambiguously reveals which usernames exist, with no auth or rate limiting of its own — correct and necessary for `check_recipient_access` to work, but only safe because it's loopback-only. This requirement is load-bearing, not cosmetic: never expose `PNEUMATUBE_LOOKUP_HOST`/`PNEUMATUBE_LOOKUP_PORT` beyond Postfix.
- **Open relay, smolmail → email, opt-in**: setting `PNEUMATUBE_REQUIRE_ACCEPTED_SENDER=0` deliberately restores the open-bridge behavior (confirmed live: 15/15 first-contact sends relayed) — an accepted tradeoff for an operator who wants a public bridge, not a default.
- **Open relay, email → smolmail, opt-in**: setting `PNEUMATUBE_REQUIRE_ACCEPTED_RECIPIENT=0` deliberately restores the "relay to anyone" behavior (confirmed live: 15/15 distinct-sender messages relayed to an unaccepted recipient) — same tradeoff, operator's explicit choice. Note there is still no *per-sender* throttling in either mode: the gate is per-recipient (have they accepted the gateway at all), not per-sender, so an accepted recipient remains reachable by any external sender once opted in.