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}