jmap_contacts_types/backend.rs
1//! Property selector enums and [`jmap_types::JmapObject`] impls for JMAP
2//! Contacts types.
3//!
4//! These are defined here so that `jmap-contacts-server` can use them without
5//! violating the orphan rule (`JmapObject` is foreign but the contacts types
6//! are local to this crate).
7//!
8//! `AddressBook` does **not** implement [`QueryObject`] because
9//! RFC 9610 does not define `AddressBook/query` or
10//! `AddressBook/queryChanges`.
11
12use jmap_types::{GetObject, JmapObject, PatchObject, QueryObject, SetObject};
13
14// ---------------------------------------------------------------------------
15// Property selector enums (server-side; no serde required)
16// ---------------------------------------------------------------------------
17
18/// Property selector for [`crate::AddressBook`] `/get` and `/set`.
19#[non_exhaustive]
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub enum AddressBookProperty {
22 /// The `id` property (RFC 9610 §2).
23 Id,
24 /// The `name` property (RFC 9610 §2).
25 Name,
26 /// The `description` property (RFC 9610 §2).
27 Description,
28 /// The `sortOrder` property (RFC 9610 §2).
29 SortOrder,
30 /// The `isDefault` property (RFC 9610 §2).
31 IsDefault,
32 /// The `isSubscribed` property (RFC 9610 §2).
33 IsSubscribed,
34 /// The `shareWith` property (RFC 9610 §2).
35 ShareWith,
36 /// The `myRights` property (RFC 9610 §2).
37 MyRights,
38}
39
40/// Property selector for [`crate::ContactCard`] `/get`, `/set`, and `/query`.
41#[non_exhaustive]
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub enum ContactCardProperty {
44 /// The `id` property (RFC 9610 §3; JMAP addition).
45 Id,
46 /// The `addressBookIds` property (RFC 9610 §3; JMAP addition).
47 AddressBookIds,
48 /// The `version` property (RFC 9553 §2.1 Metadata).
49 Version,
50 /// The `created` property (RFC 9553 §2.1 Metadata).
51 Created,
52 /// The `kind` property (RFC 9553 §2.1 Metadata).
53 Kind,
54 /// The `language` property (RFC 9553 §2.1 Metadata).
55 Language,
56 /// The `members` property (RFC 9553 §2.1 Metadata).
57 Members,
58 /// The `prodId` property (RFC 9553 §2.1 Metadata).
59 ProdId,
60 /// The `relatedTo` property (RFC 9553 §2.1 Metadata).
61 RelatedTo,
62 /// The `uid` property (RFC 9553 §2.1 Metadata).
63 Uid,
64 /// The `updated` property (RFC 9553 §2.1 Metadata).
65 Updated,
66 /// The `name` property (RFC 9553 §2.2 Name and Organization).
67 Name,
68 /// The `nicknames` property (RFC 9553 §2.2 Name and Organization).
69 Nicknames,
70 /// The `organizations` property (RFC 9553 §2.2 Name and Organization).
71 Organizations,
72 /// The `speakToAs` property (RFC 9553 §2.2 Name and Organization).
73 SpeakToAs,
74 /// The `titles` property (RFC 9553 §2.2 Name and Organization).
75 Titles,
76 /// The `emails` property (RFC 9553 §2.3 Contact).
77 Emails,
78 /// The `onlineServices` property (RFC 9553 §2.3 Contact).
79 OnlineServices,
80 /// The `phones` property (RFC 9553 §2.3 Contact).
81 Phones,
82 /// The `preferredLanguages` property (RFC 9553 §2.3 Contact).
83 PreferredLanguages,
84 /// The `calendars` property (RFC 9553 §2.4 Calendaring).
85 Calendars,
86 /// The `schedulingAddresses` property (RFC 9553 §2.4 Calendaring).
87 SchedulingAddresses,
88 /// The `addresses` property (RFC 9553 §2.5 Address).
89 Addresses,
90 /// The `cryptoKeys` property (RFC 9553 §2.6 Resources).
91 CryptoKeys,
92 /// The `directories` property (RFC 9553 §2.6 Resources).
93 Directories,
94 /// The `links` property (RFC 9553 §2.6 Resources).
95 Links,
96 /// The `media` property (RFC 9553 §2.6 Resources).
97 Media,
98 /// The `localizations` property (RFC 9553 §2.7 Multilingual).
99 Localizations,
100 /// The `anniversaries` property (RFC 9553 §2.8 Additional).
101 Anniversaries,
102 /// The `keywords` property (RFC 9553 §2.8 Additional).
103 Keywords,
104 /// The `notes` property (RFC 9553 §2.8 Additional).
105 Notes,
106 /// The `personalInfo` property (RFC 9553 §2.8 Additional).
107 PersonalInfo,
108}
109
110// ---------------------------------------------------------------------------
111// JmapObject impls
112// ---------------------------------------------------------------------------
113
114impl JmapObject for crate::AddressBook {
115 const TYPE_NAME: &'static str = "AddressBook";
116 type Property = AddressBookProperty;
117}
118
119impl GetObject for crate::AddressBook {}
120
121/// RFC 9610 §2 (rfc9610.txt:167, 191, 221): `id` is "(immutable;
122/// server-set)"; `isDefault` and `myRights` are "(server-set)".
123///
124/// draft-ietf-jmap-metadata-04 §3.2: clients write `metadata` and
125/// `privateMetadata` through `Foo/set`.
126impl SetObject for crate::AddressBook {
127 type Patch = PatchObject;
128 const SERVER_SET: &'static [&'static str] = &["id", "isDefault", "myRights"];
129 const CLIENT_SETTABLE: &'static [&'static str] = &[
130 "name",
131 "description",
132 "sortOrder",
133 "isSubscribed",
134 "shareWith",
135 "metadata",
136 "privateMetadata",
137 ];
138 const IMMUTABLE: &'static [&'static str] = &[];
139}
140
141// AddressBook does NOT implement QueryObject — spec has no AddressBook/query.
142
143impl JmapObject for crate::ContactCard {
144 const TYPE_NAME: &'static str = "ContactCard";
145 type Property = ContactCardProperty;
146}
147
148impl GetObject for crate::ContactCard {}
149
150/// RFC 9610 §3 (rfc9610.txt:305): `id` is "(immutable; server-set)".
151/// `addressBookIds` and the RFC 9553 Card properties carry neither
152/// attribute.
153///
154/// draft-ietf-jmap-metadata-04 §3.2: clients write `metadata` and
155/// `privateMetadata` through `Foo/set`.
156impl SetObject for crate::ContactCard {
157 type Patch = PatchObject;
158 const SERVER_SET: &'static [&'static str] = &["id"];
159 const CLIENT_SETTABLE: &'static [&'static str] = &[
160 "addressBookIds",
161 "version",
162 "created",
163 "kind",
164 "language",
165 "members",
166 "prodId",
167 "relatedTo",
168 "uid",
169 "updated",
170 "name",
171 "nicknames",
172 "organizations",
173 "speakToAs",
174 "titles",
175 "emails",
176 "onlineServices",
177 "phones",
178 "preferredLanguages",
179 "calendars",
180 "schedulingAddresses",
181 "addresses",
182 "cryptoKeys",
183 "directories",
184 "links",
185 "media",
186 "localizations",
187 "anniversaries",
188 "keywords",
189 "notes",
190 "personalInfo",
191 "metadata",
192 "privateMetadata",
193 ];
194 const IMMUTABLE: &'static [&'static str] = &[];
195}
196
197impl QueryObject for crate::ContactCard {
198 type Filter = crate::ContactCardFilter;
199 type Comparator = crate::ContactCardComparator;
200}
201
202#[cfg(test)]
203mod tests {
204 //! Each test builds a fully populated object with a struct literal, so a
205 //! new field is a compile error here until it is set, then a
206 //! classification failure until the `SetObject` lists name it.
207
208 use jmap_types::{Id, PatchObject, UTCDate, check_property_classification};
209 use serde_json::{Map, Value, json};
210 use std::collections::BTreeMap;
211
212 fn de<T: serde::de::DeserializeOwned>(v: Value) -> T {
213 serde_json::from_value(v).expect("hand-written fixture deserializes")
214 }
215
216 fn rights() -> crate::AddressBookRights {
217 de(json!({"mayRead": true, "mayWrite": true, "mayShare": true, "mayDelete": true}))
218 }
219
220 #[test]
221 fn address_book_properties_are_classified() {
222 let obj = crate::AddressBook {
223 id: Id::from("a1"),
224 name: "n".into(),
225 description: Some("d".into()),
226 sort_order: 1,
227 is_default: true,
228 is_subscribed: true,
229 share_with: Some(BTreeMap::from([(Id::from("p1"), rights())])),
230 my_rights: rights(),
231 metadata: Some(de(json!({"acme.example.com": {"memo": "m"}}))),
232 private_metadata: Some(de(json!({"acme.example.com": {"memo": "p"}}))),
233 extra: Map::new(),
234 };
235 assert_eq!(check_property_classification(&obj), Ok(()));
236 }
237
238 #[test]
239 fn contact_card_properties_are_classified() {
240 let v = || Some(json!({}));
241 let patch: PatchObject = de(json!({"name/full": "N"}));
242 let obj = crate::ContactCard {
243 id: Some(Id::from("c1")),
244 address_book_ids: Some(BTreeMap::from([(Id::from("a1"), true)])),
245 version: Some("1.0".into()),
246 created: Some(UTCDate::from("2026-01-01T00:00:00Z")),
247 kind: Some("individual".into()),
248 language: Some("en".into()),
249 members: Some(BTreeMap::from([("u2".into(), true)])),
250 prod_id: Some("p".into()),
251 related_to: v(),
252 uid: Some("u".into()),
253 updated: Some(UTCDate::from("2026-01-01T00:00:00Z")),
254 name: v(),
255 nicknames: v(),
256 organizations: v(),
257 speak_to_as: v(),
258 titles: v(),
259 emails: v(),
260 online_services: v(),
261 phones: v(),
262 preferred_languages: v(),
263 calendars: v(),
264 scheduling_addresses: v(),
265 addresses: v(),
266 crypto_keys: v(),
267 directories: v(),
268 links: v(),
269 media: v(),
270 localizations: Some(BTreeMap::from([("de".into(), patch)])),
271 anniversaries: v(),
272 keywords: Some(BTreeMap::from([("k".into(), true)])),
273 notes: v(),
274 personal_info: v(),
275 metadata: Some(de(json!({"acme.example.com": {"memo": "m"}}))),
276 private_metadata: Some(de(json!({"acme.example.com": {"memo": "p"}}))),
277 extra: Map::new(),
278 };
279 assert_eq!(check_property_classification(&obj), Ok(()));
280 }
281}