3.8 KiB
Development
Architecture
src/
main.rs CLI (serve, admin, config, device, probe)
lib.rs public router (ActiveSync, Autodiscover, /, /healthz)
app.rs application state: settings (ArcSwap), IMAP pool, metrics, lockouts
config.rs settings model (serde, validation, merging, secrets)
db.rs SQLite (sqlx, WAL), migrations in migrations/, housekeeping
server.rs HTTP/1.1 + HTTP/2 listener, optional TLS, graceful shutdown
wbxml/ WBXML encoder/decoder + MS-ASWBXML code pages
eas/ ActiveSync protocol
mod.rs request parsing (plain/base64 query), auth, provisioning, dispatcher
state.rs SyncKey {uuid}N and states (item fingerprints)
folders.rs FolderSync / Create / Update / Delete
sync.rs Sync, GetItemEstimate, MoveItems
ping.rs Ping and the shared wait-for-changes loop (hanging Sync)
provision.rs policies, remote wipe
mailcmd.rs SendMail / SmartReply / SmartForward / MeetingResponse
itemops.rs ItemOperations, GetAttachment
misc.rs Settings, Search, ResolveRecipients
convert/ RFC 5322 / iCalendar / vCard <-> ApplicationData
backend/
imap.rs own async IMAP client (literals, UIDPLUS, MOVE, SPECIAL-USE)
dav.rs CalDAV/CardDAV client
smtp.rs SMTP (lettre) / sendmail
ical.rs iCalendar/vCard parser and serializer
tz.rs EAS <-> IANA time zones, dates
mime.rs MIME building (SmartReply/Forward)
mod.rs session: folder hierarchy, fingerprint listings, operations
autodiscover.rs
admin.rs panel API, sessions, CSRF, SSE, Prometheus
web/ panel (dependency-free HTML/CSS/JS, embedded in the binary)
Synchronisation
The device state of a folder is a map item id -> fingerprint (IMAP: flags; DAV: ETag) stored under the key
{uuid}N. Sync compares the backend listing with the state and sends the differences (deletes -> changes ->
adds, newest mail first) within WindowSize. The new state is stored as N+1; the previous one is kept to
handle lost responses (the device replays the old key). Client adds are idempotent thanks to the ClientId map.
Ping keeps a folder signature (IMAP STATUS UIDNEXT/MESSAGES/UNSEEN/HIGHESTMODSEQ, DAV CTag); a full listing
comparison only happens when the signature changes.
Collection ids that a device uses but that do not exist (Z-Push leftovers) are remembered per device and sent as FolderHierarchy Delete on the next FolderSync.
Development environment
sudo apt install dovecot-imapd # any dovecot >= 2.3
pip install radicale aiosmtpd
./dev.sh backends # Dovecot :11143, Radicale :15232, SMTP sink :12525
./dev.sh seed && ./dev.sh # ActivePush :8081 / panel :9091
./dev.sh e2e # test of every command against live backends
Test accounts: jan, anna, biuro, kasia @test.local, password Test-Password-123.
./dev.sh lint - rustfmt, clippy -D warnings, JS syntax check and WBXML tag name check
(scripts/check-wbxml-names.py). ./dev.sh test - unit tests (WBXML, IMAP tokenizer, UTF-7, calendar/contact/
task conversions, time zones, query parser, CIDR, MIME).
Protocol debugging: add the login to ActiveSync -> Logging -> Debug users; decoded WBXML of requests and responses goes to the log.
Simulating devices: active-push probe --device-id F2LX8KQ1N72J --device-type iPhone --model "iPhone 15 Pro" ....
Documentation screenshots
tools/dev/screenshots.js (Playwright) regenerates docs/images/ from a running dev instance:
node tools/dev/screenshots.js http://127.0.0.1:9091 admin 'password'