From the app on the phone
to the object on the disk.
A ShardPost mail system has four layers: clients, the authentication and routing in front, the ShardPost replicas, and Ceph underneath. Select any part of the diagram to see what it does.
Select a part of the diagram to read what it does. The layout follows the reference deployment; Nauthilus is the authentication backend, nauthilus-director the optional front door.
ShardPost
Select a part of the diagram to read what it does.
ShardPost
The mailbox and processing layer: IMAP, POP3, JMAP, LMTP and ManageSieve on one store, with Sieve during delivery and IMAPSieve on mailbox events. Several identical replicas share the same pools. Connections, parsers and caches live in the process; the durable mail state lives in RADOS, so a client that lands on another replica needs nothing but a new connection.
Background work (maintenance jobs, learning, delayed sending, expiries) runs in the servers or in job runner pods of their own. JMAP push workers share 64 partitions through RADOS leases.
Ceph RADOS
ShardPost writes through librados straight into RADOS objects, with no file system and no database in between. The index pool holds each account's mailboxes, message entries, quota and full-text index as omap and must be replicated. The data pool holds the message blobs, which are written once and never changed, so it may be erasure coded. An optional system pool keeps the small, hot store-wide state (admin store, account directory, queues and locks) on fast OSDs.
Every account is one RADOS namespace in the shared pools; there is no pool per user. Read the storage guide →
Nauthilus
Nauthilus decides every login and resolves an address to the canonical account whose namespace holds the mail. ShardPost reaches it over gRPC AuthService (or its HTTP JSON API) and passes the client address, protocol and TLS state along for Nauthilus' brute-force and policy checks. Bearer tokens are checked by OAuth 2.0 token introspection (RFC 7662).
A short-lived auth cache in front of Nauthilus answers repeated logins, and shardpostctl auth flush empties it on every server at once. For JMapper, Nauthilus is also the OpenID Connect provider. nauthilus.org →
nauthilus-director
In the reference deployment the protocol traffic enters through nauthilus-director, a mail protocol director and transparent proxy. It authenticates each session through Nauthilus, picks a healthy backend by routing facts such as the mail shard, and forwards the connection to ShardPost with the PROXY protocol, so ShardPost sees the real client address. JMAP requests take the same path.
ShardPost accepts PROXY headers only from trusted networks. nauthilus-director on GitHub →
JMapper Mail
A native mail app for iPhone, iPad and Mac from the same author, built for JMAP and OpenID Connect. It registers itself with the identity provider, signs in through the system browser with PKCE and keeps only the issued tokens.
ShardPost serves it over JMAP (RFC 8620, RFC 8621) with a bearer token that Nauthilus verifies, including shared folders, Sieve scripts, quotas and delayed sending, and, on iPhone and iPad, wakes it with Web Push when new mail arrives. jmapper.de →
IMAP, POP3 and ManageSieve clients
Any standards client works: IMAP4rev2 and IMAP4rev1 with CONDSTORE, QRESYNC, IDLE, NOTIFY, COMPRESS=DEFLATE, METADATA, ACL, SORT and THREAD; POP3 for the INBOX; ManageSieve for filter scripts. Logins use a password, PLAIN, XOAUTH2 or OAUTHBEARER, over STARTTLS or implicit TLS.
The protocol reference lists every capability, command and deviation.
Inbound mail
ShardPost does not accept internet SMTP and has no submission listener. An MTA such as Postfix receives and queues the mail and hands it over with LMTP (RFC 2033). ShardPost asks for the canonical recipient, runs the user's Sieve script and stores the message; a storage error is answered with a temporary failure, so the MTA tries again.
For local delivery there is also shardpostctl deliver, with sysexits exit codes.
Web Push
When a JMAP client registers a push subscription, the separate shardpost push worker sends its notifications to the client's push service, encrypted with RFC 8291 and signed with VAPID (RFC 8292). JMapper's push service is a gateway that forwards them to Apple's push notification service. Clients that keep a connection open can use the JMAP EventSource stream instead.
Outbound mail
Mail the server creates (Sieve redirect, vacation and notifications, and JMAP EmailSubmission) goes to a configured SMTP relay, never directly to the internet. JMAP submissions can name the user with Postfix XCLIENT, and can be held back with FUTURERELEASE (RFC 4865) for delayed sending that the client can still cancel.
Observability
Prometheus metrics on /metrics, /healthz and /readyz (which checks the store and the drain state), OpenTelemetry traces over OTLP and structured JSON logs with a session ID and the real client address. An IMAP protocol trace can be switched on for a single account.
Administration
shardpostctl talks to the admin API, an OpenAPI contract, over a Unix socket in the pod or over mutual TLS. Roles such as diagnose, provision, repair, content and export keep metadata apart from message content. It shows accounts and quotas, runs maintenance jobs, traces the IMAP protocol of an account, exports and restores accounts, and imports them from another IMAP server with their UIDs and MODSEQ kept.
Step by step
What happens when …
Three everyday paths through the stack, written down from the storage guide and the protocol reference.
… a JMapper user opens the inbox
JMapper on an iPhone, Nauthilus as the OpenID Connect provider, ShardPost serving JMAP.
- Discovery. JMapper discovers the JMAP endpoint and the OpenID Connect issuer of the mail domain.
- Sign-in. It registers itself with Nauthilus and signs the user in through the system browser with PKCE, including any second factor. It keeps only the issued tokens.
- Request. Every JMAP request carries the bearer token. nauthilus-director checks it by introspection, finds the account's mail shard and forwards the request with the PROXY protocol.
- Authentication. ShardPost checks the token through Nauthilus (RFC 7662) and its auth cache and gets the canonical account.
- Reading. The account, its mailbox list and the INBOX state cost four RADOS reads. Envelopes and previews come from the index entries; the message bytes are read only when the body is opened.
- Staying current.
Email/changesanswers from the account change log. When new mail arrives, a push worker sends an encrypted Web Push notification to iPhone and iPad.
… a mail arrives
The MTA in front hands the message over with LMTP.
- Recipient. The LMTP session asks the auth cache, and on a miss Nauthilus, for the canonical account. An unknown recipient is refused before the message is sent.
- Sieve. The account's active script and the global scripts decide: keep,
fileinto, redirect, vacation. - Reserve. One compare-and-swap checks the quota, adds the size and records an intent that a blob is about to be written. Over quota, LMTP answers
552 5.2.2. - Write and index. The blob goes into the data pool, then its text into the full-text index.
- Link. A compare-and-swap on the mailbox allocates the next UID and MODSEQ and writes the entry; the account log records the change.
- Wake up. A notify on the mailbox and the account wakes IDLE and NOTIFY sessions and JMAP push workers on every server. LMTP answers
250.
… someone logs in with IMAP
A login is mostly not a Ceph event.
- Front door. nauthilus-director takes the connection, authenticates the session through Nauthilus and picks a healthy ShardPost backend.
- Handover. It connects to ShardPost with TLS and a PROXY header, so ShardPost sees the client's real address. In the reference deployment the Director logs in on the user's behalf as a master user (
<account>*nauthilus-director; name and separator are configurable). - Check. ShardPost asks its auth cache, which keeps a successful login for 30 seconds by default, and on a miss Nauthilus, with client address, protocol and TLS state.
- Session. The session opens the account's RADOS namespace. Everything it reads afterwards comes from the index and data pools.
… two servers write the same mailbox
Two deliveries to the same INBOX, on two replicas, at the same moment.
- Read. Both read UIDNEXT 43 together with the object version.
- Write with a condition. Both write “only if the object is still at that version”. RADOS lets exactly one of them win.
- Retry. The other write fails; that server reads again, gets 44 and writes 44. No UID is ever given twice.
- Notify. Each successful write rings the mailbox's watch, so the IDLE sessions on both servers see both messages.
Coordination
Coordination without a database server.
RADOS makes the change of one object atomic. Everything else ShardPost needs to run many replicas against the same accounts is built on that.
Compare-and-swap
Read the keys and the object version, compute, write only if the version is unchanged, otherwise try again. Every shared counter (UIDs, MODSEQ, quota) works this way.
Announce, write, record
A change across two objects is announced first and recorded last. Readers never move past an announcement without a record, so a JMAP state or an IMAP MODSEQ never skips a change.
Watch and notify
A doorbell, not a letter: the writer rings, the watcher looks for itself. A lost notification loses nothing, because the change log stays the source of truth.
Locks and leases
Locks in the OSD pick a single worker; leases in job records let another runner take over a job from its last checkpoint. The compare-and-swap on the data stays the final fence.
Intents
Before a write that could leave debris, ShardPost records an intent and removes it afterwards. A crash turns into a to-do list for the scrub, which needs no pool listing to find it.
Process-local sessions
Connections, parsers and caches belong to one process; durable state belongs to RADOS. A client that moves to another replica needs only a new connection.
Boundaries
Who does what, and what happens when it fails.
Responsibilities
- ShardPost: IMAP, POP3, JMAP, LMTP, ManageSieve, Sieve and the mail store; no public SMTP and no user management.
- Nauthilus: login and the resolution of an address to the canonical account; no mail content.
- nauthilus-director: public protocol access and backend routing, client address via PROXY.
- Ceph: persistence, replication and erasure coding; no transaction across mail objects.
- SMTP relay: redirect, vacation, notifications and JMAP sending.
Failures and their visible effect
- Nauthilus unreachable: new logins fail temporarily and are reported as unavailable, never as a wrong password.
- Ceph unreachable: store operations fail and readiness turns red; LMTP answers with a temporary error, so the MTA retries.
- A replica stops: its sessions end after a drain; clients reconnect to another one.
- A notification is lost: updates arrive later; the change log remains authoritative.
From the architecture overview. The architecture decisions explain why it is built this way, and what each decision costs.
Go deeper
The storage guide starts with Ceph in five minutes.
It builds the picture from the basics: Ceph, the pools, one account's objects, walkthroughs, the programs, the operator's job, monitoring and sizing.