Skip to main content

jmap_server/
backend.rs

1//! Shared backend infrastructure for all JMAP server crates.
2//!
3//! Re-exports the marker traits from `jmap-types` and adds the result types,
4//! `BackendChangesError`, and [`JmapBackend`] supertrait. Domain crates add
5//! their write-side methods and domain-specific error variants on top.
6
7pub use jmap_types::{GetObject, JmapObject, QueryObject, SetObject};
8
9// ---------------------------------------------------------------------------
10// SetError — RFC 8620 §5.3 per-object set-method error
11// ---------------------------------------------------------------------------
12
13/// A per-item error in a `/set` response (`notCreated`, `notUpdated`,
14/// `notDestroyed` maps) (RFC 8620 §5.3).
15///
16/// Construct with [`SetError::new`] and chain the builder methods as needed.
17#[non_exhaustive]
18#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct SetError {
21    /// The machine-readable error type.
22    #[serde(rename = "type")]
23    pub error_type: SetErrorType,
24    /// Optional human-readable description of the error.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub description: Option<String>,
27    /// Property names that caused the error (for `invalidProperties`).
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub properties: Option<Vec<String>>,
30    /// The existing object id (for `alreadyExists` — RFC 8621 §5.7).
31    #[serde(rename = "existingId", skip_serializing_if = "Option::is_none")]
32    pub existing_id: Option<jmap_types::Id>,
33    /// Maximum recipients allowed (for `tooManyRecipients` — RFC 8621 §7.5).
34    #[serde(rename = "maxRecipients", skip_serializing_if = "Option::is_none")]
35    pub max_recipients: Option<u64>,
36    /// Invalid recipient addresses (for `invalidRecipients` — RFC 8621 §7.5).
37    #[serde(rename = "invalidRecipients", skip_serializing_if = "Option::is_none")]
38    pub invalid_recipients: Option<Vec<String>>,
39    /// Missing blob IDs (for `blobNotFound` — RFC 8621 §5.5).
40    #[serde(rename = "notFound", skip_serializing_if = "Option::is_none")]
41    pub not_found: Option<Vec<jmap_types::Id>>,
42    /// Maximum message size in octets (for `tooLarge` on EmailSubmission — RFC 8621 §7.5).
43    #[serde(rename = "maxSize", skip_serializing_if = "Option::is_none")]
44    pub max_size: Option<u64>,
45    /// Catch-all for extension-defined SetError fields not covered by
46    /// the typed members above.
47    ///
48    /// JMAP extensions sometimes ship error variants whose wire shape
49    /// includes additional structured fields beyond the RFC 8620 §5.3
50    /// base set — e.g. JMAP Chat's `rateLimited` SetError carries a
51    /// `serverRetryAfter` UTCDate telling the client when it may
52    /// retry, and `mdnAlreadySent` (RFC 8621 §7.7) is a typed
53    /// extension error variant. This map preserves any such field
54    /// across serialize / deserialize round-trip, mirroring the
55    /// extras-preservation policy on the client-side
56    /// [`jmap_types::SetError`] type.
57    ///
58    /// Use [`SetError::with_extra`] to populate from handler code:
59    ///
60    /// ```ignore
61    /// SetError::new(SetErrorType::custom("rateLimited"))
62    ///     .with_description("Slow mode is active for this chat")
63    ///     .with_extra("serverRetryAfter", json!(retry_after_str))
64    /// ```
65    ///
66    /// Per workspace AGENTS.md "Extras-preservation policy" — wire
67    /// format is byte-identical to a pre-extras SetError when the
68    /// map is empty (the `skip_serializing_if` collapses it).
69    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
70    pub extra: serde_json::Map<String, serde_json::Value>,
71}
72
73impl SetError {
74    /// Construct a [`SetError`] with the given type and all optional fields `None`.
75    pub fn new(error_type: SetErrorType) -> Self {
76        Self {
77            error_type,
78            description: None,
79            properties: None,
80            existing_id: None,
81            max_recipients: None,
82            invalid_recipients: None,
83            not_found: None,
84            max_size: None,
85            extra: serde_json::Map::new(),
86        }
87    }
88
89    /// Set the human-readable description.
90    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
91        self.description = Some(desc.into());
92        self
93    }
94
95    /// Set the list of property names that caused the error.
96    pub fn with_properties<I, S>(mut self, props: I) -> Self
97    where
98        I: IntoIterator<Item = S>,
99        S: Into<String>,
100    {
101        self.properties = Some(props.into_iter().map(|s| s.into()).collect());
102        self
103    }
104
105    /// Set the existing object id (used with `alreadyExists`).
106    pub fn with_existing_id(mut self, id: jmap_types::Id) -> Self {
107        self.existing_id = Some(id);
108        self
109    }
110
111    /// Set the maximum recipients (used with `tooManyRecipients` — RFC 8621 §7.5).
112    pub fn with_max_recipients(mut self, n: u64) -> Self {
113        self.max_recipients = Some(n);
114        self
115    }
116
117    /// Set the invalid recipient addresses (used with `invalidRecipients` — RFC 8621 §7.5).
118    pub fn with_invalid_recipients<I, S>(mut self, addrs: I) -> Self
119    where
120        I: IntoIterator<Item = S>,
121        S: Into<String>,
122    {
123        self.invalid_recipients = Some(addrs.into_iter().map(|s| s.into()).collect());
124        self
125    }
126
127    /// Set the missing blob IDs (used with `blobNotFound` — RFC 8621 §5.5).
128    pub fn with_not_found(mut self, ids: Vec<jmap_types::Id>) -> Self {
129        self.not_found = Some(ids);
130        self
131    }
132
133    /// Set the maximum message size in octets (used with `tooLarge` on EmailSubmission — RFC 8621 §7.5).
134    pub fn with_max_size(mut self, n: u64) -> Self {
135        self.max_size = Some(n);
136        self
137    }
138
139    /// Insert an extension-defined field into [`Self::extra`].
140    ///
141    /// Used by handlers to attach typed wire fields that no `with_*`
142    /// builder covers — for example JMAP Chat's `rateLimited` SetError
143    /// must carry a `serverRetryAfter` UTCDate:
144    ///
145    /// ```ignore
146    /// SetError::new(SetErrorType::custom("rateLimited"))
147    ///     .with_description("Slow mode is active for this chat")
148    ///     .with_extra("serverRetryAfter", serde_json::json!(retry_after_str))
149    /// ```
150    ///
151    /// The serialized wire shape merges `key`/`value` at the same
152    /// level as the typed fields (via `#[serde(flatten)]` on
153    /// [`Self::extra`]). Calling `with_extra("type", ...)`,
154    /// `with_extra("properties", ...)`, or any other reserved
155    /// wire-name will produce a malformed SetError on the wire —
156    /// callers are responsible for choosing extension-namespace keys
157    /// that do not collide with the typed-field wire names.
158    pub fn with_extra(mut self, key: &str, value: serde_json::Value) -> Self {
159        self.extra.insert(key.to_owned(), value);
160        self
161    }
162}
163
164impl std::fmt::Display for SetError {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        write!(f, "{}", self.error_type)?;
167        if let Some(ref desc) = self.description {
168            write!(f, ": {desc}")?;
169        }
170        Ok(())
171    }
172}
173
174/// The machine-readable type for a [`SetError`] (RFC 8620 §5.3 and RFC 8621).
175///
176/// Extension crates define their own error strings via [`SetErrorType::custom`]
177/// rather than adding variants here. This keeps the base crate stable as new
178/// JMAP extension crates (calendar, contacts, etc.) are added.
179#[non_exhaustive]
180#[derive(Debug, Clone, PartialEq)]
181pub enum SetErrorType {
182    /// The action would violate an ACL or other access control policy.
183    Forbidden,
184    /// Creating or modifying the object would exceed a server quota.
185    OverQuota,
186    /// The object is too large to be stored by the server.
187    TooLarge,
188    /// The server is rate-limiting this client.
189    RateLimit,
190    /// The object to be updated or destroyed does not exist.
191    NotFound,
192    /// The patch object is not a valid JSON Merge Patch or cannot be applied.
193    InvalidPatch,
194    /// The client requested destruction of an object that will be destroyed
195    /// implicitly when another object is destroyed.
196    WillDestroy,
197    /// One or more properties have invalid values.
198    InvalidProperties,
199    /// The object type is a singleton and cannot be created or destroyed.
200    Singleton,
201    /// An object with the same unique key already exists.
202    AlreadyExists,
203    /// RFC 8621 §2.5 — Mailbox has child mailboxes and cannot be destroyed.
204    MailboxHasChild,
205    /// RFC 8621 §2.5 — Mailbox contains emails and `onDestroyRemoveEmails` is false.
206    MailboxHasEmail,
207    /// RFC 8621 §5.5 — Too many keywords on the Email.
208    TooManyKeywords,
209    /// RFC 8621 §5.5 — Email is in too many mailboxes.
210    TooManyMailboxes,
211    /// RFC 8621 §5.5 — A referenced blob was not found.
212    BlobNotFound,
213    /// RFC 8621 §6.3 — The `from` address is not permitted for this Identity.
214    ForbiddenFrom,
215    /// RFC 8621 §7.5 — The Email is invalid for submission.
216    InvalidEmail,
217    /// RFC 8621 §7.5 — Too many recipients.
218    TooManyRecipients,
219    /// RFC 8621 §7.5 — No recipients specified.
220    NoRecipients,
221    /// RFC 8621 §7.5 — One or more recipient addresses are invalid.
222    InvalidRecipients,
223    /// RFC 8621 §7.5 — The MAIL FROM address is not permitted.
224    ForbiddenMailFrom,
225    /// RFC 8621 §7.5 — The user does not have send permission.
226    ForbiddenToSend,
227    /// RFC 8621 §7.5 — The submission cannot be undone.
228    CannotUnsend,
229    /// An extension-defined error type not covered by the variants above.
230    /// Serializes as the inner string directly (e.g. `"mdnAlreadySent"`).
231    Custom(String),
232}
233
234impl SetErrorType {
235    /// Construct a [`SetErrorType::Custom`] from any string.
236    ///
237    /// Use this in extension crates to emit domain-specific error types
238    /// without adding variants to this enum.
239    pub fn custom(s: impl Into<String>) -> Self {
240        Self::Custom(s.into())
241    }
242}
243
244impl std::fmt::Display for SetErrorType {
245    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246        let s: &str = match self {
247            Self::Forbidden => "forbidden",
248            Self::OverQuota => "overQuota",
249            Self::TooLarge => "tooLarge",
250            Self::RateLimit => "rateLimit",
251            Self::NotFound => "notFound",
252            Self::InvalidPatch => "invalidPatch",
253            Self::WillDestroy => "willDestroy",
254            Self::InvalidProperties => "invalidProperties",
255            Self::Singleton => "singleton",
256            Self::AlreadyExists => "alreadyExists",
257            Self::MailboxHasChild => "mailboxHasChild",
258            Self::MailboxHasEmail => "mailboxHasEmail",
259            Self::TooManyKeywords => "tooManyKeywords",
260            Self::TooManyMailboxes => "tooManyMailboxes",
261            Self::BlobNotFound => "blobNotFound",
262            Self::ForbiddenFrom => "forbiddenFrom",
263            Self::InvalidEmail => "invalidEmail",
264            Self::TooManyRecipients => "tooManyRecipients",
265            Self::NoRecipients => "noRecipients",
266            Self::InvalidRecipients => "invalidRecipients",
267            Self::ForbiddenMailFrom => "forbiddenMailFrom",
268            Self::ForbiddenToSend => "forbiddenToSend",
269            Self::CannotUnsend => "cannotUnsend",
270            Self::Custom(s) => s.as_str(),
271        };
272        f.write_str(s)
273    }
274}
275
276impl serde::Serialize for SetErrorType {
277    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
278        s.serialize_str(&self.to_string())
279    }
280}
281
282impl<'de> serde::Deserialize<'de> for SetErrorType {
283    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
284        struct Visitor;
285        impl serde::de::Visitor<'_> for Visitor {
286            type Value = SetErrorType;
287            fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288                f.write_str("a JMAP SetError type string")
289            }
290            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
291                Ok(match v {
292                    "forbidden" => SetErrorType::Forbidden,
293                    "overQuota" => SetErrorType::OverQuota,
294                    "tooLarge" => SetErrorType::TooLarge,
295                    "rateLimit" => SetErrorType::RateLimit,
296                    "notFound" => SetErrorType::NotFound,
297                    "invalidPatch" => SetErrorType::InvalidPatch,
298                    "willDestroy" => SetErrorType::WillDestroy,
299                    "invalidProperties" => SetErrorType::InvalidProperties,
300                    "singleton" => SetErrorType::Singleton,
301                    "alreadyExists" => SetErrorType::AlreadyExists,
302                    "mailboxHasChild" => SetErrorType::MailboxHasChild,
303                    "mailboxHasEmail" => SetErrorType::MailboxHasEmail,
304                    "tooManyKeywords" => SetErrorType::TooManyKeywords,
305                    "tooManyMailboxes" => SetErrorType::TooManyMailboxes,
306                    "blobNotFound" => SetErrorType::BlobNotFound,
307                    "forbiddenFrom" => SetErrorType::ForbiddenFrom,
308                    "invalidEmail" => SetErrorType::InvalidEmail,
309                    "tooManyRecipients" => SetErrorType::TooManyRecipients,
310                    "noRecipients" => SetErrorType::NoRecipients,
311                    "invalidRecipients" => SetErrorType::InvalidRecipients,
312                    "forbiddenMailFrom" => SetErrorType::ForbiddenMailFrom,
313                    "forbiddenToSend" => SetErrorType::ForbiddenToSend,
314                    "cannotUnsend" => SetErrorType::CannotUnsend,
315                    other => SetErrorType::Custom(other.to_owned()),
316                })
317            }
318        }
319        d.deserialize_str(Visitor)
320    }
321}
322
323/// Error type returned by create/update/destroy backend methods.
324#[non_exhaustive]
325#[derive(Debug)]
326pub enum BackendSetError<E> {
327    /// A well-typed JMAP [`SetError`] to place verbatim in the
328    /// `notCreated`/`notUpdated`/`notDestroyed` map.
329    SetError(SetError),
330    /// An unexpected storage-layer error.
331    Other(E),
332}
333
334impl<E: std::fmt::Display> std::fmt::Display for BackendSetError<E> {
335    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336        match self {
337            Self::SetError(se) => write!(f, "set error: {se}"),
338            Self::Other(e) => write!(f, "{e}"),
339        }
340    }
341}
342
343impl<E: std::error::Error + 'static> std::error::Error for BackendSetError<E> {
344    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
345        match self {
346            Self::Other(e) => Some(e),
347            _ => None,
348        }
349    }
350}
351
352impl<E> From<SetError> for BackendSetError<E> {
353    fn from(e: SetError) -> Self {
354        Self::SetError(e)
355    }
356}
357
358// ---------------------------------------------------------------------------
359// Backend error envelopes
360// ---------------------------------------------------------------------------
361
362/// Error type returned by [`JmapBackend::get_changes`] and
363/// [`JmapBackend::query_changes`].
364#[non_exhaustive]
365#[derive(Debug)]
366pub enum BackendChangesError<E> {
367    /// The server cannot supply incremental changes for the given `sinceState`.
368    ///
369    /// Two sub-cases share this variant:
370    ///
371    /// - **`limit > 0`** — maps to `tooManyChanges` in the JMAP response, with
372    ///   `limit` as the suggested maximum. The client may retry with a smaller
373    ///   window.
374    ///
375    /// - **`limit: 0`** — maps to `cannotCalculateChanges`. This signals a
376    ///   **full state reset**: the client MUST discard ALL locally cached
377    ///   objects for the affected type, reset its local state token to the
378    ///   empty string, and perform a full resync (`/get` with `ids: null`).
379    ///   Partial recovery is not permitted — the server has no usable
380    ///   change log for this state window. (Source: RFC 8620 §5.6; authoritative
381    ///   behavior documented in jmapio/jmap-js `mail-model.js`.)
382    TooManyChanges {
383        /// Maximum window size the server can supply in a single
384        /// `/changes` response. A value of `0` signals a full state
385        /// reset is required per RFC 8620 §5.6; any non-zero value is
386        /// the suggested maximum the client may retry with.
387        limit: u64,
388    },
389    /// An unexpected storage-layer error.
390    Other(E),
391}
392
393impl<E: std::fmt::Display> std::fmt::Display for BackendChangesError<E> {
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        match self {
396            Self::TooManyChanges { limit: 0 } => write!(f, "cannot calculate changes"),
397            Self::TooManyChanges { limit } => write!(f, "too many changes (limit: {limit})"),
398            Self::Other(e) => write!(f, "{e}"),
399        }
400    }
401}
402
403impl<E: std::error::Error + 'static> std::error::Error for BackendChangesError<E> {
404    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
405        match self {
406            Self::Other(e) => Some(e),
407            _ => None,
408        }
409    }
410}
411
412impl<E> From<E> for BackendChangesError<E> {
413    fn from(e: E) -> Self {
414        Self::Other(e)
415    }
416}
417
418impl<E: std::error::Error> From<BackendChangesError<E>> for jmap_types::JmapError {
419    fn from(e: BackendChangesError<E>) -> Self {
420        match e {
421            BackendChangesError::TooManyChanges { limit: 0 } => {
422                jmap_types::JmapError::cannot_calculate_changes()
423            }
424            BackendChangesError::TooManyChanges { limit } => {
425                jmap_types::JmapError::too_many_changes_with_limit(limit)
426            }
427            BackendChangesError::Other(inner) => {
428                jmap_types::JmapError::server_fail(inner.to_string())
429            }
430        }
431    }
432}
433
434// ---------------------------------------------------------------------------
435// Result types
436// ---------------------------------------------------------------------------
437
438/// Result of a `/changes` call (RFC 8620 §5.2).
439#[derive(Debug)]
440#[non_exhaustive]
441pub struct ChangesResult {
442    /// Ids of objects that were created since `sinceState`.
443    pub created: Vec<jmap_types::Id>,
444    /// Ids of objects that were updated since `sinceState`.
445    pub updated: Vec<jmap_types::Id>,
446    /// Ids of objects that were destroyed since `sinceState`.
447    pub destroyed: Vec<jmap_types::Id>,
448    /// `true` if there are more changes beyond this batch.
449    pub has_more_changes: bool,
450    /// The current state token after applying all reported changes.
451    pub new_state: jmap_types::State,
452}
453
454impl ChangesResult {
455    /// Construct a [`ChangesResult`].
456    pub fn new(
457        created: Vec<jmap_types::Id>,
458        updated: Vec<jmap_types::Id>,
459        destroyed: Vec<jmap_types::Id>,
460        has_more_changes: bool,
461        new_state: jmap_types::State,
462    ) -> Self {
463        Self {
464            created,
465            updated,
466            destroyed,
467            has_more_changes,
468            new_state,
469        }
470    }
471}
472
473/// Result of a `/query` call (RFC 8620 §5.5).
474#[derive(Debug)]
475#[non_exhaustive]
476pub struct QueryResult {
477    /// The ordered list of matching object ids.
478    pub ids: Vec<jmap_types::Id>,
479    /// The 0-based index of the first returned id in the complete result list.
480    pub position: i64,
481    /// Total number of results, if the backend can calculate it.
482    pub total: Option<u64>,
483    /// Opaque query state token for subsequent `/queryChanges` calls.
484    pub query_state: jmap_types::State,
485    /// Whether the backend supports `/queryChanges` for this query.
486    pub can_calculate_changes: bool,
487}
488
489impl QueryResult {
490    /// Construct a [`QueryResult`].
491    pub fn new(
492        ids: Vec<jmap_types::Id>,
493        position: i64,
494        total: Option<u64>,
495        query_state: jmap_types::State,
496        can_calculate_changes: bool,
497    ) -> Self {
498        Self {
499            ids,
500            position,
501            total,
502            query_state,
503            can_calculate_changes,
504        }
505    }
506}
507
508/// One entry in the `added` list of a `/queryChanges` response (RFC 8620 §5.6).
509#[derive(Debug)]
510#[non_exhaustive]
511pub struct AddedItem {
512    /// The id of the newly-added object.
513    pub id: jmap_types::Id,
514    /// Its 0-based position in the result list after applying all changes.
515    pub index: u64,
516}
517
518impl AddedItem {
519    /// Construct an [`AddedItem`].
520    pub fn new(id: jmap_types::Id, index: u64) -> Self {
521        Self { id, index }
522    }
523}
524
525/// Result of a `/queryChanges` call (RFC 8620 §5.6).
526#[derive(Debug)]
527#[non_exhaustive]
528pub struct QueryChangesResult {
529    /// The query state token supplied by the client in `sinceQueryState`.
530    pub old_query_state: jmap_types::State,
531    /// The current query state token.
532    pub new_query_state: jmap_types::State,
533    /// Total number of results in the new query, if the backend can calculate it.
534    pub total: Option<u64>,
535    /// Ids removed from the result set since `oldQueryState`.
536    pub removed: Vec<jmap_types::Id>,
537    /// Ids added to the result set since `oldQueryState`, with their positions.
538    pub added: Vec<AddedItem>,
539}
540
541impl QueryChangesResult {
542    /// Construct a [`QueryChangesResult`].
543    pub fn new(
544        old_query_state: jmap_types::State,
545        new_query_state: jmap_types::State,
546        total: Option<u64>,
547        removed: Vec<jmap_types::Id>,
548        added: Vec<AddedItem>,
549    ) -> Self {
550        Self {
551            old_query_state,
552            new_query_state,
553            total,
554            removed,
555            added,
556        }
557    }
558}
559
560// ---------------------------------------------------------------------------
561// JmapBackend — the read-side supertrait
562// ---------------------------------------------------------------------------
563
564/// Read-side backend supertrait shared by all JMAP server crates.
565///
566/// Domain-specific backend traits (`MailBackend`, `ChatBackend`, etc.) require
567/// this trait as a supertrait and add write-side methods on top.
568///
569/// Only the read operations that have an identical signature across all JMAP
570/// object types belong here. Write operations (`create_object`, `update_object`,
571/// `destroy_object`) and domain-specific operations remain in the domain crate.
572///
573/// The `collapse_threads` parameter on `query_changes` is included for
574/// `Email/queryChanges` (RFC 8621 §4.5). Non-mail backends should pass `false`
575/// and may ignore the parameter.
576///
577/// This trait is not object-safe by design (generic methods). Use
578/// `Arc<impl JmapBackend>` when sharing across tasks.
579///
580/// # CallerCtx
581///
582/// Every backend method takes a `caller: &Self::CallerCtx` parameter as the
583/// first argument after `&self`. This is the per-request authentication /
584/// authorisation context produced by the caller's auth layer and forwarded
585/// unchanged through [`crate::Dispatcher::dispatch`] → [`crate::JmapHandler`]
586/// → the registered closure → the backend.
587///
588/// Implementations that do not need an auth identity can use the unit type:
589///
590/// ```rust,ignore
591/// impl JmapBackend for MyBackend {
592///     type Error = MyError;
593///     type CallerCtx = ();
594///     // ...
595/// }
596/// ```
597///
598/// Implementations that do need to differentiate behaviour per caller (e.g.
599/// applying per-user visibility rules, or rejecting reads with
600/// `forbidden` when the caller is not the owner of the account) read the
601/// `caller` parameter to decide.
602///
603/// The trait bound `Clone + Send + 'static` is what [`crate::Dispatcher`]
604/// requires; the bound is repeated here so the supertrait can stand on its
605/// own without depending on the dispatcher.
606pub trait JmapBackend: Send + Sync + 'static {
607    /// The error type returned by storage operations.
608    ///
609    /// # Security
610    ///
611    /// The `Display` impl of this type is surfaced through
612    /// [`BackendSetError::Other`]'s and [`BackendChangesError::Other`]'s
613    /// own `Display` impls, which in turn flow into
614    /// [`crate::request_error`]'s `RequestError::Display` output. When a
615    /// downstream consumer wires tracing-style logging on top, the
616    /// formatted error text lands in operator logs verbatim.
617    ///
618    /// Implementations MUST NOT include any of the following in this
619    /// type's `Display` output:
620    ///
621    /// - **Credential material** — auth tokens, passwords, push
622    ///   verification codes, invite codes, session cookies, or anything
623    ///   derived byte-for-byte from an `Authorization`-header value.
624    /// - **Blob content** — email bodies, sieve scripts, file contents,
625    ///   or any user-supplied opaque payload. An error like
626    ///   `"sieve parse error at line 42: <script excerpt>"` violates
627    ///   this — emit the line number and a short type-only summary
628    ///   ("sieve parse error at line 42: unexpected token") and let the
629    ///   server log the full script body separately under a redacted
630    ///   path.
631    /// - **PII shaped like an email address** in any code path that an
632    ///   unauthenticated caller can trigger. Wrapping a downstream
633    ///   service error that interpolates the caller's email is the
634    ///   common foot-gun.
635    ///
636    /// Errors that wrap a downstream-service failure should sanitize
637    /// the downstream error text — or strip it entirely and replace it
638    /// with a static summary — before constructing the `Display`
639    /// string. The same rule applies to every extension `*Backend`
640    /// trait that inherits this associated type by transitivity:
641    /// `MailBackend::Error`, `ChatBackend::Error`,
642    /// `CalendarsBackend::Error`, `TasksBackend::Error`,
643    /// `ContactsBackend::Error`, `FileNodeBackend::Error`, and
644    /// `SharingBackend::Error` are all the same `JmapBackend::Error`
645    /// associated type — the contract here governs all of them.
646    ///
647    /// Precedent: bd:JMAP-sc1b.79 redacted `BearerAuth` and `BasicAuth`
648    /// at the type-derive level; bd:JMAP-sc1b.100 documents the
649    /// equivalent contract at the trait-associated-type level.
650    type Error: std::error::Error + Send + Sync + 'static;
651
652    /// The per-request caller context type produced by the auth layer and
653    /// forwarded by [`crate::Dispatcher::dispatch`] into every method call.
654    ///
655    /// Use `()` when no auth context is needed.
656    ///
657    /// The bound is `Clone + Send + Sync + 'static`:
658    /// - `Clone` because [`crate::Dispatcher`] clones the value once per
659    ///   method call in the batch.
660    /// - `Send + 'static` because each method call is spawned on a
661    ///   [`tokio::task`].
662    /// - `Sync` because handler method bodies take `&Self::CallerCtx`
663    ///   and hold that reference across `.await` boundaries inside a
664    ///   `Send` future (a `&T` is `Send` iff `T: Sync`).
665    type CallerCtx: Clone + Send + Sync + 'static;
666
667    /// Return `true` if the given account exists in this backend.
668    ///
669    /// Handlers call this at the start of each method to return
670    /// `accountNotFound` (RFC 8620 §3.6.2) rather than surfacing
671    /// the wrong error when `accountId` is unknown.
672    fn account_exists(
673        &self,
674        caller: &Self::CallerCtx,
675        account_id: &jmap_types::Id,
676    ) -> impl std::future::Future<Output = Result<bool, Self::Error>> + Send;
677
678    /// Fetch objects by id (or all objects when `ids` is `None`).
679    ///
680    /// `properties` is the list of property names requested by the client
681    /// (RFC 8620 §5.1). `None` means the client did not send a `properties`
682    /// field; the backend should return all properties. When `Some`, the backend
683    /// MAY filter the response to only the named properties, but is not required
684    /// to — implementations that always return all properties are correct.
685    ///
686    /// Returns `(found, not_found)` — objects that exist and ids that do not.
687    fn get_objects<O: GetObject + Send + Sync>(
688        &self,
689        caller: &Self::CallerCtx,
690        account_id: &jmap_types::Id,
691        ids: Option<&[jmap_types::Id]>,
692        properties: Option<&[String]>,
693    ) -> impl std::future::Future<Output = Result<(Vec<O>, Vec<jmap_types::Id>), Self::Error>> + Send;
694
695    /// Return the current state token for an object type in the given account.
696    fn get_state<O: JmapObject + Send + Sync>(
697        &self,
698        caller: &Self::CallerCtx,
699        account_id: &jmap_types::Id,
700    ) -> impl std::future::Future<Output = Result<jmap_types::State, Self::Error>> + Send;
701
702    /// Return changes since `since_state`, up to `max_changes` entries.
703    fn get_changes<O: JmapObject + Send + Sync>(
704        &self,
705        caller: &Self::CallerCtx,
706        account_id: &jmap_types::Id,
707        since_state: &jmap_types::State,
708        max_changes: Option<u64>,
709    ) -> impl std::future::Future<Output = Result<ChangesResult, BackendChangesError<Self::Error>>> + Send;
710
711    /// Execute a `/query` and return a page of matching ids.
712    ///
713    /// `position` may be negative — negative values are relative to the end of
714    /// the result set per RFC 8620 §5.5 (e.g. -1 means the last result).
715    ///
716    /// # Filter and sort handling
717    ///
718    /// Implementations MUST honour the supplied `filter` and `sort` arguments
719    /// efficiently — typically by pushing both into the indexed storage layer
720    /// (database WHERE / ORDER BY, search index, etc.). Returning every
721    /// matching id and relying on the caller to paginate after the fact
722    /// degenerates to O(n) per page for IMAP-migration accounts.
723    ///
724    /// Handler implementations in `jmap-*-server` crates SHOULD NOT
725    /// post-filter or post-sort the backend's result; doing so re-introduces
726    /// the O(n) cost this method exists to avoid. The Mailbox handler in
727    /// `jmap-mail-server` is the canonical example of pushing filter/sort
728    /// fully into the backend.
729    #[allow(clippy::too_many_arguments)]
730    fn query_objects<O: QueryObject + Send + Sync>(
731        &self,
732        caller: &Self::CallerCtx,
733        account_id: &jmap_types::Id,
734        filter: Option<&O::Filter>,
735        sort: Option<&[O::Comparator]>,
736        limit: Option<u64>,
737        position: i64,
738    ) -> impl std::future::Future<Output = Result<QueryResult, Self::Error>> + Send;
739
740    /// Execute a `/queryChanges` and return deltas since `since_query_state`.
741    ///
742    /// `collapse_threads` is only meaningful for `Email/queryChanges`
743    /// (RFC 8621 §4.5). Pass `false` for all other object types.
744    #[allow(clippy::too_many_arguments)]
745    fn query_changes<O: QueryObject + Send + Sync>(
746        &self,
747        caller: &Self::CallerCtx,
748        account_id: &jmap_types::Id,
749        since_query_state: &jmap_types::State,
750        filter: Option<&O::Filter>,
751        sort: Option<&[O::Comparator]>,
752        max_changes: Option<u64>,
753        up_to_id: Option<&jmap_types::Id>,
754        collapse_threads: bool,
755    ) -> impl std::future::Future<
756        Output = Result<QueryChangesResult, BackendChangesError<Self::Error>>,
757    > + Send;
758
759    /// The caller's stable identity within this account namespace.
760    ///
761    /// Returns `None` for deployments that have not wired identity
762    /// (test fixtures, single-user dev servers). A `None`-returning
763    /// backend CANNOT honor JMAP semantics that depend on caller
764    /// identity — chat role-hierarchy, calendar ACLs, sharing/myRights,
765    /// per-user $seen on shared mailboxes, metadata isPrivate
766    /// visibility scoping, etc. Authentication is still the HTTP
767    /// layer's job; this method exposes the result of that
768    /// authentication to the JMAP layer for in-method semantics.
769    ///
770    /// Implementations MUST NOT mint identity — they MUST read it
771    /// from the `CallerCtx` populated by the HTTP/auth middleware
772    /// before `dispatch()` was called.
773    ///
774    /// Backends that honor identity-dependent semantics MUST override
775    /// this method. Handlers and downstream backend traits MAY rely on
776    /// it being correct when it returns `Some`.
777    fn principal_id(caller: &Self::CallerCtx) -> Option<&jmap_types::Id> {
778        let _ = caller;
779        None
780    }
781}
782
783// ---------------------------------------------------------------------------
784// Tests
785// ---------------------------------------------------------------------------
786
787#[cfg(test)]
788mod tests {
789    use super::*;
790
791    /// Oracle: BackendChangesError::TooManyChanges { limit: 0 } must map to
792    /// cannotCalculateChanges (RFC 8620 §5.6), not tooManyChanges with limit 0.
793    ///
794    /// limit=0 is the convention for "cannot calculate".
795    #[test]
796    fn backend_changes_error_limit_zero_maps_to_cannot_calculate() {
797        let err = jmap_types::JmapError::from(
798            BackendChangesError::<std::convert::Infallible>::TooManyChanges { limit: 0 },
799        );
800        assert_eq!(
801            err.error_type.as_str(),
802            "cannotCalculateChanges",
803            "limit=0 must produce cannotCalculateChanges; got: {:?}",
804            err.error_type
805        );
806    }
807
808    /// Oracle: BackendChangesError::TooManyChanges { limit: N } (N > 0) maps to
809    /// tooManyChanges with the suggested limit.
810    #[test]
811    fn backend_changes_error_nonzero_limit_maps_to_too_many_changes() {
812        let err = jmap_types::JmapError::from(
813            BackendChangesError::<std::convert::Infallible>::TooManyChanges { limit: 50 },
814        );
815        assert_eq!(
816            err.error_type.as_str(),
817            "tooManyChanges",
818            "limit=50 must produce tooManyChanges; got: {:?}",
819            err.error_type
820        );
821    }
822
823    /// Oracle: SetErrorType::Custom("mdnAlreadySent") must serialize as the bare
824    /// string "mdnAlreadySent" and deserialize back to Custom("mdnAlreadySent").
825    /// Extension crates depend on this round-trip to emit domain-specific errors.
826    #[test]
827    fn set_error_type_custom_round_trips_as_bare_string() {
828        let original = SetErrorType::custom("mdnAlreadySent");
829        let serialized = serde_json::to_string(&original).expect("serialize");
830        assert_eq!(
831            serialized, r#""mdnAlreadySent""#,
832            "Custom must serialize as bare string"
833        );
834        let deserialized: SetErrorType = serde_json::from_str(&serialized).expect("deserialize");
835        assert_eq!(
836            deserialized, original,
837            "Custom must deserialize back to Custom"
838        );
839    }
840
841    /// Oracle (bd:JMAP-dha0): SetError gains an `extra` map that captures
842    /// extension-defined fields not covered by the typed `with_*` builders.
843    /// A handler that emits `rateLimited` with `serverRetryAfter` must
844    /// see the value round-trip through serialize / deserialize.
845    #[test]
846    fn set_error_extra_field_round_trips() {
847        let original = SetError::new(SetErrorType::custom("rateLimited"))
848            .with_description("Slow mode is active")
849            .with_extra(
850                "serverRetryAfter",
851                serde_json::Value::String("2025-12-31T23:59:59Z".to_owned()),
852            );
853
854        let wire = serde_json::to_value(&original).expect("serialize");
855        assert_eq!(wire["type"], "rateLimited");
856        assert_eq!(wire["description"], "Slow mode is active");
857        assert_eq!(
858            wire["serverRetryAfter"], "2025-12-31T23:59:59Z",
859            "extra field must flatten into the SetError wire shape"
860        );
861
862        let round: SetError = serde_json::from_value(wire).expect("deserialize");
863        assert_eq!(round.error_type, original.error_type);
864        assert_eq!(round.description, original.description);
865        assert_eq!(
866            round.extra.get("serverRetryAfter").and_then(|v| v.as_str()),
867            Some("2025-12-31T23:59:59Z"),
868            "extra field must survive deserialize"
869        );
870    }
871
872    /// Oracle (bd:JMAP-dha0): a SetError with no extras serializes to a
873    /// wire shape byte-identical to the pre-extras layout. The
874    /// `skip_serializing_if` on `extra` collapses the empty map.
875    #[test]
876    fn set_error_empty_extra_is_invisible_on_the_wire() {
877        let err = SetError::new(SetErrorType::Forbidden);
878        let wire = serde_json::to_value(&err).expect("serialize");
879        let obj = wire.as_object().expect("object");
880        assert!(
881            !obj.contains_key("extra"),
882            "empty `extra` map must not appear on the wire (got {wire})"
883        );
884        // The only key on the wire for a bare SetError must be `type`.
885        assert_eq!(
886            obj.len(),
887            1,
888            "bare SetError must have exactly one key on the wire"
889        );
890        assert_eq!(obj["type"], "forbidden");
891    }
892
893    /// Oracle (bd:JMAP-dha0): unknown wire fields on a deserialized
894    /// SetError land in `extra`. This means a future spec adding
895    /// `someNewSetErrorField` will round-trip through current
896    /// versions of the kit losslessly.
897    #[test]
898    fn set_error_unknown_field_lands_in_extra() {
899        let wire = serde_json::json!({
900            "type": "forbidden",
901            "futureSpecField": "future-value",
902            "anotherOne": 42
903        });
904        let err: SetError = serde_json::from_value(wire).expect("deserialize");
905        assert_eq!(err.error_type, SetErrorType::Forbidden);
906        assert_eq!(
907            err.extra.get("futureSpecField").and_then(|v| v.as_str()),
908            Some("future-value")
909        );
910        assert_eq!(
911            err.extra.get("anotherOne").and_then(|v| v.as_u64()),
912            Some(42)
913        );
914    }
915
916    /// Oracle: known SetErrorType variants (e.g. Singleton) must still
917    /// serialize as their camelCase wire strings and deserialize back correctly.
918    #[test]
919    fn set_error_type_known_variant_round_trips() {
920        let original = SetErrorType::Singleton;
921        let serialized = serde_json::to_string(&original).expect("serialize");
922        assert_eq!(
923            serialized, r#""singleton""#,
924            "Singleton must serialize as \"singleton\""
925        );
926        let deserialized: SetErrorType = serde_json::from_str(&serialized).expect("deserialize");
927        assert_eq!(
928            deserialized, original,
929            "Singleton must deserialize back to Singleton"
930        );
931    }
932
933    /// Oracle (bd:JMAP-ga0q.1): `JmapBackend::principal_id` has a default impl
934    /// that returns `None`. A backend whose `CallerCtx = ()` and that does NOT
935    /// override `principal_id` inherits that default and signals "identity not
936    /// wired" to callers. JMAP semantics that depend on caller identity must
937    /// treat `None` as a hard "cannot honor".
938    #[test]
939    fn principal_id_default_impl_returns_none_for_unit_caller_ctx() {
940        // Minimal stub backend exercising only the default impl. All other
941        // trait methods are stubbed with `unreachable!()` and never invoked.
942        struct StubBackend;
943
944        #[derive(Debug)]
945        struct StubError;
946
947        impl std::fmt::Display for StubError {
948            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
949                f.write_str("stub")
950            }
951        }
952        impl std::error::Error for StubError {}
953
954        impl JmapBackend for StubBackend {
955            type Error = StubError;
956            type CallerCtx = ();
957
958            async fn account_exists(
959                &self,
960                _caller: &(),
961                _account_id: &jmap_types::Id,
962            ) -> Result<bool, Self::Error> {
963                unreachable!("only principal_id is exercised in this test")
964            }
965
966            async fn get_objects<O: GetObject + Send + Sync>(
967                &self,
968                _caller: &(),
969                _account_id: &jmap_types::Id,
970                _ids: Option<&[jmap_types::Id]>,
971                _properties: Option<&[String]>,
972            ) -> Result<(Vec<O>, Vec<jmap_types::Id>), Self::Error> {
973                unreachable!("only principal_id is exercised in this test")
974            }
975
976            async fn get_state<O: JmapObject + Send + Sync>(
977                &self,
978                _caller: &(),
979                _account_id: &jmap_types::Id,
980            ) -> Result<jmap_types::State, Self::Error> {
981                unreachable!("only principal_id is exercised in this test")
982            }
983
984            async fn get_changes<O: JmapObject + Send + Sync>(
985                &self,
986                _caller: &(),
987                _account_id: &jmap_types::Id,
988                _since_state: &jmap_types::State,
989                _max_changes: Option<u64>,
990            ) -> Result<ChangesResult, BackendChangesError<Self::Error>> {
991                unreachable!("only principal_id is exercised in this test")
992            }
993
994            async fn query_objects<O: QueryObject + Send + Sync>(
995                &self,
996                _caller: &(),
997                _account_id: &jmap_types::Id,
998                _filter: Option<&O::Filter>,
999                _sort: Option<&[O::Comparator]>,
1000                _limit: Option<u64>,
1001                _position: i64,
1002            ) -> Result<QueryResult, Self::Error> {
1003                unreachable!("only principal_id is exercised in this test")
1004            }
1005
1006            async fn query_changes<O: QueryObject + Send + Sync>(
1007                &self,
1008                _caller: &(),
1009                _account_id: &jmap_types::Id,
1010                _since_query_state: &jmap_types::State,
1011                _filter: Option<&O::Filter>,
1012                _sort: Option<&[O::Comparator]>,
1013                _max_changes: Option<u64>,
1014                _up_to_id: Option<&jmap_types::Id>,
1015                _collapse_threads: bool,
1016            ) -> Result<QueryChangesResult, BackendChangesError<Self::Error>> {
1017                unreachable!("only principal_id is exercised in this test")
1018            }
1019        }
1020
1021        let caller: <StubBackend as JmapBackend>::CallerCtx = ();
1022        let id = <StubBackend as JmapBackend>::principal_id(&caller);
1023        assert!(
1024            id.is_none(),
1025            "default principal_id impl must return None; got Some({:?})",
1026            id
1027        );
1028    }
1029}