Skip to main content

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}