jmap_mail_types/capability.rs
1//! Capability types for the JMAP for Mail extension (RFC 8621).
2//!
3//! RFC 8621 §1.3 defines three capability URIs. Each has both a
4//! session-level capability (value in the JMAP Session `capabilities` map)
5//! and an account-level capability (value in the account's
6//! `accountCapabilities` map). Session-level values are empty objects for
7//! all three URIs; only `urn:ietf:params:jmap:mail` and
8//! `urn:ietf:params:jmap:submission` carry non-empty account-level
9//! capabilities.
10
11use std::collections::HashMap;
12
13use serde::{Deserialize, Serialize};
14
15/// Capability URI for core JMAP for Mail support (RFC 8621 §1.3.1).
16pub const JMAP_MAIL_URI: &str = "urn:ietf:params:jmap:mail";
17
18/// Capability URI for the JMAP Email Submission extension (RFC 8621 §1.3.2).
19pub const JMAP_SUBMISSION_URI: &str = "urn:ietf:params:jmap:submission";
20
21/// Capability URI for the JMAP Vacation Response extension (RFC 8621 §1.3.3).
22pub const JMAP_VACATIONRESPONSE_URI: &str = "urn:ietf:params:jmap:vacationresponse";
23
24/// Session-level Mail capability (RFC 8621 §1.3.1).
25///
26/// The value of `capabilities["urn:ietf:params:jmap:mail"]` in the JMAP
27/// Session object. RFC 8621 §1.3.1 mandates that this is an empty object
28/// `{}`.
29#[non_exhaustive]
30#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
31pub struct MailCapability {}
32
33/// Account-level Mail capability (RFC 8621 §1.3.1).
34///
35/// The value of `accountCapabilities["urn:ietf:params:jmap:mail"]` for a
36/// given account. Describes server capabilities and account-level
37/// permissions for the Mail extension.
38#[non_exhaustive]
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct MailAccountCapability {
42 /// Maximum number of Mailboxes that may be assigned to a single Email,
43 /// or `null` for no limit (RFC 8621 §1.3.1). MUST be >= 1 when set.
44 pub max_mailboxes_per_email: Option<u64>,
45
46 /// Maximum depth of the Mailbox hierarchy (one more than the maximum
47 /// number of ancestors a Mailbox may have), or `null` for no limit
48 /// (RFC 8621 §1.3.1).
49 pub max_mailbox_depth: Option<u64>,
50
51 /// Maximum length, in UTF-8 octets, allowed for a Mailbox name
52 /// (RFC 8621 §1.3.1). MUST be at least 100.
53 pub max_size_mailbox_name: u64,
54
55 /// Maximum total size, in octets, of attachments allowed for a single
56 /// Email object (RFC 8621 §1.3.1). This is the unencoded attachment
57 /// total; servers may enforce a separate limit on encoded message size.
58 pub max_size_attachments_per_email: u64,
59
60 /// List of values the server supports for the `property` field of the
61 /// Comparator object in an `Email/query` sort (RFC 8621 §1.3.1 / §4.4.2).
62 /// May include vendor-specific extensions; clients MUST ignore unknown
63 /// entries.
64 pub email_query_sort_options: Vec<String>,
65
66 /// If `true`, the user may create a Mailbox in this account with a
67 /// `null` parentId (RFC 8621 §1.3.1). Permission to create a child of
68 /// an existing Mailbox is governed by `Mailbox.myRights` instead.
69 pub may_create_top_level_mailbox: bool,
70}
71
72/// Session-level Submission capability (RFC 8621 §1.3.2).
73///
74/// The value of `capabilities["urn:ietf:params:jmap:submission"]` in the
75/// JMAP Session object. RFC 8621 §1.3.2 mandates that this is an empty
76/// object `{}`.
77#[non_exhaustive]
78#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
79pub struct SubmissionCapability {}
80
81/// Account-level Submission capability (RFC 8621 §1.3.2).
82///
83/// The value of `accountCapabilities["urn:ietf:params:jmap:submission"]`
84/// for a given account. Describes server capabilities and account-level
85/// permissions for the Email Submission extension.
86#[non_exhaustive]
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct SubmissionAccountCapability {
90 /// Maximum delay, in seconds, that the server supports for delayed
91 /// send (RFC 8621 §1.3.2 / §7). Zero if the server does not support
92 /// delayed send.
93 pub max_delayed_send: u64,
94
95 /// SMTP submission extensions supported by the server. Each key is
96 /// an `ehlo-name` and the value is a list of `ehlo-args`
97 /// (RFC 8621 §1.3.2).
98 pub submission_extensions: HashMap<String, Vec<String>>,
99}
100
101/// Session-level VacationResponse capability (RFC 8621 §1.3.3).
102///
103/// The value of `capabilities["urn:ietf:params:jmap:vacationresponse"]`
104/// in the JMAP Session object. RFC 8621 §1.3.3 mandates that this is an
105/// empty object `{}` at both session level and account level.
106#[non_exhaustive]
107#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
108pub struct VacationResponseCapability {}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 /// Oracle: hand-written JSON from RFC 8621 §1.3.1 field definitions.
115 #[test]
116 fn mail_account_capability_round_trip() {
117 let json = r#"{"maxMailboxesPerEmail":null,"maxMailboxDepth":10,"maxSizeMailboxName":100,"maxSizeAttachmentsPerEmail":50000000,"emailQuerySortOptions":["receivedAt","size"],"mayCreateTopLevelMailbox":true}"#;
118 let cap: MailAccountCapability = serde_json::from_str(json).expect("must parse");
119 assert!(cap.max_mailboxes_per_email.is_none());
120 assert_eq!(cap.max_mailbox_depth, Some(10));
121 assert_eq!(cap.max_size_mailbox_name, 100);
122 assert_eq!(cap.max_size_attachments_per_email, 50_000_000);
123 assert_eq!(
124 cap.email_query_sort_options,
125 vec!["receivedAt".to_owned(), "size".to_owned()]
126 );
127 assert!(cap.may_create_top_level_mailbox);
128 let back = serde_json::to_string(&cap).expect("serialize");
129 assert_eq!(back, json);
130 }
131
132 /// Oracle: RFC 8621 §1.3.1 requires the session-level value to be `{}`.
133 #[test]
134 fn mail_capability_session_level_is_empty_object() {
135 let cap = MailCapability::default();
136 let json = serde_json::to_string(&cap).expect("serialize");
137 assert_eq!(json, "{}");
138 let back: MailCapability = serde_json::from_str("{}").expect("must parse");
139 assert_eq!(back, cap);
140 }
141
142 /// Oracle: hand-written JSON from RFC 8621 §1.3.2 field definitions.
143 #[test]
144 fn submission_account_capability_round_trip() {
145 let json = r#"{"maxDelayedSend":86400,"submissionExtensions":{"FUTURERELEASE":["86400","2024-01-01T00:00:00Z"]}}"#;
146 let cap: SubmissionAccountCapability = serde_json::from_str(json).expect("must parse");
147 assert_eq!(cap.max_delayed_send, 86_400);
148 assert_eq!(
149 cap.submission_extensions.get("FUTURERELEASE"),
150 Some(&vec!["86400".to_owned(), "2024-01-01T00:00:00Z".to_owned()])
151 );
152 let back = serde_json::to_string(&cap).expect("serialize");
153 assert_eq!(back, json);
154 }
155
156 /// Oracle: RFC 8621 §1.3.2 requires the session-level value to be `{}`.
157 #[test]
158 fn submission_capability_session_level_is_empty_object() {
159 let cap = SubmissionCapability::default();
160 let json = serde_json::to_string(&cap).expect("serialize");
161 assert_eq!(json, "{}");
162 }
163
164 /// Oracle: RFC 8621 §1.3.3 requires the value to be `{}` at both
165 /// session-level and account-level (the same struct serves both).
166 #[test]
167 fn vacation_response_capability_is_empty_object() {
168 let cap = VacationResponseCapability::default();
169 let json = serde_json::to_string(&cap).expect("serialize");
170 assert_eq!(json, "{}");
171 let back: VacationResponseCapability = serde_json::from_str("{}").expect("must parse");
172 assert_eq!(back, cap);
173 }
174
175 /// Oracle: capability URI constants match the RFC 8621 §1.3 IANA
176 /// registrations verbatim.
177 #[test]
178 fn capability_uri_constants_match_rfc8621() {
179 assert_eq!(JMAP_MAIL_URI, "urn:ietf:params:jmap:mail");
180 assert_eq!(JMAP_SUBMISSION_URI, "urn:ietf:params:jmap:submission");
181 assert_eq!(
182 JMAP_VACATIONRESPONSE_URI,
183 "urn:ietf:params:jmap:vacationresponse"
184 );
185 }
186}