Skip to main content

jmap_mail_types/
backend.rs

1//! Property selector enums and [`jmap_types::JmapObject`] impls for RFC 8621 types.
2//!
3//! These are defined here so that `jmap-mail-server` can use them without
4//! violating the orphan rule (`JmapObject` is foreign but the mail types are
5//! local to this crate).
6
7use jmap_types::{GetObject, JmapObject, PatchObject, QueryObject, SetObject};
8
9// ---------------------------------------------------------------------------
10// Property selector enums (server-side; no serde required)
11// ---------------------------------------------------------------------------
12
13/// Property selector for [`crate::Mailbox`] `/get` and `/set`.
14#[non_exhaustive]
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub enum MailboxProperty {
17    /// The `id` property (RFC 8621 §2).
18    Id,
19    /// The `name` property (RFC 8621 §2).
20    Name,
21    /// The `parentId` property (RFC 8621 §2).
22    ParentId,
23    /// The `role` property (RFC 8621 §2).
24    Role,
25    /// The `sortOrder` property (RFC 8621 §2).
26    SortOrder,
27    /// The `totalEmails` property (RFC 8621 §2).
28    TotalEmails,
29    /// The `unreadEmails` property (RFC 8621 §2).
30    UnreadEmails,
31    /// The `totalThreads` property (RFC 8621 §2).
32    TotalThreads,
33    /// The `unreadThreads` property (RFC 8621 §2).
34    UnreadThreads,
35    /// The `myRights` property (RFC 8621 §2).
36    MyRights,
37    /// The `isSubscribed` property (RFC 8621 §2).
38    IsSubscribed,
39}
40
41/// Property selector for [`crate::Thread`] `/get`.
42#[non_exhaustive]
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub enum ThreadProperty {
45    /// The `id` property (RFC 8621 §3).
46    Id,
47    /// The `emailIds` property (RFC 8621 §3).
48    EmailIds,
49}
50
51/// Property selector for [`crate::Email`] `/get` and `/set`.
52#[non_exhaustive]
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub enum EmailProperty {
55    /// The `id` property (RFC 8621 §4).
56    Id,
57    /// The `blobId` property (RFC 8621 §4).
58    BlobId,
59    /// The `threadId` property (RFC 8621 §4).
60    ThreadId,
61    /// The `mailboxIds` property (RFC 8621 §4).
62    MailboxIds,
63    /// The `keywords` property (RFC 8621 §4).
64    Keywords,
65    /// The `size` property (RFC 8621 §4).
66    Size,
67    /// The `receivedAt` property (RFC 8621 §4).
68    ReceivedAt,
69    /// The `messageId` property (RFC 8621 §4).
70    MessageId,
71    /// The `inReplyTo` property (RFC 8621 §4).
72    InReplyTo,
73    /// The `references` property (RFC 8621 §4).
74    References,
75    /// The `subject` property (RFC 8621 §4).
76    Subject,
77    /// The `from` property (RFC 8621 §4).
78    From,
79    /// The `to` property (RFC 8621 §4).
80    To,
81    /// The `cc` property (RFC 8621 §4).
82    Cc,
83    /// The `bcc` property (RFC 8621 §4).
84    Bcc,
85    /// The `replyTo` property (RFC 8621 §4).
86    ReplyTo,
87    /// The `sender` property (RFC 8621 §4).
88    Sender,
89    /// The `sentAt` property (RFC 8621 §4).
90    SentAt,
91    /// The `hasAttachment` property (RFC 8621 §4).
92    HasAttachment,
93    /// The `preview` property (RFC 8621 §4).
94    Preview,
95    /// The `bodyStructure` property (RFC 8621 §4).
96    BodyStructure,
97    /// The `textBody` property (RFC 8621 §4).
98    TextBody,
99    /// The `htmlBody` property (RFC 8621 §4).
100    HtmlBody,
101    /// The `attachments` property (RFC 8621 §4).
102    Attachments,
103    /// The `bodyValues` property (RFC 8621 §4).
104    BodyValues,
105    /// The `headers` property (RFC 8621 §4).
106    Headers,
107}
108
109/// Property selector for [`crate::Identity`] `/get` and `/set`.
110#[non_exhaustive]
111#[derive(Debug, Clone, PartialEq, Eq, Hash)]
112pub enum IdentityProperty {
113    /// The `id` property (RFC 8621 §6).
114    Id,
115    /// The `name` property (RFC 8621 §6).
116    Name,
117    /// The `email` property (RFC 8621 §6).
118    Email,
119    /// The `replyTo` property (RFC 8621 §6).
120    ReplyTo,
121    /// The `bcc` property (RFC 8621 §6).
122    Bcc,
123    /// The `textSignature` property (RFC 8621 §6).
124    TextSignature,
125    /// The `htmlSignature` property (RFC 8621 §6).
126    HtmlSignature,
127    /// The `mayDelete` property (RFC 8621 §6).
128    MayDelete,
129}
130
131/// Property selector for [`crate::EmailSubmission`] `/get` and `/set`.
132#[non_exhaustive]
133#[derive(Debug, Clone, PartialEq, Eq, Hash)]
134pub enum EmailSubmissionProperty {
135    /// The `id` property (RFC 8621 §7).
136    Id,
137    /// The `identityId` property (RFC 8621 §7).
138    IdentityId,
139    /// The `emailId` property (RFC 8621 §7).
140    EmailId,
141    /// The `threadId` property (RFC 8621 §7).
142    ThreadId,
143    /// The `envelope` property (RFC 8621 §7).
144    Envelope,
145    /// The `sendAt` property (RFC 8621 §7).
146    SendAt,
147    /// The `undoStatus` property (RFC 8621 §7).
148    UndoStatus,
149    /// The `deliveryStatus` property (RFC 8621 §7).
150    DeliveryStatus,
151    /// The `dsnBlobIds` property (RFC 8621 §7).
152    DsnBlobIds,
153    /// The `mdnBlobIds` property (RFC 8621 §7).
154    MdnBlobIds,
155}
156
157/// Property selector for [`crate::VacationResponse`] `/get` and `/set`.
158#[non_exhaustive]
159#[derive(Debug, Clone, PartialEq, Eq, Hash)]
160pub enum VacationResponseProperty {
161    /// The `id` property (RFC 8621 §8).
162    Id,
163    /// The `isEnabled` property (RFC 8621 §8).
164    IsEnabled,
165    /// The `fromDate` property (RFC 8621 §8).
166    FromDate,
167    /// The `toDate` property (RFC 8621 §8).
168    ToDate,
169    /// The `subject` property (RFC 8621 §8).
170    Subject,
171    /// The `textBody` property (RFC 8621 §8).
172    TextBody,
173    /// The `htmlBody` property (RFC 8621 §8).
174    HtmlBody,
175}
176
177/// Property selector for [`crate::SearchSnippet`] `/get`.
178#[non_exhaustive]
179#[derive(Debug, Clone, PartialEq, Eq, Hash)]
180pub enum SearchSnippetProperty {
181    /// The `emailId` property (RFC 8621 §5).
182    EmailId,
183    /// The `subject` property (RFC 8621 §5).
184    Subject,
185    /// The `preview` property (RFC 8621 §5).
186    Preview,
187}
188
189/// Property selector for [`crate::SieveScript`] `/get` and `/set`.
190#[cfg(feature = "sieve")]
191#[non_exhaustive]
192#[derive(Debug, Clone, PartialEq, Eq, Hash)]
193pub enum SieveScriptProperty {
194    /// The `id` property (RFC 9661).
195    Id,
196    /// The `name` property (RFC 9661).
197    Name,
198    /// The `blobId` property (RFC 9661).
199    BlobId,
200    /// The `isActive` property (RFC 9661).
201    IsActive,
202}
203
204// ---------------------------------------------------------------------------
205// JmapObject impls
206// ---------------------------------------------------------------------------
207
208impl JmapObject for crate::Mailbox {
209    const TYPE_NAME: &'static str = "Mailbox";
210    type Property = MailboxProperty;
211}
212
213impl GetObject for crate::Mailbox {}
214
215/// RFC 8621 §2 (rfc8621.txt:487, 554, 567, 572, 577, 623): `id` is
216/// "(immutable; server-set)"; the four counts and `myRights` are
217/// "(server-set)". draft-ietf-jmap-metadata-04 §3.2: clients write
218/// `metadata` and `privateMetadata` through `Foo/set`.
219impl SetObject for crate::Mailbox {
220    type Patch = PatchObject;
221    const SERVER_SET: &'static [&'static str] = &[
222        "id",
223        "totalEmails",
224        "unreadEmails",
225        "totalThreads",
226        "unreadThreads",
227        "myRights",
228    ];
229    const CLIENT_SETTABLE: &'static [&'static str] = &[
230        "name",
231        "parentId",
232        "role",
233        "sortOrder",
234        "isSubscribed",
235        "metadata",
236        "privateMetadata",
237    ];
238    const IMMUTABLE: &'static [&'static str] = &[];
239}
240
241impl QueryObject for crate::Mailbox {
242    type Filter = crate::MailboxFilter;
243    type Comparator = crate::MailboxComparator;
244}
245
246impl JmapObject for crate::Thread {
247    const TYPE_NAME: &'static str = "Thread";
248    type Property = ThreadProperty;
249}
250
251impl GetObject for crate::Thread {}
252
253impl JmapObject for crate::Email {
254    const TYPE_NAME: &'static str = "Email";
255    type Property = EmailProperty;
256}
257
258impl GetObject for crate::Email {}
259
260/// RFC 8621 §4.1 (rfc8621.txt:1316-2135): `id`, `blobId`, `threadId`,
261/// `size`, `hasAttachment` and `preview` are "(immutable; server-set)";
262/// `mailboxIds` and `keywords` carry no attribute; every other property is
263/// "(immutable)". The `header:` forms (§4.1.3) are not struct fields.
264/// draft-ietf-jmap-metadata-04 §3.2: clients write `metadata` and
265/// `privateMetadata` through `Foo/set`.
266impl SetObject for crate::Email {
267    type Patch = PatchObject;
268    const SERVER_SET: &'static [&'static str] = &[
269        "id",
270        "blobId",
271        "threadId",
272        "size",
273        "hasAttachment",
274        "preview",
275    ];
276    const CLIENT_SETTABLE: &'static [&'static str] = &[
277        "mailboxIds",
278        "keywords",
279        "receivedAt",
280        "messageId",
281        "inReplyTo",
282        "references",
283        "sender",
284        "from",
285        "to",
286        "cc",
287        "bcc",
288        "replyTo",
289        "subject",
290        "sentAt",
291        "headers",
292        "bodyStructure",
293        "bodyValues",
294        "textBody",
295        "htmlBody",
296        "attachments",
297        "metadata",
298        "privateMetadata",
299    ];
300    const IMMUTABLE: &'static [&'static str] = &[
301        "receivedAt",
302        "messageId",
303        "inReplyTo",
304        "references",
305        "sender",
306        "from",
307        "to",
308        "cc",
309        "bcc",
310        "replyTo",
311        "subject",
312        "sentAt",
313        "headers",
314        "bodyStructure",
315        "bodyValues",
316        "textBody",
317        "htmlBody",
318        "attachments",
319    ];
320}
321
322impl QueryObject for crate::Email {
323    type Filter = crate::EmailFilter;
324    type Comparator = crate::EmailComparator;
325}
326
327impl JmapObject for crate::Identity {
328    const TYPE_NAME: &'static str = "Identity";
329    type Property = IdentityProperty;
330}
331
332impl GetObject for crate::Identity {}
333
334/// RFC 8621 §6 (rfc8621.txt:3988, 3997, 4039): `id` is "(immutable;
335/// server-set)", `email` is "(immutable)", `mayDelete` is "(server-set)".
336impl SetObject for crate::Identity {
337    type Patch = PatchObject;
338    const SERVER_SET: &'static [&'static str] = &["id", "mayDelete"];
339    const CLIENT_SETTABLE: &'static [&'static str] = &[
340        "name",
341        "email",
342        "replyTo",
343        "bcc",
344        "textSignature",
345        "htmlSignature",
346    ];
347    const IMMUTABLE: &'static [&'static str] = &["email"];
348}
349
350impl JmapObject for crate::EmailSubmission {
351    const TYPE_NAME: &'static str = "EmailSubmission";
352    type Property = EmailSubmissionProperty;
353}
354
355impl GetObject for crate::EmailSubmission {}
356
357/// RFC 8621 §7 (rfc8621.txt:4134-4417): `id`, `threadId` and `sendAt` are
358/// "(immutable; server-set)"; `deliveryStatus`, `dsnBlobIds` and
359/// `mdnBlobIds` are "(server-set)"; `identityId`, `emailId` and `envelope`
360/// are "(immutable)"; `undoStatus` carries no attribute.
361impl SetObject for crate::EmailSubmission {
362    type Patch = PatchObject;
363    const SERVER_SET: &'static [&'static str] = &[
364        "id",
365        "threadId",
366        "sendAt",
367        "deliveryStatus",
368        "dsnBlobIds",
369        "mdnBlobIds",
370    ];
371    const CLIENT_SETTABLE: &'static [&'static str] =
372        &["identityId", "emailId", "envelope", "undoStatus"];
373    const IMMUTABLE: &'static [&'static str] = &["identityId", "emailId", "envelope"];
374}
375
376impl QueryObject for crate::EmailSubmission {
377    type Filter = crate::EmailSubmissionFilter;
378    type Comparator = crate::EmailSubmissionComparator;
379}
380
381impl JmapObject for crate::VacationResponse {
382    const TYPE_NAME: &'static str = "VacationResponse";
383    type Property = VacationResponseProperty;
384}
385
386impl GetObject for crate::VacationResponse {}
387
388/// RFC 8621 §8 (rfc8621.txt:4801): `id` is "(immutable; server-set)"; the
389/// other properties carry no attribute.
390impl SetObject for crate::VacationResponse {
391    type Patch = PatchObject;
392    const SERVER_SET: &'static [&'static str] = &["id"];
393    const CLIENT_SETTABLE: &'static [&'static str] = &[
394        "isEnabled",
395        "fromDate",
396        "toDate",
397        "subject",
398        "textBody",
399        "htmlBody",
400    ];
401    const IMMUTABLE: &'static [&'static str] = &[];
402}
403
404impl JmapObject for crate::SearchSnippet {
405    const TYPE_NAME: &'static str = "SearchSnippet";
406    type Property = SearchSnippetProperty;
407}
408
409#[cfg(feature = "sieve")]
410impl JmapObject for crate::sieve::SieveScript {
411    const TYPE_NAME: &'static str = "SieveScript";
412    type Property = SieveScriptProperty;
413}
414
415#[cfg(feature = "sieve")]
416impl GetObject for crate::sieve::SieveScript {}
417
418/// RFC 9661 §2 (rfc9661.txt:261, 284): `id` is "(immutable; server-set)",
419/// `isActive` is "(server-set; default: false)".
420#[cfg(feature = "sieve")]
421impl SetObject for crate::sieve::SieveScript {
422    type Patch = PatchObject;
423    const SERVER_SET: &'static [&'static str] = &["id", "isActive"];
424    const CLIENT_SETTABLE: &'static [&'static str] = &["name", "blobId"];
425    const IMMUTABLE: &'static [&'static str] = &[];
426}
427
428#[cfg(test)]
429mod tests {
430    //! Each test builds a fully populated object with a struct literal, so a
431    //! new field is a compile error here until it is set, then a
432    //! classification failure until the `SetObject` lists name it.
433
434    use jmap_types::{Date, Id, UTCDate, check_property_classification};
435    use serde_json::{Map, Value, json};
436    use std::collections::BTreeMap;
437
438    fn de<T: serde::de::DeserializeOwned>(v: Value) -> T {
439        serde_json::from_value(v).expect("hand-written fixture deserializes")
440    }
441
442    fn addrs() -> Option<Vec<crate::EmailAddress>> {
443        Some(de(json!([{"name": "A", "email": "a@example.com"}])))
444    }
445
446    fn part() -> crate::EmailBodyPart {
447        de(json!({"partId": "1", "type": "text/plain"}))
448    }
449
450    #[test]
451    fn mailbox_properties_are_classified() {
452        let obj = crate::Mailbox {
453            id: Id::from("m1"),
454            name: "Inbox".into(),
455            parent_id: Some(Id::from("m0")),
456            role: Some(de(json!("inbox"))),
457            sort_order: 1,
458            total_emails: 1,
459            unread_emails: 1,
460            total_threads: 1,
461            unread_threads: 1,
462            my_rights: crate::MailboxRights::default(),
463            is_subscribed: true,
464            metadata: Some(de(json!({"acme.example.com": {"memo": "m"}}))),
465            private_metadata: Some(de(json!({"acme.example.com": {"memo": "p"}}))),
466            extra: Map::new(),
467        };
468        assert_eq!(check_property_classification(&obj), Ok(()));
469    }
470
471    #[test]
472    fn email_properties_are_classified() {
473        let obj = crate::Email {
474            id: Id::from("e1"),
475            blob_id: Id::from("b1"),
476            thread_id: Id::from("t1"),
477            mailbox_ids: BTreeMap::from([(Id::from("m1"), true)]),
478            keywords: de(json!({"$seen": true})),
479            size: 1,
480            received_at: UTCDate::from("2026-01-01T00:00:00Z"),
481            message_id: Some(vec!["a@example.com".into()]),
482            in_reply_to: Some(vec!["b@example.com".into()]),
483            references: Some(vec!["b@example.com".into()]),
484            sender: addrs(),
485            from: addrs(),
486            to: addrs(),
487            cc: addrs(),
488            bcc: addrs(),
489            reply_to: addrs(),
490            subject: Some("s".into()),
491            sent_at: Some(Date::from("2026-01-01T00:00:00Z")),
492            headers: de(json!([{"name": "Subject", "value": " s"}])),
493            body_values: de(json!({"1": {"value": "v"}})),
494            text_body: vec![part()],
495            html_body: vec![part()],
496            attachments: vec![part()],
497            body_structure: Some(part()),
498            has_attachment: true,
499            preview: Some("p".into()),
500            metadata: Some(de(json!({"acme.example.com": {"memo": "m"}}))),
501            private_metadata: Some(de(json!({"acme.example.com": {"memo": "p"}}))),
502            extra: Map::new(),
503        };
504        assert_eq!(check_property_classification(&obj), Ok(()));
505    }
506
507    #[test]
508    fn identity_properties_are_classified() {
509        let obj = crate::Identity {
510            id: Id::from("i1"),
511            name: "A".into(),
512            email: "a@example.com".into(),
513            reply_to: addrs(),
514            bcc: addrs(),
515            text_signature: "t".into(),
516            html_signature: "h".into(),
517            may_delete: true,
518            extra: Map::new(),
519        };
520        assert_eq!(check_property_classification(&obj), Ok(()));
521    }
522
523    #[test]
524    fn email_submission_properties_are_classified() {
525        let obj = crate::EmailSubmission {
526            id: Id::from("s1"),
527            identity_id: Id::from("i1"),
528            email_id: Id::from("e1"),
529            thread_id: Id::from("t1"),
530            envelope: Some(de(json!({
531                "mailFrom": {"email": "a@example.com"},
532                "rcptTo": [{"email": "b@example.com"}],
533            }))),
534            send_at: UTCDate::from("2026-01-01T00:00:00Z"),
535            undo_status: de(json!("pending")),
536            delivery_status: Some(de(json!({"b@example.com": {
537                "smtpReply": "250 OK",
538                "delivered": "queued",
539                "displayed": "unknown",
540            }}))),
541            dsn_blob_ids: vec![Id::from("d1")],
542            mdn_blob_ids: vec![Id::from("n1")],
543            extra: Map::new(),
544        };
545        assert_eq!(check_property_classification(&obj), Ok(()));
546    }
547
548    #[test]
549    fn vacation_response_properties_are_classified() {
550        let obj = crate::VacationResponse {
551            id: Id::from("singleton"),
552            is_enabled: true,
553            from_date: Some(UTCDate::from("2026-01-01T00:00:00Z")),
554            to_date: Some(UTCDate::from("2026-02-01T00:00:00Z")),
555            subject: Some("s".into()),
556            text_body: Some("t".into()),
557            html_body: Some("h".into()),
558            extra: Map::new(),
559        };
560        assert_eq!(check_property_classification(&obj), Ok(()));
561    }
562
563    #[cfg(feature = "sieve")]
564    #[test]
565    fn sieve_script_properties_are_classified() {
566        let obj = crate::SieveScript {
567            id: Id::from("s1"),
568            name: Some("n".into()),
569            blob_id: Id::from("b1"),
570            is_active: true,
571            extra: Map::new(),
572        };
573        assert_eq!(check_property_classification(&obj), Ok(()));
574    }
575}