Skip to main content

jmap_contacts_types/
addressbook.rs

1//! RFC 9610 §2 — AddressBook object and component types.
2//!
3//! Provides [`AddressBook`] and [`AddressBookRights`].
4//!
5//! Note: the spec defines no `AddressBook/query` method, so no
6//! `AddressBookFilterCondition` type exists.
7
8use std::collections::BTreeMap;
9
10use jmap_types::Id;
11use serde::{Deserialize, Serialize};
12
13use crate::collision::{self, CollisionError};
14
15/// Camel-case wire-format names of every typed field on
16/// [`AddressBookRights`]. Used by
17/// [`AddressBookRights::validate_extras`]. JMAP-glx8.25.
18const ADDRESS_BOOK_RIGHTS_TYPED_FIELDS: &[&str] = &[
19    "mayRead",
20    "mayWrite",
21    "mayShare",
22    "mayDelete",
23    "maySetMetadata",
24];
25
26/// Camel-case wire-format names of every typed field on [`AddressBook`].
27/// Used by [`AddressBook::validate_extras`]. JMAP-glx8.25.
28const ADDRESS_BOOK_TYPED_FIELDS: &[&str] = &[
29    "id",
30    "name",
31    "description",
32    "sortOrder",
33    "isDefault",
34    "isSubscribed",
35    "shareWith",
36    "myRights",
37    "metadata",
38    "privateMetadata",
39];
40
41/// Access rights a principal holds on an AddressBook (RFC 9610 §2).
42///
43/// All four rights are booleans.  `Default` produces all-false, which is the
44/// most restrictive valid value and a safe starting point when constructing
45/// rights in tests or server code.
46#[non_exhaustive]
47#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct AddressBookRights {
50    /// User may fetch the ContactCards in this AddressBook.
51    pub may_read: bool,
52    /// User may create, modify, or destroy all ContactCards in this AddressBook,
53    /// or move them to or from this AddressBook.
54    pub may_write: bool,
55    /// User may modify the `shareWith` property for this AddressBook.
56    pub may_share: bool,
57    /// User may delete the AddressBook itself.
58    pub may_delete: bool,
59    /// User may modify the shared metadata of this AddressBook and of the
60    /// ContactCards in it (draft-ietf-jmap-metadata-04 §4.1).
61    /// `None` when the server omits it: the right is present only when the
62    /// metadata capability lists this type or a type it contains (§4.1).
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub may_set_metadata: Option<bool>,
65
66    /// Catch-all for vendor / site / private extension fields not covered
67    /// by the typed fields above. Preserves unknown fields across
68    /// deserialize/serialize round-trip per workspace extras-preservation
69    /// policy (see workspace AGENTS.md).
70    ///
71    /// See [`ContactCard::extra`](crate::ContactCard::extra) for the
72    /// collision contract: programmatically inserting a key that
73    /// matches a typed field name produces a duplicate JSON object key
74    /// on serialize. Treat `extra` as a write-only catch-all for unknown
75    /// keys discovered at deserialize. JMAP-glx8.19.
76    ///
77    /// See [`AddressBookRights::validate_extras`] for a runtime
78    /// pre-serialize check that detects this hazard. JMAP-glx8.25.
79    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
80    pub extra: serde_json::Map<String, serde_json::Value>,
81}
82
83impl AddressBookRights {
84    /// Check that no [`extra`](Self::extra) key shadows a typed
85    /// wire-format field of this struct. Returns
86    /// [`Err(CollisionError)`](CollisionError) listing any colliding
87    /// keys; otherwise returns `Ok(())`. See
88    /// [`ContactCard::validate_extras`](crate::ContactCard::validate_extras)
89    /// for the design rationale. JMAP-glx8.25.
90    ///
91    /// # Errors
92    ///
93    /// Returns [`CollisionError`] when one or more keys in
94    /// [`extra`](Self::extra) match one of the camelCase wire-format
95    /// names of this struct's typed fields.
96    pub fn validate_extras(&self) -> Result<(), CollisionError> {
97        collision::check(&self.extra, ADDRESS_BOOK_RIGHTS_TYPED_FIELDS)
98    }
99}
100
101/// A JMAP AddressBook object (RFC 9610 §2).
102///
103/// An AddressBook is a named collection of ContactCards.  All ContactCards
104/// belong to one or more AddressBooks.
105#[non_exhaustive]
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct AddressBook {
109    /// Server-assigned immutable identifier.
110    pub id: Id,
111    /// User-visible name; MUST NOT be empty and MUST NOT exceed 255 UTF-8 octets.
112    pub name: String,
113    /// Optional longer-form description (default: null).
114    ///
115    /// Required-and-nullable per RFC 9553 + RFC 9610 §4.1: always present on
116    /// the wire as `"description": null` when unset.  Must NOT use
117    /// `skip_serializing_if`.
118    #[serde(default)]
119    pub description: Option<String>,
120    /// UI sort order; lower values display first.  Range: [0, 2^31).
121    /// Defaults to 0 per RFC 9610 §2.
122    #[serde(default)]
123    pub sort_order: u32,
124    /// True for at most one AddressBook per account; server-set.
125    pub is_default: bool,
126    /// True if the user has subscribed to this AddressBook.
127    pub is_subscribed: bool,
128    /// Map of principal id → rights for principals this AddressBook is shared
129    /// with.
130    ///
131    /// Required-and-nullable (RFC 9610 §2 type: `Id[AddressBookRights]|null`):
132    /// always present in wire JSON; serializes as `null` when not shared.
133    /// The spec §4.1 example shows `"shareWith": null` explicitly — never absent.
134    #[serde(default)]
135    pub share_with: Option<BTreeMap<Id, AddressBookRights>>,
136    /// ACL rights the authenticated user has on this AddressBook; server-set.
137    pub my_rights: AddressBookRights,
138
139    /// Shared metadata, keyed by namespace identifier
140    /// (draft-ietf-jmap-metadata-04 §2). Each value is a JSON object (§2.1).
141    /// `None` when absent, which -04 §2 treats as `{}`; in an update a
142    /// `null` value "is equivalent to {}" (-04 §3.2) and also decodes as
143    /// `None`.
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub metadata: Option<serde_json::Map<String, serde_json::Value>>,
146
147    /// The authenticated user's own metadata, keyed by namespace
148    /// identifier (draft-ietf-jmap-metadata-04 §2). Same shape and `None`
149    /// handling as `metadata`.
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub private_metadata: Option<serde_json::Map<String, serde_json::Value>>,
152
153    /// Catch-all for vendor / site / private extension fields not covered
154    /// by the typed fields above. Preserves unknown fields across
155    /// deserialize/serialize round-trip per workspace extras-preservation
156    /// policy (see workspace AGENTS.md).
157    ///
158    /// See [`ContactCard::extra`](crate::ContactCard::extra) for the
159    /// collision contract: programmatically inserting a key that
160    /// matches a typed field name produces a duplicate JSON object key
161    /// on serialize. JMAP-glx8.19.
162    ///
163    /// See [`AddressBook::validate_extras`] for a runtime pre-serialize
164    /// check that detects this hazard. JMAP-glx8.25.
165    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
166    pub extra: serde_json::Map<String, serde_json::Value>,
167}
168
169impl AddressBook {
170    /// Check that no [`extra`](Self::extra) key shadows a typed
171    /// wire-format field of this struct. Returns
172    /// [`Err(CollisionError)`](CollisionError) listing any colliding
173    /// keys; otherwise returns `Ok(())`. See
174    /// [`ContactCard::validate_extras`](crate::ContactCard::validate_extras)
175    /// for the design rationale. JMAP-glx8.25.
176    ///
177    /// # Errors
178    ///
179    /// Returns [`CollisionError`] when one or more keys in
180    /// [`extra`](Self::extra) match one of the camelCase wire-format
181    /// names of this struct's typed fields.
182    pub fn validate_extras(&self) -> Result<(), CollisionError> {
183        collision::check(&self.extra, ADDRESS_BOOK_TYPED_FIELDS)
184    }
185}