Guidance for AI coding agents (and humans) working on this repo. Project: dc_oxide — a single-binary, one-TOML-file chatmail server for Delta Chat, written in Rust. No Python, no Dovecot, no Postfix. The binary is the MX, submission server, and IMAP server.
Version control: Fossil, not Git
This repo is a Fossil repository. All work happens in Fossil:
fossilis the only VCS used here; do NOT usegitin this repo (no migration mirrors, no tooling that writes.git).- Every feature, bug, and task gets a Fossil ticket first — commits
reference the ticket by its short id in the commit message
(
... fixes <id>/resolves <id>). - Checkin flow: edit →
fossil add/fossil rm→fossil commit --nosync, with the ticket id in the check-in comment. (--nosyncis always passed: commits do not auto-pull/push; syncing is a separate deliberate step.) - The web UI (
fossil ui, orfossil serverheadless) hosts the wiki (architecture docs live there) and the ticket tracker. - Commits stay small and one-topic; never bundle ticket work into an unrelated check-in.
Repository checkout
This is a public repo: docs, comments, and scripts must not assume
machine-specific paths (home-dir layouts, private hostnames, internal
IPs). Canonical public clone:
fossil clone https://code.lewman.com/dc_oxide dc_oxide.fossil;
fossil open dc_oxide.fossil creates the working tree wherever you
choose. fossil info from inside the tree identifies both.
Ground rules
- Rust edition 2021, toolchain 1.92+ (
cargo checkmust be clean: zero errors, zero warnings). - Single binary, single TOML. Any new setting goes in
src/config.rsas a flat top-level key (no[sections]) AND indc_oxide.toml.examplewith a comment.deny_unknown_fieldsis on — typos fail at startup on purpose. - No databases. Accounts are maildir trees on disk
(
<storage_dir>/users/<localpart>/), passwords are argon2id PHC strings inpassword.argon2. Anything that "needs state" should end up as a file understorage_dir(certs, outbound queue), never SQLite. - No new dependencies without stating why in the ticket comment.
The crate set is deliberately small; every entry in
Cargo.tomlis referenced fromsrc/: tokio, tokio-rustls/rustls, rcgen, serde/toml, mail-parser, argon2, base64, clap, tracing + tracing-subscriber, anyhow, rand, webpki-roots (STARTTLS peer verification), hickory-resolver (DNS MX lookups), socket2 (IPV6_V6ONLY for dual-stack binds); dev-only: tempfile, serde_json (JSON-RPC framing for the acceptance harness). If a crate is no longer referenced fromsrc/, remove it fromCargo.tomlin the same check-in rather than parking it. - Chatmail policy, not generic email. The server exists to serve
Delta Chat: 9-digit numeric local parts, TLS mandatory on
non-loopback binds, plaintext tolerated only on
127.0.0.1. One documented exception:smtp_port(MX, port 25) is server-to-server — foreign MTAs deliver unauthenticated, so the MX listener starts plaintext and offers opportunistic STARTTLS (RFC 3207), accepting mail only for existing local accounts, never relaying. But the submission listener still rejects unauthenticated inbound mail atMAIL(530). Full RFC-width generality is a bug, not a feature. - Test naming. Every nontrivial behavior gets a unit test in the same
file (see existing
mod testsconventions). Integration against a real listener goes inmail_in.rs/mail_out.rstests (self-signed TLS via tls::init + a no-verify client connector is the established pattern). - Verify before claiming done.
cargo check --all-targets,cargo test, andcargo clippy --all-targets --all-features -- -D warningsall clean before any "ready" answer. The live end-to-end check that has caught every real bug so far: build, start the server on a temp config (mail_domain,storage_dirin /tmp, high ports), drive SMTP AUTH PLAIN over TLS (openssl/python ssl), check the message appears in the maildir on disk, then IMAP FETCH over TLS returns it. The same harness replays against a deployed server with strict certs viaDC_OXIDE_LIVE=<host> cargo test accept_dc -- --nocapture. For client-level changes also run the Delta Chat acceptance test:DC_OXIDE_ACCEPTANCE=1 cargo test accept_dc -- --nocapture(boots the real server and drives secure-join + encrypted delivery through deltachat-rpc-server; about twenty seconds).
Layout
src/main.rs — CLI parsing, tracing init, TLS init, tokio::select over servers
src/common.rs — Conn (plaintext-or-TLS), is_loopback_bind, and the
dual-stack (v6only) bind_listener shared by listeners
src/config.rs — serde of dc_oxide.toml (flat keys, deny_unknown_fields)
src/store.rs — maildir store + argon2 password hashing + address validation
src/mail_in.rs — SMTP submission listener (TLS, AUTH PLAIN/LOGIN, JIT creation)
src/mx_in.rs — MX inbound listener (port 25): plaintext + opportunistic
STARTTLS, no AUTH/no JIT, local existing accounts only
src/mail_out.rs — IMAP listener (TLS, DC subset: SELECT/UID FETCH/APPEND/IDLE/
GETMETADATA/SETMETADATA-NO/...)
src/relay.rs — outbound: enqueue non-local rcpts with the envelope sender,
60s queue drain, MX resolution, STARTTLS-capable smtp_send,
encrypted-only gate
src/acra.rs — onboarding endpoint (optional acra_port): invite-token HTTPS
POST → fresh account credentials as JSON; `dc_oxide invite`
mints token files under storage_dir/invites/
src/tls.rs — rustls ServerConfig: load PEMs or self-generate + persist
src/accept_dc.rs — test-only Delta Chat acceptance harness (JSON-RPC over
deltachat-rpc-server; gated on DC_OXIDE_ACCEPTANCE)
dc_oxide.toml.example — fully commented example config, mirrors config.rs
install/ — per-OS deployment kits (openbsd/linux/freebsd/netbsd:
copy-paste READMEs, rc.d/systemd units, pf + acme recipes)
Style notes
- Comments explain why and cite RFC/section where relevant (RFC 4616 SASL PLAIN, RFC 4954 AUTH LOGIN, RFC 5321 §4.5.2 dot-stuffing).
- Error handling is
anyhowwith context everywhere a file/port/parse could be wrong. - Maildir filenames follow the standard
<secs>.M<usec>R<pid>.hostshape withcreate_newcollision retries. IMAP\Seenis persisted by moving the file fromnew/tocur/with the standard:2,Sinfo suffix; the IMAP uidlist keys on the flag-less unique name so flag renames never renumber a message. - The IMAP implementation is deliberately narrow — a feature is added only when a Delta Chat client is observed to need it. Do not "finish" IMAP.
Keeping this doc true
This doc is maintained against the real code; where it and the code
disagree, fix the code or this doc in the same check-in. When you add a
setting, command, capability, dependency, or port, update the matching
line here and in README.md / dc_oxide.toml.example in the same
check-in.