Skip to content

Architecture

Antiphon is a Rust workspace with two binaries and a ring of focused library crates. The design falls out of one rule: the client never touches the network.

antiphon (TUI)  ──reads/writes──▶  the store  ◀──syncs── antiphond
        │                                                    │
        └───────────── IPC (unix socket) ────────────────────┘

The store

$XDG_DATA_HOME/antiphon/store holds everything: one Maildir tree per account, the notmuch index, the operation log, the outbox and draft spools, sync state, OAuth tokens and the harvested contact ranking. The vault, when configured, mounts its decrypted view at exactly this path, which is why nothing else in the system needs to know whether one exists.

Local folder names are the server's, lowercased; a delivered file's name carries the server UID it came from, which is what lets operations replay precisely later.

The operation log

Every mutation, flag, move, delete, is appended to a durable log and applied to the local store immediately: the UI is never waiting, and a crash loses nothing. The daemon replays pending ops against the server on each pass, resolving conflicts server-wins (a message deleted elsewhere drops the op rather than erroring the batch) and advancing a cursor only over the resolved prefix. Moves record the folder the message sat in when the op was made, because by replay time the local file has already moved.

The daemon

antiphond runs one worker thread for mail work and keeps the IPC serve loop free, so a status request answers mid-sync. Jobs (sync passes, outbox drains, op drains) coalesce in a queue; IMAP IDLE watcher threads turn server pushes into passes; a periodic UID sweep reconciles server-side deletions; and the vault is unlocked on start, sealed on stop and optionally on idle.

The crates

Crate Owns
antiphon the TUI, commands, compose, setup wizard, sendmail
antiphond serve loop, worker, mailflow, notifications, vault control
antiphon-core actions, keymap, key sequences
antiphon-store store layout, search, oplog, outbox, spools, contacts
antiphon-sync IMAP engine, replay, IDLE, reconciliation, SMTP send, rules
antiphon-render message parsing, HTML-to-text, links, patches, iTIP
antiphon-config strict TOML loading, accounts, diagnostics
antiphon-ipc the socket protocol between client and daemon
antiphon-vault APFS, LUKS2 and gocryptfs backends
antiphon-pgp Sequoia verification against the curated keyring
antiphon-pgp-agent the gpg-agent Assuan bridge for sign/decrypt
antiphon-oauth device-code and PKCE flows, rotation-safe token store
antiphon-ui the theme registry and its TOML format
antiphon-buildinfo version stamping from git or the packager's environment

Conventions throughout: files stay under 500 lines, functions do one thing, tables over repeated branches, comments only for what the code cannot say, and every tunable is a named constant.

Trust boundaries

  • The client parses untrusted mail; it never fetches remote content, never runs anything from a message, and hands only http, https and mailto URLs to the system opener.
  • Secrets (account passwords, the vault passphrase) are fetched through user-supplied commands at the moment of use, never stored in configuration.
  • Private key operations live in gpg-agent; Antiphon speaks Assuan to it and never sees key material.