AGENTS.md — dc_oxide
Not logged in

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:

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

  1. Rust edition 2021, toolchain 1.92+ (cargo check must be clean: zero errors, zero warnings).
  2. Single binary, single TOML. Any new setting goes in src/config.rs as a flat top-level key (no [sections]) AND in dc_oxide.toml.example with a comment. deny_unknown_fields is on — typos fail at startup on purpose.
  3. No databases. Accounts are maildir trees on disk (<storage_dir>/users/<localpart>/), passwords are argon2id PHC strings in password.argon2. Anything that "needs state" should end up as a file under storage_dir (certs, outbound queue), never SQLite.
  4. No new dependencies without stating why in the ticket comment. The crate set is deliberately small; every entry in Cargo.toml is referenced from src/: 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 from src/, remove it from Cargo.toml in the same check-in rather than parking it.
  5. 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 at MAIL (530). Full RFC-width generality is a bug, not a feature.
  6. Test naming. Every nontrivial behavior gets a unit test in the same file (see existing mod tests conventions). Integration against a real listener goes in mail_in.rs / mail_out.rs tests (self-signed TLS via tls::init + a no-verify client connector is the established pattern).
  7. Verify before claiming done. cargo check --all-targets, cargo test, and cargo clippy --all-targets --all-features -- -D warnings all 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_dir in /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 via DC_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

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.