Files
2026-11-05 12:52:32 +01:00

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'