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::dispatch → crate::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§
Sourcetype Error: Error + Send + Sync + 'static
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.
Sourcetype CallerCtx: Clone + Send + Sync + 'static
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:
Clonebecausecrate::Dispatcherclones the value once per method call in the batch.Send + 'staticbecause each method call is spawned on atokio::task.Syncbecause handler method bodies take&Self::CallerCtxand hold that reference across.awaitboundaries inside aSendfuture (a&TisSendiffT: Sync).
Required Methods§
Sourcefn account_exists(
&self,
caller: &Self::CallerCtx,
account_id: &Id,
) -> impl Future<Output = Result<bool, Self::Error>> + Send
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.
Sourcefn 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_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.
Sourcefn get_state<O: JmapObject + Send + Sync>(
&self,
caller: &Self::CallerCtx,
account_id: &Id,
) -> impl Future<Output = Result<State, 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
Return the current state token for an object type in the given account.
Sourcefn 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 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.
Sourcefn 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_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.
Sourcefn 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
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§
Sourcefn principal_id(caller: &Self::CallerCtx) -> Option<&Id>
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.
Sourcefn max_objects_in_set(&self, caller: &Self::CallerCtx, account_id: &Id) -> u64
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".