oximail-core is the foundation crate every other domain crate builds on. It implements JMAP Core (RFC 8620): the session resource a client discovers the server with, the dispatcher that routes a batch of method calls, the capability model that gates them, the push delivery types, the blob and ID primitives, the typed error vocabulary, and the EventBus that lets domain crates talk to each other without importing one another.
Overview
Every authenticated JMAP request passes through this crate. The HTTP layer in oximail-server parses the request, builds a RequestContext, and hands it to the MethodRegistry defined here. The registry resolves back-references, checks capabilities and access, and dispatches each call to the handler registered for that method name. Domain crates (oximail-mail, oximail-calendar, oximail-contact, and the rest) implement the JmapMethod trait and register their handlers; oximail-core itself owns only the cross-cutting machinery and a small set of core-level methods.
It sits at the bottom of the request path. It depends on oximail-store for the ID, blob, and state primitives, and on nothing above it. The mail crate never imports the calendar crate; both depend on oximail-core and communicate through its EventBus.
Features
- Session resource (RFC 8620 §2): the single discovery document served at
/.well-known/jmap, listing capabilities, accounts, primary accounts, and the API / upload / download / EventSource URLs.
- Method dispatch (RFC 8620 §3.5): a registry of
JmapMethod handlers keyed by method name, executing each call in a batched request in order.
- Back-reference resolution (RFC 8620 §3.7): creation references (
#clientId) and result references ({ "resultOf", "name", "path" }, including #-prefixed keys and the * wildcard JSON Pointer).
- Capability model (RFC 8620 §2, ADR-028): server-level capabilities, dynamic per-account capabilities, and the v1 / v2 URN surfaces advertised side by side.
- Typed error vocabulary (RFC 8620 §3.6): request-level, method-level, and set-level error enums that serialize to the exact JMAP wire
type.
- Generic filter machinery: a recursive AND / OR / NOT combinator shared by every
Foo/query method.
- Push delivery types (RFC 8620 §7): push subscriptions, the
StateChange notification payload, and the Web Push (RFC 8291) and VAPID primitives behind EventSource and WebSocket delivery.
- EventBus (ADR-011): a typed publish / subscribe channel for cross-domain events.
- Core-level methods:
Core/echo, PushSubscription/*, plus the saved-message (bookmark) and message-task-link types and the v2 streaming surfaces.
How it works
The session resource
A client begins by fetching the session resource. Session::build assembles the server-level capabilities map, the accounts map (the user’s personal account plus any shared accounts), the primaryAccounts map, the API / upload / download / EventSource URL templates, and an opaque state string. The limits announced in the Core capability (maxSizeUpload, maxCallsInRequest, maxObjectsInGet, and the rest) are defined as constants in this crate so that the values advertised in the session and the values enforced at dispatch come from one source of truth.
Session::build_for_account layers the ADR-028 pipeline on top: it computes the per-account capabilities from the organization’s plan and the account’s role, and trims primaryAccounts to only the capabilities the account actually has. Shared accounts are added separately by add_shared_accounts, each keyed shared:{ownerId} and carrying only the capabilities for the object types that were shared.
Dispatch and back-references
MethodRegistry::process walks the method calls of a request in order. For each call it:
- Resolves back-references in the arguments. A string
"#name" is replaced by the server id created earlier in the batch; an object with resultOf / name / path (or a #-prefixed key) is replaced by the value at that JSON Pointer in a prior call’s result. A reference to an unknown call id, or a path that does not resolve, returns invalidResultReference rather than a silent null.
- Validates
accountId: the requested account must match the authenticated account, a shared:-prefixed form of it, or the authenticating account. A mismatch returns accountNotFound.
- Checks the capability: the handler’s required capability (or one of its additional capabilities) must be present in the request’s
using array, otherwise unknownCapability.
- Applies the ADR-028 access checks: the capability must be in this account’s capabilities (
accountNotSupportedByMethod if not), a read-only account cannot call a /set method (forbidden), and an app-password scope can narrow access further (forbidden).
- Executes the handler and stores its result so later calls in the same batch can reference it.
Created ids are accumulated across the batch and returned in the response createdIds. A failure in one call produces an error response for that call and processing continues with the next; the batch is not aborted.
Capabilities: v1 and v2 coexist
OxiMail advertises two generations of capability URNs in the same session: the standard urn:ietf:params:jmap:* set (v1, RFC 8620 / RFC 8621 and friends) and the OxiMail urn:oximail:params:jmap:v2:* set. A v2 handler does not replace its v1 counterpart; it registers its v2 URN as an additional capability on the same handler.
v1 and v2 capabilities are not mutually exclusive. The dispatcher keeps a list of mutually-exclusive v1 / v2 pairs (V1_V2_MUTUAL_EXCLUSION_PAIRS), but on the current line that list is empty: every domain that once had a pair (mail, contacts, calendars) has dropped it. A request that lists both the v1 and the v2 URN of a domain in using is accepted, and the wire shape is the union of the v1 and v2 properties. The mutex check still runs, so a future v3 surface could re-introduce a pair without re-implementing the rejection path, but today it iterates an empty list.
EventBus: cross-domain without coupling
The EventBus is a typed broadcast channel. A crate emits a DomainEvent (for example EmailIngested, CalendarEventCreated, StateChanged) and any number of subscribers in other crates react to it. Each event variant carries all the data a subscriber needs, so the subscribing crate never has to import the emitting crate. This is what keeps the mail crate and the calendar crate from depending on each other: an inbound iTIP email becomes an ITipReceived event that the calendar subscriber turns into a calendar event.
StateChanged is the bridge to push: after a /set handler writes its changes, it emits StateChanged with the new state, and the push worker turns that into a notification for subscribed clients.
Push delivery
Push subscriptions (RFC 8620 §7.2) let a client register a URL to be POSTed when state changes. The StateChange payload (RFC 8620 §7.1) carries a map of account id to changed type and new state, optionally tagged with a distributed-causality vector so multi-device clients can apply patches in a deterministic order. The Web Push encryption (RFC 8291) and VAPID signing primitives live in this crate; the actual delivery loop runs in the push worker.
Invariants
- Never fail silently. Unparseable ids, unknown capabilities, account mismatches, and bad result references all return a typed error; nothing is dropped on the floor.
- One source of truth for limits. Session-advertised limits and dispatch-enforced limits are the same constants.
- No upward dependencies.
oximail-core depends only on oximail-store. Cross-domain communication goes through the EventBus, never through a direct import.
Configuration
oximail-core reads no configuration files directly; it consumes values resolved by the binary (base URL, capability set, organization plan, account role). The server-level limits it advertises are compile-time constants. Operator-facing knobs that influence the surface this crate builds (the public base URL, TLS, organization plans and roles) are documented in the configuration reference.
Protocol / JMAP surface
The bulk of the JMAP surface is implemented in the domain crates. oximail-core owns the dispatch machinery and these core-level methods:
| Method | Purpose |
|---|
Core/echo | RFC 8620 §4.1 — returns its arguments unchanged. The canonical connectivity check. |
PushSubscription/get, PushSubscription/set | RFC 8620 §7.2 — manage a client’s push subscriptions. |
SavedMessage/get, /set, /query, /queryChanges, /changes | Bookmarked messages (the savedmessages capability). |
MessageTaskLink/get, /set, /query, /queryChanges, /changes | Associations between messages and tasks (the tasklinks capability). |
Core/queryStream | JMAP v2 streaming — wraps a Foo/query to stream results in chunks. |
Push/catchup | JMAP v2 streaming — replays state changes a reconnecting client missed. |
It also provides QueryChangesStub, a generic handler that returns cannotCalculateChanges (RFC 8620 §5.6) for Foo/queryChanges surfaces that fall back to a full re-query.
The reusable filter and dispatch contracts it exports are the JmapMethod trait, the MethodRegistry, and the generic Filter / FilterOperator types. See JMAP Core for the client-facing protocol walk-through.
Design decisions
- ADR-011 (Event bus for cross-domain) — the mail crate never imports the calendar crate; they communicate through the
DomainEvent broadcast channel defined here.
- ADR-028 (Dynamic account capabilities) — per-account capabilities are the intersection of the server capabilities, the organization plan, and the account role, with explicit overrides; the dispatch-time access checks enforce it.
- ADR-008 (UUIDv7 IDs) — the
Id primitive (re-exported from oximail-store); a malformed id is never silently dropped.
- ADR-014 (Blob dedup by content hash) and ADR-009 (store raw plus structured) — the
BlobId primitive this crate re-exports is the SHA-256 of content.
- ADR-054 / ADR-056 / ADR-066 (JMAP v2 architecture, capability negotiation, schema evolution) — the v1 / v2 dual-capability model and the (currently empty) mutual-exclusion list.
- ADR-072 (Per-collection volume caps) — the
tooManyEntries error variants and the advertised resource limits.
ADRs live in the OxiMail server repository; see the ADR index.
Standards
- RFC 8620 — JMAP Core (session, dispatch, back-references, capabilities, push, errors). The primary reference for this crate.
- RFC 6750 — Bearer token authentication (the EventSource query-param token, §2.3).
- RFC 6901 — JSON Pointer, used to resolve result-reference paths.
- RFC 8291 — Message Encryption for Web Push.
- RFC 8292 — VAPID, for push authentication.
- RFC 8887 — JMAP over WebSocket (the
websocket capability).
Source map & tests
| File (within the crate) | What it holds |
|---|
src/session.rs | The Session resource, server limits as constants, shared-account assembly. Guarded by test_session_full_checklist. |
src/dispatch.rs | MethodRegistry, the JmapMethod trait, back-reference resolution, the v1 / v2 mutex list, Core/echo, QueryChangesStub. Guarded by test_dispatch_full_checklist. |
src/capabilities.rs | TenantPlan, AccountRole, and build_account_capabilities (the ADR-028 intersection). |
src/event_bus.rs | DomainEvent, EventBus, EventEmitter. |
src/jmap_filter.rs | The generic Filter / FilterOperator combinator and structural validation. |
src/push.rs, src/push_store.rs, src/push_worker.rs, src/web_push.rs, src/vapid.rs | Push subscriptions, the StateChange payload, Web Push and VAPID. |
src/error.rs | RequestError, MethodError, SetError — the typed JMAP error vocabulary. |
src/types.rs | Account, plus the Id, BlobId, State re-exports from oximail-store. |
src/methods/ | The core-level method handlers (SavedMessage/*, MessageTaskLink/*, Core/queryStream, Push/catchup, PushSubscription/*). |
tests/concurrency.rs | Integration coverage for concurrent dispatch. |
The dispatch and session modules each carry a single “checklist” test that exercises every responsibility of the module in one flow; changing dispatch or session behavior means updating that test.
Status
Implemented and in production at v0.30.0: the session resource, the full dispatch and back-reference machinery, the ADR-028 capability model, the typed error vocabulary, the generic filter combinator, the EventBus, push subscriptions and the StateChange payload, and all the core-level methods listed above.
The v2 streaming methods (Core/queryStream, Push/catchup) and the v2 capability URNs are advertised and wired on the current line. The v1 / v2 mutual-exclusion infrastructure exists but is inert: the pair list is empty, so no request is rejected for pairing v1 and v2 capabilities.