Organization
An organization is an isolated customer of an OxiMail instance: a company, a public body, a hospital, a university, anything that gets its own walled-off slice of a shared server. In the storage model an organization is a tenant, identified by atenant_id. Every object in the system, every mailbox, email, calendar, contact, and blob, belongs to exactly one organization. Isolation is enforced in the storage layer, not just in access-control checks: tenant_id is a mandatory parameter on every store method, and every SQL query carries a WHERE tenant_id = ? clause. A bug in a request handler cannot physically reach another organization’s rows. This is defense in depth, separate from and underneath role-based access control.
Decided in ADR-015 (tenant isolation at the storage layer). The ADR also documents two narrow, deliberate exceptions where a query runs without the tenant_id filter: the cross-tenant spam-feedback aggregation, which contains no personal data, and the WebAuthn discoverable-credential lookup, which has no tenant context yet because resolving it is the whole point of that query.
Account and Principal
OxiMail keeps three concepts that other servers tend to blur cleanly apart, following RFC 9670:- User is a human who authenticates. A user has credentials and belongs to an organization.
- Account is a data space: a set of mailboxes, calendars, and contacts. Each user has a primary account and can be granted access to other accounts through sharing.
- Principal is a shareable identity. A user is a principal; so is a group, and so is a resource such as a meeting room. You share an account, or objects within it, with a principal.
UUIDv7 IDs
Every object ID in OxiMail is a UUIDv7, written as lowercase hex with dashes (for example550e8400-e29b-71d4-a716-446655440000). There is no custom encoding. UUIDv7 is time-ordered, so newer IDs sort after older ones, which gives chronological ordering and pagination for free. Validation is standard: a string is either a valid UUID or it is not. A malformed ID is never silently dropped; it immediately returns notFound.
Decided in ADR-008 (UUIDv7 for all IDs).
Blob
A blob is a piece of opaque content, most often the raw bytes of an email or an attachment. Blobs are content-addressed: a blob’s identifier is the SHA-256 hash of its content. Two identical attachments therefore resolve to the same blob and are stored once. Deduplication happens only at this storage layer and only by content hash, never byMessage-Id: two emails that share a Message-Id are always two distinct Email objects, even if they point at the same underlying blob. This is what makes sending mail to yourself work correctly.
When an email is ingested it is parsed exactly once. The raw RFC 5322 bytes are stored as a blob, and a compact structured snapshot (parsed headers, flags, thread references, derived fields such as hasAttachment) is stored in the database. Reads never re-parse the raw message: queries use the structured snapshot, and the raw blob is kept for faithful export, MIME download, and signature re-verification.
Decided in ADR-014 (blob dedup by content hash) and ADR-009 (store raw plus structured).
Capabilities
A capability is a feature set the server advertises in the JMAP session, identified by a URI such asurn:ietf:params:jmap:mail. A client opts in to the capabilities it intends to use by listing them in the using array of a request; calling a method whose capability was not requested is an error, not a silent success.
OxiMail advertises two generations of capabilities side by side. The v1 capabilities are the standard RFC 8620 / RFC 8621 set. Alongside them it advertises a modernized v2 set under the urn:oximail:params:jmap:v2:* namespace. Both are visible in the session, and a request may use either one or both at the same time. Each handler registers its v2 URN as an additional capability, so a request that lists both a v1 capability and its v2 counterpart receives the union of their properties. The dispatcher keeps a mutual-exclusion mechanism, but its pair list is currently empty, so no combination is rejected today (the mechanism is retained for a possible future generation). Which capabilities a given account actually sees is filtered down by its organization plan and its account role; these levels can only restrict the server set, never enlarge it.
See ADR-028 (dynamic accountCapabilities) for the per-account filtering model. For the wire detail of the session and the using array, see JMAP Core.
Fail-loud
Fail-loud is the project’s first rule: OxiMail never silently drops data and never swallows an error. An invalid ID is reported innotFound, not discarded. A serialization failure returns serverFail, not an empty object. An email that was accepted with 250 OK is always delivered somewhere, even if later processing fails. Defaults are never used to mask an error, and a Rust Result is never quietly ignored. This principle is enforced in the code by clippy::unwrap_used and clippy::expect_used deny rules, so a stray unwrap() in production code fails the build.
Decided in ADR-027 (fail-silent audit), which records the original sweep that fixed every fail-silent pattern then known and put the lint gate in place.
Where to go next
- Architecture at a glance for how these concepts sit inside the single binary.
- JMAP Core for the session, capabilities, and request format on the wire.