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}