Skip to main content

JmapBackend

Trait JmapBackend 

Source
pub trait JmapBackend:
    Send
    + Sync
    + 'static {
    type Error: Error + Send + Sync + 'static;
    type CallerCtx: Clone + Send + Sync + 'static;

    // Required methods
    fn account_exists(
        &self,
        caller: &Self::CallerCtx,
        account_id: &Id,
    ) -> impl Future<Output = Result<bool, Self::Error>> + Send;
    fn get_objects<O: GetObject + Send + Sync>(
        &self,
        caller: &Self::CallerCtx,
        account_id: &Id,
        ids: Option<&[Id]>,
        properties: Option<&[String]>,
    ) -> impl Future<Output = Result<(Vec<O>, Vec<Id>), Self::Error>> + Send;
    fn get_state<O: JmapObject + Send + Sync>(
        &self,
        caller: &Self::CallerCtx,
        account_id: &Id,
    ) -> impl Future<Output = Result<State, Self::Error>> + Send;
    fn get_changes<O: JmapObject + Send + Sync>(
        &self,
        caller: &Self::CallerCtx,
        account_id: &Id,
        since_state: &State,
        max_changes: Option<u64>,
    ) -> impl Future<Output = Result<ChangesResult, BackendChangesError<Self::Error>>> + Send;
    fn query_objects<O: QueryObject + Send + Sync>(
        &self,
        caller: &Self::CallerCtx,
        account_id: &Id,
        filter: Option<&O::Filter>,
        sort: Option<&[O::Comparator]>,
        limit: Option<u64>,
        position: i64,
    ) -> impl Future<Output = Result<QueryResult, Self::Error>> + Send;
    fn query_changes<O: QueryObject + Send + Sync>(
        &self,
        caller: &Self::CallerCtx,
        account_id: &Id,
        since_query_state: &State,
        filter: Option<&O::Filter>,
        sort: Option<&[O::Comparator]>,
        max_changes: Option<u64>,
        up_to_id: Option<&Id>,
        collapse_threads: bool,
    ) -> impl Future<Output = Result<QueryChangesResult, BackendChangesError<Self::Error>>> + Send;

    // Provided methods
    fn principal_id(caller: &Self::CallerCtx) -> Option<&Id> { ... }
    fn max_objects_in_set(
        &self,
        caller: &Self::CallerCtx,
        account_id: &Id,
    ) -> u64 { ... }
}
Expand description

Read-side backend supertrait shared by all JMAP server crates.

Domain-specific backend traits (MailBackend, ChatBackend, etc.) require this trait as a supertrait and add write-side methods on top.

Only the read operations that have an identical signature across all JMAP object types belong here. Write operations (create_object, update_object, destroy_object) and domain-specific operations remain in the domain crate.

The collapse_threads parameter on query_changes is included for Email/queryChanges (RFC 8621 §4.5). Non-mail backends should pass false and may ignore the parameter.

This trait is not object-safe by design (generic methods). Use Arc<impl JmapBackend> when sharing across tasks.

§CallerCtx

Every backend method takes a caller: &Self::CallerCtx parameter as the first argument after &self. This is the per-request authentication / authorisation context produced by the caller’s auth layer and forwarded unchanged through crate::Dispatcher::dispatchcrate::JmapHandler → the registered closure → the backend.

Implementations that do not need an auth identity can use the unit type:

impl JmapBackend for MyBackend {
    type Error = MyError;
    type CallerCtx = ();
    // ...
}

Implementations that do need to differentiate behaviour per caller (e.g. applying per-user visibility rules, or rejecting reads with forbidden when the caller is not the owner of the account) read the caller parameter to decide.

The trait bound Clone + Send + 'static is what crate::Dispatcher requires; the bound is repeated here so the supertrait can stand on its own without depending on the dispatcher.

Required Associated Types§

Source

type Error: Error + Send + Sync + 'static

The error type returned by storage operations.

§Security

The Display impl of this type is surfaced through BackendSetError::Other’s and BackendChangesError::Other’s own Display impls, which in turn flow into crate::request_error’s RequestError::Display output. When a downstream consumer wires tracing-style logging on top, the formatted error text lands in operator logs verbatim.

Implementations MUST NOT include any of the following in this type’s Display output:

  • Credential material — auth tokens, passwords, push verification codes, invite codes, session cookies, or anything derived byte-for-byte from an Authorization-header value.
  • Blob content — email bodies, sieve scripts, file contents, or any user-supplied opaque payload. An error like "sieve parse error at line 42: <script excerpt>" violates this — emit the line number and a short type-only summary (“sieve parse error at line 42: unexpected token”) and let the server log the full script body separately under a redacted path.
  • PII shaped like an email address in any code path that an unauthenticated caller can trigger. Wrapping a downstream service error that interpolates the caller’s email is the common foot-gun.

Errors that wrap a downstream-service failure should sanitize the downstream error text — or strip it entirely and replace it with a static summary — before constructing the Display string. The same rule applies to every extension *Backend trait that inherits this associated type by transitivity: MailBackend::Error, ChatBackend::Error, CalendarsBackend::Error, TasksBackend::Error, ContactsBackend::Error, FileNodeBackend::Error, and SharingBackend::Error are all the same JmapBackend::Error associated type — the contract here governs all of them.

Precedent: bd:JMAP-sc1b.79 redacted BearerAuth and BasicAuth at the type-derive level; bd:JMAP-sc1b.100 documents the equivalent contract at the trait-associated-type level.

Source

type CallerCtx: Clone + Send + Sync + 'static

The per-request caller context type produced by the auth layer and forwarded by crate::Dispatcher::dispatch into every method call.

Use () when no auth context is needed.

The bound is Clone + Send + Sync + 'static:

  • Clone because crate::Dispatcher clones the value once per method call in the batch.
  • Send + 'static because each method call is spawned on a tokio::task.
  • Sync because handler method bodies take &Self::CallerCtx and hold that reference across .await boundaries inside a Send future (a &T is Send iff T: Sync).

Required Methods§

Source

fn account_exists( &self, caller: &Self::CallerCtx, account_id: &Id, ) -> impl Future<Output = Result<bool, Self::Error>> + Send

Return true if the given account exists in this backend.

Handlers call this at the start of each method to return accountNotFound (RFC 8620 §3.6.2) rather than surfacing the wrong error when accountId is unknown.

§Performance contract (bd:JMAP-jfia.27)

Implementations SHOULD return in sub-millisecond time. The JMAP standard handlers (handle_get, handle_changes, handle_query, handle_query_changes) each call account_exists at the START of the method, BEFORE delegating to the per-domain backend. A typical JMAP request batch is 4–16 method calls; a naive remote round-trip per call would add 4–16× the network latency to every batch.

Acceptable backing strategies: in-memory cache (the reference MemoryBackend impls all use this); an indexed primary-key lookup against a local database; a bloom filter for negative lookups paired with a cache for positives. A naive SELECT 1 FROM accounts WHERE id = ? against a remote database is INCORRECT for this method even though it would return the right value — the round-trip cost is the bug.

The dispatcher does not cache this call across the method calls in a single batch (workspace-architectural decision — the dispatcher is intentionally stateless across the batch loop). Caching is the backend implementor’s responsibility.

Source

fn get_objects<O: GetObject + Send + Sync>( &self, caller: &Self::CallerCtx, account_id: &Id, ids: Option<&[Id]>, properties: Option<&[String]>, ) -> impl Future<Output = Result<(Vec<O>, Vec<Id>), Self::Error>> + Send

Fetch objects by id (or all objects when ids is None).

properties is the list of property names requested by the client (RFC 8620 §5.1). None means the client did not send a properties field; the backend should return all properties. When Some, the backend MAY filter the response to only the named properties, but is not required to — implementations that always return all properties are correct.

Returns (found, not_found) — objects that exist and ids that do not.

Source

fn get_state<O: JmapObject + Send + Sync>( &self, caller: &Self::CallerCtx, account_id: &Id, ) -> impl Future<Output = Result<State, Self::Error>> + Send

Return the current state token for an object type in the given account.

Source

fn get_changes<O: JmapObject + Send + Sync>( &self, caller: &Self::CallerCtx, account_id: &Id, since_state: &State, max_changes: Option<u64>, ) -> impl Future<Output = Result<ChangesResult, BackendChangesError<Self::Error>>> + Send

Return changes since since_state, up to max_changes entries.

Source

fn query_objects<O: QueryObject + Send + Sync>( &self, caller: &Self::CallerCtx, account_id: &Id, filter: Option<&O::Filter>, sort: Option<&[O::Comparator]>, limit: Option<u64>, position: i64, ) -> impl Future<Output = Result<QueryResult, Self::Error>> + Send

Execute a /query and return a page of matching ids.

position may be negative — negative values are relative to the end of the result set per RFC 8620 §5.5 (e.g. -1 means the last result).

§Filter and sort handling

Implementations MUST honour the supplied filter and sort arguments efficiently — typically by pushing both into the indexed storage layer (database WHERE / ORDER BY, search index, etc.). Returning every matching id and relying on the caller to paginate after the fact degenerates to O(n) per page for IMAP-migration accounts.

Handler implementations in jmap-*-server crates SHOULD NOT post-filter or post-sort the backend’s result; doing so re-introduces the O(n) cost this method exists to avoid. The Mailbox handler in jmap-mail-server is the canonical example of pushing filter/sort fully into the backend.

Source

fn query_changes<O: QueryObject + Send + Sync>( &self, caller: &Self::CallerCtx, account_id: &Id, since_query_state: &State, filter: Option<&O::Filter>, sort: Option<&[O::Comparator]>, max_changes: Option<u64>, up_to_id: Option<&Id>, collapse_threads: bool, ) -> impl Future<Output = Result<QueryChangesResult, BackendChangesError<Self::Error>>> + Send

Execute a /queryChanges and return deltas since since_query_state.

collapse_threads is only meaningful for Email/queryChanges (RFC 8621 §4.5). Pass false for all other object types.

Provided Methods§

Source

fn principal_id(caller: &Self::CallerCtx) -> Option<&Id>

The caller’s stable identity within this account namespace.

Returns None for deployments that have not wired identity (test fixtures, single-user dev servers). A None-returning backend CANNOT honor JMAP semantics that depend on caller identity — chat role-hierarchy, calendar ACLs, sharing/myRights, per-user $seen on shared mailboxes, metadata isPrivate visibility scoping, etc. Authentication is still the HTTP layer’s job; this method exposes the result of that authentication to the JMAP layer for in-method semantics.

Implementations MUST NOT mint identity — they MUST read it from the CallerCtx populated by the HTTP/auth middleware before dispatch() was called.

Backends that honor identity-dependent semantics MUST override this method. Handlers and downstream backend traits MAY rely on it being correct when it returns Some.

§Why an associated function and not a method (bd:JMAP-wlip.6 / bd:JMAP-jfia.13)

The signature deliberately takes caller: &Self::CallerCtx without a &self receiver. Backends therefore have no access to their own storage state from inside principal_id. The auth-layer middleware MUST pre-resolve the principal (e.g. map a JWT sub claim to a local Id via an internal lookup) and stash the result inside CallerCtx before it calls dispatch. The JMAP layer reads the pre-resolved value here; no JIT lookup is possible.

This is a structural enforcement of the “identity is not the JMAP layer’s job to mint” rule. A consumer that wants JIT-resolved identity (e.g. database-backed JWT → principal mapping) wires that mapping into the HTTP layer’s CallerCtx construction step instead of trying to fit it inside the backend’s principal_id impl.

Decision record (bd:JMAP-jfia.13): a future reviewer or AI tool will reasonably suggest “this is an oversight, surely the backend wants &self access to its identity store” and propose fn principal_id(&self, caller: ...) -> .... That suggestion is WRONG and must be rejected: the function-vs-method distinction is the structural enforcement of the no-JIT-lookup policy. Comments alone could be ignored; the type system makes the wrong thing impossible. Defending this shape protects the workspace identity-seam policy from drift.

Source

fn max_objects_in_set(&self, caller: &Self::CallerCtx, account_id: &Id) -> u64

Maximum number of objects this account permits in a single /set call (RFC 8620 §5.3 maxObjectsInSet) (bd:JMAP-ayoz.41.1).

Counts the sum of create + update + destroy entries in the wire arguments. Handlers MUST enforce this at the top of every handle_*_set via crate::helpers::enforce_max_objects_in_set so a single batched request cannot drive O(M·N) work against the storage layer.

The default value 500 mirrors the workspace’s testjig Session JSON advertised cap and matches common Fastmail / Cyrus IMAP server defaults. The cap is a floor on permissiveness, not a floor on capability — backends MAY override per account (Free vs Pro tier, multi-tenant SaaS, etc.). The caller and account_id arguments are passed even though the default impl ignores them so production backends can vary without an API break.

Returning 0 makes every /set call fail with limit maxObjectsInSet (defensive read-only mode). Returning u64::MAX effectively disables the cap (NOT recommended — the cap is a DoS defence and disabling it forfeits that defence).

§Why on JmapBackend and not a per-extension XxxLimits struct

maxObjectsInSet is RFC 8620 §5.3 base-protocol scope, not an extension concept. Putting it on the foundation supertrait covers all 8 extension server crates with one default impl; per-extension XxxLimits structs are the right shape for extension-specific caps (per-Space content limits, per-Mailbox message size, etc. per workspace AGENTS.md “Backend caps and limits”) but are out of scope here.

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§