Single-binary chatmail server for Delta Chat, written in
Rust and driven by one flat TOML config file (dc_oxide.toml) — no sections,
no optional daemons. One process = MX + SMTP submission + IMAP + outbound relay.
What it does
MX inbound (port 25): server-to-server SMTP from other MTAs — plaintext with opportunistic STARTTLS (RFC 3207), no AUTH, no JIT: only existing local accounts are accepted (uniform 550 for unknown/foreign, so no address enumeration), never an open relay. RFC 1870 SIZE pre-check at MAIL time.
SMTP submission + AUTH with JIT accounts: clients authenticate over SMTP (implicit TLS) with a 9-digit chatmail address and password; accounts are created on first use (just-in-time), password reset is rejected, unauthenticated inbound mail is refused (chatmail policy), and the envelope MAIL FROM is pinned to the authenticated account (no sender spoofing).
IMAP subset for Delta Chat: LOGIN/AUTHENTICATE PLAIN, SELECT INBOX, UID FETCH (incl. RFC822.SIZE, BODY.PEEK[HEADER.FIELDS]), STATUS, UID STORE Seen (persisted as the maildir
:2,Ssuffix), APPEND, NOOP, IDLE, EXPUNGE/CLOSE, GETMETADATA (Delta Chat vendor keys, e.g.maxsmtprecipients), SETMETADATA answered NO (keeps Delta Chat push off), LOGOUT.Outbound relay: non-local recipients are enqueued to a disk queue and drained every 60 s — via
relay_hostsmart-host, or recipient-domain direct with DNS MX lookup (RFC 5321 preference order, A-record fallback) and opportunistic STARTTLS verified against Mozilla roots — gated by an OpenPGP-content policy check that refuses plaintext-only relaying. The envelope MAIL FROM is carried through the queue so bounces reach the original sender.ACRA onboarding (optional,
acra_port): HTTPS endpoint for the app's "scan QR, account appears" flow.dc_oxide invite --uses Nmints a token (a flat file understorage_dir/invites/) and prints aDCACCOUNT:https://...URL; a scanned app empty-POSTs it and gets back{"email":"...","password":"..."}for a freshly created account. Distribute only this HTTPS form: the scheme-lessDCACCOUNT:<host>QR makes the app generate an alphanumeric local part that the 9-digit chatmail policy rejects.TLS: rustls implicit-TLS on every listener; cert/key from
tls_cert_path/tls_key_path, or a self-signed cert generated into the storage dir on first start and reused thereafter.
Config layout (dc_oxide.toml)
Flat top-level keys mirroring src/config.rs (unknown keys are rejected at
startup):
mail_domain = "chat.example.org" # required
storage_dir = "/var/lib/dc_oxide" # required — maildirs + certs + queue live here
bind_addr = "127.0.0.1" # one string or a list; '["0.0.0.0", "::"]' for
# dual-stack DNS-fronted deployments (TLS-only)
smtp_port = 25 # MX inbound (plaintext + STARTTLS, local accounts only)
submission_port = 465 # SMTP+AUTH client submission (implicit TLS)
imap_port = 993 # IMAP (implicit TLS)
acra_port = 0 # onboarding HTTPS endpoint (0 = disabled)
max_message_size = 31457280 # 30 MiB, chatmail reference default
max_recipients = 50
# Optional PEM cert/key; omit both for auto-generated self-signed (dev only).
# tls_cert_path = "/etc/dc_oxide/fullchain.pem"
# tls_key_path = "/etc/dc_oxide/privkey.pem"
# Outbound relay.
relay_host = ""
# relay_port = 587 # defaults to 25 when relay_host is set
relay_user = ""
relay_pass = ""
See dc_oxide.toml.example for a fully commented copy.
Running
cargo build --release
cp dc_oxide.toml.example dc_oxide.toml # edit mail_domain + storage_dir
RUST_LOG=info ./target/release/dc_oxide --config dc_oxide.toml
Flags: --config/-c (default dc_oxide.toml), --log/-l (default info,
overridable via RUST_LOG). The storage dir is created if missing; TLS is
initialized before any listener binds.
Subcommands: dc_oxide invite --uses N mints an ACRA onboarding token and
prints its DCACCOUNT:https://... URL (the server keeps running separately;
the URL is only served while acra_port is nonzero).
Deploying
Per-OS deployment kits (config template, rc/service scripts, README) live in
install/ — OpenBSD, Linux, FreeBSD, and NetBSD. See
install/README.md for the portability gate.
Version control
dc_oxide uses Fossil (not git). The repository file is dc_oxide.fossil;
open it with fossil open dc_oxide.fossil or browse with fossil ui. The
Fossil wiki holds the architecture docs (Architecture, ConfigReference,
MaildirLayout, RoadmapAndTickets); every feature/bug gets a Fossil ticket and
commits reference the ticket id. Conventions for working in the repo are in
AGENTS.md.
Milestones
Done:
- SMTP + AUTH with JIT account creation (loopback first, now TLS)
- IMAP subset: SELECT/UID FETCH/STATUS/STORE/APPEND/NOOP/IDLE/EXPUNGE/CLOSE, plus GETMETADATA for Delta Chat vendor keys
- MX inbound port: STARTTLS-offering, local-accounts-only, relay-free
- Implicit TLS on submission/IMAP listeners; PEM config or self-signed auto-generated
- Outbound relay: disk queue, 60 s drain, OpenPGP-content policy gate, DNS MX lookup + opportunistic STARTTLS for direct delivery
- Delta Chat Desktop 2.62 core flows pass (2026-10-04)
- Live acceptance harness (
src/accept_dc.rs): full server + deltachat-rpc-server, secure-join both directions, encrypted 1:1 round-trip, and a three-member verified group joined via invitation QR, encrypted in every direction, in about 25 s. SetDC_OXIDE_LIVE=<host>to replay the same scenario against a deployed server, with fresh JIT-created accounts and strict certificate validation (ports/domain overridable viaDC_OXIDE_LIVE_*env) - Install kits for OpenBSD/Linux/FreeBSD/NetBSD under
install/ - ACRA onboarding endpoint +
dc_oxide invitetoken minting - 63 unit tests (the live-DNS probe is gated on
DC_OXIDE_LIVE_DNS); live TLS end-to-end loop verified (SMTP AUTH → maildir → IMAP FETCH)
Pending (Fossil tickets):
- Chatmail end-to-end encryption parity beyond the harness (real clients on public networks, wider client matrix)
- Relay AUTH (smart-host credentials) for locked-down outbound relays
- DKIM/SPF on outbound, MTA-STS, ACME automatic certificates