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,httpsandmailtoURLs 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.