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}
46
47impl SetError {
48    /// Construct a [`SetError`] with the given type and all optional fields `None`.
49    pub fn new(error_type: SetErrorType) -> Self {
50        Self {
51            error_type,
52            description: None,
53            properties: None,
54            existing_id: None,
55            max_recipients: None,
56            invalid_recipients: None,
57            not_found: None,
58            max_size: None,
59        }
60    }
61
62    /// Set the human-readable description.
63    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
64        self.description = Some(desc.into());
65        self
66    }
67
68    /// Set the list of property names that caused the error.
69    pub fn with_properties<I, S>(mut self, props: I) -> Self
70    where
71        I: IntoIterator<Item = S>,
72        S: Into<String>,
73    {
74        self.properties = Some(props.into_iter().map(|s| s.into()).collect());
75        self
76    }
77
78    /// Set the existing object id (used with `alreadyExists`).
79    pub fn with_existing_id(mut self, id: jmap_types::Id) -> Self {
80        self.existing_id = Some(id);
81        self
82    }
83
84    /// Set the maximum recipients (used with `tooManyRecipients` — RFC 8621 §7.5).
85    pub fn with_max_recipients(mut self, n: u64) -> Self {
86        self.max_recipients = Some(n);
87        self
88    }
89
90    /// Set the invalid recipient addresses (used with `invalidRecipients` — RFC 8621 §7.5).
91    pub fn with_invalid_recipients<I, S>(mut self, addrs: I) -> Self
92    where
93        I: IntoIterator<Item = S>,
94        S: Into<String>,
95    {
96        self.invalid_recipients = Some(addrs.into_iter().map(|s| s.into()).collect());
97        self
98    }
99
100    /// Set the missing blob IDs (used with `blobNotFound` — RFC 8621 §5.5).
101    pub fn with_not_found(mut self, ids: Vec<jmap_types::Id>) -> Self {
102        self.not_found = Some(ids);
103        self
104    }
105
106    /// Set the maximum message size in octets (used with `tooLarge` on EmailSubmission — RFC 8621 §7.5).
107    pub fn with_max_size(mut self, n: u64) -> Self {
108        self.max_size = Some(n);
109        self
110    }
111}
112
113impl std::fmt::Display for SetError {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        write!(f, "{}", self.error_type)?;
116        if let Some(ref desc) = self.description {
117            write!(f, ": {desc}")?;
118        }
119        Ok(())
120    }
121}
122
123/// The machine-readable type for a [`SetError`] (RFC 8620 §5.3 and RFC 8621).
124#[non_exhaustive]
125#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub enum SetErrorType {
128    /// The action would violate an ACL or other access control policy.
129    Forbidden,
130    /// Creating or modifying the object would exceed a server quota.
131    OverQuota,
132    /// The object is too large to be stored by the server.
133    TooLarge,
134    /// The server is rate-limiting this client.
135    RateLimit,
136    /// The object to be updated or destroyed does not exist.
137    NotFound,
138    /// The patch object is not a valid JSON Merge Patch or cannot be applied.
139    InvalidPatch,
140    /// The client requested destruction of an object that will be destroyed
141    /// implicitly when another object is destroyed.
142    WillDestroy,
143    /// One or more properties have invalid values.
144    InvalidProperties,
145    /// The object type is a singleton and cannot be created or destroyed.
146    Singleton,
147    /// An object with the same unique key already exists.
148    AlreadyExists,
149    /// RFC 8621 §2.5 — Mailbox has child mailboxes and cannot be destroyed.
150    MailboxHasChild,
151    /// RFC 8621 §2.5 — Mailbox contains emails and `onDestroyRemoveEmails` is false.
152    MailboxHasEmail,
153    /// RFC 8621 §5.5 — Too many keywords on the Email.
154    TooManyKeywords,
155    /// RFC 8621 §5.5 — Email is in too many mailboxes.
156    TooManyMailboxes,
157    /// RFC 8621 §5.5 — A referenced blob was not found.
158    BlobNotFound,
159    /// RFC 8621 §6.3 — The `from` address is not permitted for this Identity.
160    ForbiddenFrom,
161    /// RFC 8621 §7.5 — The Email is invalid for submission.
162    InvalidEmail,
163    /// RFC 8621 §7.5 — Too many recipients.
164    TooManyRecipients,
165    /// RFC 8621 §7.5 — No recipients specified.
166    NoRecipients,
167    /// RFC 8621 §7.5 — One or more recipient addresses are invalid.
168    InvalidRecipients,
169    /// RFC 8621 §7.5 — The MAIL FROM address is not permitted.
170    ForbiddenMailFrom,
171    /// RFC 8621 §7.5 — The user does not have send permission.
172    ForbiddenToSend,
173    /// RFC 8621 §7.5 — The submission cannot be undone.
174    CannotUnsend,
175}
176
177impl std::fmt::Display for SetErrorType {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        let s = match self {
180            Self::Forbidden => "forbidden",
181            Self::OverQuota => "overQuota",
182            Self::TooLarge => "tooLarge",
183            Self::RateLimit => "rateLimit",
184            Self::NotFound => "notFound",
185            Self::InvalidPatch => "invalidPatch",
186            Self::WillDestroy => "willDestroy",
187            Self::InvalidProperties => "invalidProperties",
188            Self::Singleton => "singleton",
189            Self::AlreadyExists => "alreadyExists",
190            Self::MailboxHasChild => "mailboxHasChild",
191            Self::MailboxHasEmail => "mailboxHasEmail",
192            Self::TooManyKeywords => "tooManyKeywords",
193            Self::TooManyMailboxes => "tooManyMailboxes",
194            Self::BlobNotFound => "blobNotFound",
195            Self::ForbiddenFrom => "forbiddenFrom",
196            Self::InvalidEmail => "invalidEmail",
197            Self::TooManyRecipients => "tooManyRecipients",
198            Self::NoRecipients => "noRecipients",
199            Self::InvalidRecipients => "invalidRecipients",
200            Self::ForbiddenMailFrom => "forbiddenMailFrom",
201            Self::ForbiddenToSend => "forbiddenToSend",
202            Self::CannotUnsend => "cannotUnsend",
203        };
204        f.write_str(s)
205    }
206}
207
208/// Error type returned by create/update/destroy backend methods.
209#[derive(Debug)]
210pub enum BackendSetError<E> {
211    /// A well-typed JMAP [`SetError`] to place verbatim in the
212    /// `notCreated`/`notUpdated`/`notDestroyed` map.
213    SetError(SetError),
214    /// An unexpected storage-layer error.
215    Other(E),
216}
217
218impl<E: std::fmt::Display> std::fmt::Display for BackendSetError<E> {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        match self {
221            Self::SetError(se) => write!(f, "set error: {se}"),
222            Self::Other(e) => write!(f, "{e}"),
223        }
224    }
225}
226
227impl<E: std::error::Error + 'static> std::error::Error for BackendSetError<E> {
228    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
229        match self {
230            Self::Other(e) => Some(e),
231            _ => None,
232        }
233    }
234}
235
236impl<E> From<SetError> for BackendSetError<E> {
237    fn from(e: SetError) -> Self {
238        Self::SetError(e)
239    }
240}
241
242// ---------------------------------------------------------------------------
243// Backend error envelopes
244// ---------------------------------------------------------------------------
245
246/// Error type returned by [`JmapBackend::get_changes`] and
247/// [`JmapBackend::query_changes`].
248#[non_exhaustive]
249#[derive(Debug)]
250pub enum BackendChangesError<E> {
251    /// The `sinceState` is too old or the server cannot calculate the full set
252    /// of intermediate states. Maps to `tooManyChanges` in the response with
253    /// the given suggested limit. Use `limit: 0` for `cannotCalculateChanges`.
254    TooManyChanges { limit: u64 },
255    /// An unexpected storage-layer error.
256    Other(E),
257}
258
259impl<E: std::fmt::Display> std::fmt::Display for BackendChangesError<E> {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        match self {
262            Self::TooManyChanges { limit: 0 } => write!(f, "cannot calculate changes"),
263            Self::TooManyChanges { limit } => write!(f, "too many changes (limit: {limit})"),
264            Self::Other(e) => write!(f, "{e}"),
265        }
266    }
267}
268
269impl<E: std::error::Error + 'static> std::error::Error for BackendChangesError<E> {
270    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
271        match self {
272            Self::Other(e) => Some(e),
273            _ => None,
274        }
275    }
276}
277
278impl<E> From<E> for BackendChangesError<E> {
279    fn from(e: E) -> Self {
280        Self::Other(e)
281    }
282}
283
284impl<E: std::error::Error> From<BackendChangesError<E>> for jmap_types::JmapError {
285    fn from(e: BackendChangesError<E>) -> Self {
286        match e {
287            BackendChangesError::TooManyChanges { limit: 0 } => {
288                jmap_types::JmapError::cannot_calculate_changes()
289            }
290            BackendChangesError::TooManyChanges { limit } => {
291                jmap_types::JmapError::too_many_changes_with_limit(limit)
292            }
293            BackendChangesError::Other(inner) => {
294                jmap_types::JmapError::server_fail(inner.to_string())
295            }
296        }
297    }
298}
299
300// ---------------------------------------------------------------------------
301// Result types
302// ---------------------------------------------------------------------------
303
304/// Result of a `/changes` call (RFC 8620 §5.2).
305#[derive(Debug)]
306#[non_exhaustive]
307pub struct ChangesResult {
308    /// Ids of objects that were created since `sinceState`.
309    pub created: Vec<jmap_types::Id>,
310    /// Ids of objects that were updated since `sinceState`.
311    pub updated: Vec<jmap_types::Id>,
312    /// Ids of objects that were destroyed since `sinceState`.
313    pub destroyed: Vec<jmap_types::Id>,
314    /// `true` if there are more changes beyond this batch.
315    pub has_more_changes: bool,
316    /// The current state token after applying all reported changes.
317    pub new_state: jmap_types::State,
318}
319
320impl ChangesResult {
321    /// Construct a [`ChangesResult`].
322    pub fn new(
323        created: Vec<jmap_types::Id>,
324        updated: Vec<jmap_types::Id>,
325        destroyed: Vec<jmap_types::Id>,
326        has_more_changes: bool,
327        new_state: jmap_types::State,
328    ) -> Self {
329        Self {
330            created,
331            updated,
332            destroyed,
333            has_more_changes,
334            new_state,
335        }
336    }
337}
338
339/// Result of a `/query` call (RFC 8620 §5.5).
340#[derive(Debug)]
341#[non_exhaustive]
342pub struct QueryResult {
343    /// The ordered list of matching object ids.
344    pub ids: Vec<jmap_types::Id>,
345    /// The 0-based index of the first returned id in the complete result list.
346    pub position: i64,
347    /// Total number of results, if the backend can calculate it.
348    pub total: Option<u64>,
349    /// Opaque query state token for subsequent `/queryChanges` calls.
350    pub query_state: jmap_types::State,
351    /// Whether the backend supports `/queryChanges` for this query.
352    pub can_calculate_changes: bool,
353}
354
355impl QueryResult {
356    /// Construct a [`QueryResult`].
357    pub fn new(
358        ids: Vec<jmap_types::Id>,
359        position: i64,
360        total: Option<u64>,
361        query_state: jmap_types::State,
362        can_calculate_changes: bool,
363    ) -> Self {
364        Self {
365            ids,
366            position,
367            total,
368            query_state,
369            can_calculate_changes,
370        }
371    }
372}
373
374/// One entry in the `added` list of a `/queryChanges` response (RFC 8620 §5.6).
375#[derive(Debug)]
376#[non_exhaustive]
377pub struct AddedItem {
378    /// The id of the newly-added object.
379    pub id: jmap_types::Id,
380    /// Its 0-based position in the result list after applying all changes.
381    pub index: u64,
382}
383
384impl AddedItem {
385    /// Construct an [`AddedItem`].
386    pub fn new(id: jmap_types::Id, index: u64) -> Self {
387        Self { id, index }
388    }
389}
390
391/// Result of a `/queryChanges` call (RFC 8620 §5.6).
392#[derive(Debug)]
393#[non_exhaustive]
394pub struct QueryChangesResult {
395    /// The query state token supplied by the client in `sinceQueryState`.
396    pub old_query_state: jmap_types::State,
397    /// The current query state token.
398    pub new_query_state: jmap_types::State,
399    /// Total number of results in the new query, if the backend can calculate it.
400    pub total: Option<u64>,
401    /// Ids removed from the result set since `oldQueryState`.
402    pub removed: Vec<jmap_types::Id>,
403    /// Ids added to the result set since `oldQueryState`, with their positions.
404    pub added: Vec<AddedItem>,
405}
406
407impl QueryChangesResult {
408    /// Construct a [`QueryChangesResult`].
409    pub fn new(
410        old_query_state: jmap_types::State,
411        new_query_state: jmap_types::State,
412        total: Option<u64>,
413        removed: Vec<jmap_types::Id>,
414        added: Vec<AddedItem>,
415    ) -> Self {
416        Self {
417            old_query_state,
418            new_query_state,
419            total,
420            removed,
421            added,
422        }
423    }
424}
425
426// ---------------------------------------------------------------------------
427// JmapBackend — the read-side supertrait
428// ---------------------------------------------------------------------------
429
430/// Read-side backend supertrait shared by all JMAP server crates.
431///
432/// Domain-specific backend traits (`MailBackend`, `ChatBackend`, etc.) require
433/// this trait as a supertrait and add write-side methods on top.
434///
435/// Only the read operations that have an identical signature across all JMAP
436/// object types belong here. Write operations (`create_object`, `update_object`,
437/// `destroy_object`) and domain-specific operations remain in the domain crate.
438///
439/// The `collapse_threads` parameter on `query_changes` is included for
440/// `Email/queryChanges` (RFC 8621 §4.5). Non-mail backends should pass `false`
441/// and may ignore the parameter.
442///
443/// This trait is not object-safe by design (generic methods). Use
444/// `Arc<impl JmapBackend>` when sharing across tasks.
445pub trait JmapBackend: Send + Sync + 'static {
446    /// The error type returned by storage operations.
447    type Error: std::error::Error + Send + Sync + 'static;
448
449    /// Fetch objects by id (or all objects when `ids` is `None`).
450    ///
451    /// `properties` is the list of property names requested by the client
452    /// (RFC 8620 §5.1). `None` means the client did not send a `properties`
453    /// field; the backend should return all properties. When `Some`, the backend
454    /// MAY filter the response to only the named properties, but is not required
455    /// to — implementations that always return all properties are correct.
456    ///
457    /// Returns `(found, not_found)` — objects that exist and ids that do not.
458    fn get_objects<O: GetObject + Send + Sync>(
459        &self,
460        account_id: &jmap_types::Id,
461        ids: Option<&[jmap_types::Id]>,
462        properties: Option<&[String]>,
463    ) -> impl std::future::Future<Output = Result<(Vec<O>, Vec<jmap_types::Id>), Self::Error>> + Send;
464
465    /// Return the current state token for an object type in the given account.
466    fn get_state<O: JmapObject + Send + Sync>(
467        &self,
468        account_id: &jmap_types::Id,
469    ) -> impl std::future::Future<Output = Result<jmap_types::State, Self::Error>> + Send;
470
471    /// Return changes since `since_state`, up to `max_changes` entries.
472    fn get_changes<O: JmapObject + Send + Sync>(
473        &self,
474        account_id: &jmap_types::Id,
475        since_state: &jmap_types::State,
476        max_changes: Option<u64>,
477    ) -> impl std::future::Future<Output = Result<ChangesResult, BackendChangesError<Self::Error>>> + Send;
478
479    /// Execute a `/query` and return a page of matching ids.
480    ///
481    /// `position` may be negative — negative values are relative to the end of
482    /// the result set per RFC 8620 §5.5 (e.g. -1 means the last result).
483    fn query_objects<O: QueryObject + Send + Sync>(
484        &self,
485        account_id: &jmap_types::Id,
486        filter: Option<&O::Filter>,
487        sort: Option<&[O::Comparator]>,
488        limit: Option<u64>,
489        position: i64,
490    ) -> impl std::future::Future<Output = Result<QueryResult, Self::Error>> + Send;
491
492    /// Execute a `/queryChanges` and return deltas since `since_query_state`.
493    ///
494    /// `collapse_threads` is only meaningful for `Email/queryChanges`
495    /// (RFC 8621 §4.5). Pass `false` for all other object types.
496    #[allow(clippy::too_many_arguments)]
497    fn query_changes<O: QueryObject + Send + Sync>(
498        &self,
499        account_id: &jmap_types::Id,
500        since_query_state: &jmap_types::State,
501        filter: Option<&O::Filter>,
502        sort: Option<&[O::Comparator]>,
503        max_changes: Option<u64>,
504        up_to_id: Option<&jmap_types::Id>,
505        collapse_threads: bool,
506    ) -> impl std::future::Future<
507        Output = Result<QueryChangesResult, BackendChangesError<Self::Error>>,
508    > + Send;
509}
510
511// ---------------------------------------------------------------------------
512// Tests
513// ---------------------------------------------------------------------------
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518
519    /// Oracle: BackendChangesError::TooManyChanges { limit: 0 } must map to
520    /// cannotCalculateChanges (RFC 8620 §5.6), not tooManyChanges with limit 0.
521    ///
522    /// limit=0 is the convention for "cannot calculate".
523    #[test]
524    fn backend_changes_error_limit_zero_maps_to_cannot_calculate() {
525        let err = jmap_types::JmapError::from(
526            BackendChangesError::<std::convert::Infallible>::TooManyChanges { limit: 0 },
527        );
528        assert_eq!(
529            err.error_type.as_str(),
530            "cannotCalculateChanges",
531            "limit=0 must produce cannotCalculateChanges; got: {:?}",
532            err.error_type
533        );
534    }
535
536    /// Oracle: BackendChangesError::TooManyChanges { limit: N } (N > 0) maps to
537    /// tooManyChanges with the suggested limit.
538    #[test]
539    fn backend_changes_error_nonzero_limit_maps_to_too_many_changes() {
540        let err = jmap_types::JmapError::from(
541            BackendChangesError::<std::convert::Infallible>::TooManyChanges { limit: 50 },
542        );
543        assert_eq!(
544            err.error_type.as_str(),
545            "tooManyChanges",
546            "limit=50 must produce tooManyChanges; got: {:?}",
547            err.error_type
548        );
549    }
550}