io_jmap/rfc8621/mailbox/set.rs
1//! JMAP `Mailbox/set` coroutine (RFC 8621 §2.6): wraps the generic [`JmapSet`]
2//! with [`JmapMailboxSetArgs`] (create/update/destroy) and decodes per-object
3//! [`JmapMailboxSetItemError`] payloads.
4//!
5//! # Example
6//!
7//! ```rust,no_run
8//! use std::{
9//! io::{Read, Write},
10//! net::TcpStream,
11//! };
12//!
13//! use io_jmap::{
14//! coroutine::{JmapCoroutine, JmapCoroutineState, JmapYield},
15//! rfc8620::session::JmapSession,
16//! rfc8621::mailbox::set::{JmapMailboxSet, JmapMailboxSetArgs},
17//! };
18//! use secrecy::SecretString;
19//!
20//! // Ready stream needed (TCP-connected, TLS-negociated)
21//! let mut stream = TcpStream::connect("api.example.com:443").unwrap();
22//! let mut buf = [0u8; 4096];
23//!
24//! let session: JmapSession = serde_json::from_str(r#"{
25//! "username": "",
26//! "accounts": {},
27//! "primaryAccounts": {"urn:ietf:params:jmap:mail": "a1"},
28//! "capabilities": {},
29//! "apiUrl": "https://api.example.com/jmap/",
30//! "downloadUrl": "",
31//! "uploadUrl": "",
32//! "eventSourceUrl": "",
33//! "state": ""
34//! }"#).unwrap();
35//! let auth = SecretString::from("Bearer xyz");
36//! let mut coroutine =
37//! JmapMailboxSet::new(&session, &auth, JmapMailboxSetArgs::default()).unwrap();
38//! let mut arg = None;
39//!
40//! let out = loop {
41//! match coroutine.resume(arg.take()) {
42//! JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => {
43//! stream.write_all(&bytes).unwrap();
44//! }
45//! JmapCoroutineState::Yielded(JmapYield::WantsRead) => {
46//! let n = stream.read(&mut buf).unwrap();
47//! arg = Some(&buf[..n]);
48//! }
49//! JmapCoroutineState::Complete(Ok(out)) => break out,
50//! JmapCoroutineState::Complete(Err(err)) => panic!("{err}"),
51//! }
52//! };
53//!
54//! println!("new state {}", out.new_state);
55//! ```
56
57use alloc::{collections::BTreeMap, string::String, vec, vec::Vec};
58
59use secrecy::SecretString;
60use serde::{Deserialize, Serialize};
61use thiserror::Error;
62
63use crate::{
64 coroutine::*,
65 jmap_try,
66 rfc8620::{JMAP_CORE_CAPABILITY, request::JmapBatch, send::*, session::JmapSession, set::*},
67 rfc8621::{
68 JMAP_MAIL_CAPABILITY,
69 mailbox::{JmapMailbox, JmapMailboxRole},
70 },
71};
72
73/// Client-settable subset of [`JmapMailbox`] for `Mailbox/set` create requests
74/// (RFC 8621 §2.1). Server-assigned fields are excluded.
75#[derive(Clone, Debug, Default, Serialize)]
76#[serde(rename_all = "camelCase")]
77pub struct JmapMailboxCreate {
78 /// The user-visible mailbox name.
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub name: Option<String>,
81 /// The parent mailbox id; `None` for a top-level mailbox.
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub parent_id: Option<String>,
84 /// The special-use role of the mailbox.
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub role: Option<JmapMailboxRole>,
87 /// Position hint for display ordering (lower first).
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub sort_order: Option<u32>,
90 /// Whether the user is subscribed to the mailbox.
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub is_subscribed: Option<bool>,
93}
94
95/// Patch object for `Mailbox/set` update requests (RFC 8620 §5.3): only
96/// `Some` fields are serialised.
97#[derive(Clone, Debug, Default, Serialize)]
98#[serde(rename_all = "camelCase")]
99pub struct JmapMailboxUpdate {
100 /// The user-visible mailbox name.
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub name: Option<String>,
103 /// The parent mailbox id; `None` for a top-level mailbox.
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub parent_id: Option<String>,
106 /// The special-use role of the mailbox.
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub role: Option<JmapMailboxRole>,
109 /// Position hint for display ordering (lower first).
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub sort_order: Option<u32>,
112 /// Whether the user is subscribed to the mailbox.
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub is_subscribed: Option<bool>,
115}
116
117/// Per-object error returned in `Mailbox/set` responses (RFC 8621 §2.6).
118///
119/// Covers the standard RFC 8620 §5.3 set errors plus the mailbox-specific
120/// errors defined in RFC 8621 §2.6.
121#[derive(Clone, Debug, Deserialize)]
122#[serde(tag = "type", rename_all = "camelCase")]
123pub enum JmapMailboxSetItemError {
124 /// The mailbox cannot be destroyed because it has child mailboxes.
125 MailboxHasChild {
126 /// Optional human-readable detail.
127 description: Option<String>,
128 },
129 /// The mailbox cannot be destroyed because it contains email.
130 MailboxHasEmail {
131 /// Optional human-readable detail.
132 description: Option<String>,
133 },
134 /// The referenced object does not exist.
135 NotFound {
136 /// Optional human-readable detail.
137 description: Option<String>,
138 },
139 /// The update patch is invalid.
140 InvalidPatch {
141 /// Optional human-readable detail.
142 description: Option<String>,
143 },
144 /// The object will be destroyed by this request, so it cannot be updated.
145 WillDestroy {
146 /// Optional human-readable detail.
147 description: Option<String>,
148 },
149 /// One or more object properties are invalid.
150 InvalidProperties {
151 /// Optional human-readable detail.
152 description: Option<String>,
153 /// The invalid property names.
154 #[serde(default)]
155 properties: Vec<String>,
156 },
157 /// The type is a singleton, objects cannot be created or destroyed.
158 Singleton {
159 /// Optional human-readable detail.
160 description: Option<String>,
161 },
162 /// Any error type this library does not know about.
163 #[serde(other)]
164 Unknown,
165}
166
167/// Failure causes during a JMAP `Mailbox/set` flow.
168#[derive(Debug, Error)]
169pub enum JmapMailboxSetError {
170 /// The inner send coroutine failed.
171 #[error("JMAP Mailbox/set failed: {0}")]
172 Send(#[from] JmapSendError),
173 /// The method arguments could not be serialized.
174 #[error("JMAP Mailbox/set failed: serialize args: {0}")]
175 SerializeArgs(#[source] serde_json::Error),
176 /// The inner generic set coroutine failed.
177 #[error("JMAP Mailbox/set failed: {0}")]
178 Set(#[from] JmapSetError),
179}
180
181/// Arguments for a `Mailbox/set` request.
182#[derive(Clone, Debug, Default, Serialize)]
183#[serde(rename_all = "camelCase")]
184pub struct JmapMailboxSetArgs {
185 /// Objects to create (client ID → partial mailbox object).
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub create: Option<BTreeMap<String, JmapMailboxCreate>>,
188 /// Objects to update (mailbox ID → patch object).
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub update: Option<BTreeMap<String, JmapMailboxUpdate>>,
191 /// IDs of objects to destroy.
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub destroy: Option<Vec<String>>,
194 /// Whether to destroy contained emails when destroying a mailbox.
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub on_destroy_remove_emails: Option<bool>,
197}
198
199/// Successful terminal output of [`JmapMailboxSet`].
200#[derive(Clone, Debug)]
201pub struct JmapMailboxSetOutput {
202 /// The new server state after the call.
203 pub new_state: String,
204 /// The created mailboxes, keyed by client id.
205 pub created: BTreeMap<String, JmapMailbox>,
206 /// The updated mailboxes, keyed by id.
207 pub updated: BTreeMap<String, Option<JmapMailbox>>,
208 /// Ids of the destroyed objects.
209 pub destroyed: Vec<String>,
210 /// The failed creates, keyed by client id.
211 pub not_created: BTreeMap<String, JmapMailboxSetItemError>,
212 /// The failed updates, keyed by id.
213 pub not_updated: BTreeMap<String, JmapMailboxSetItemError>,
214 /// The failed destroys, keyed by id.
215 pub not_destroyed: BTreeMap<String, JmapMailboxSetItemError>,
216 /// Whether the server indicated the connection can be reused.
217 pub keep_alive: bool,
218}
219
220/// I/O-free coroutine for the JMAP `Mailbox/set` method.
221pub struct JmapMailboxSet {
222 state: State,
223}
224
225impl JmapMailboxSet {
226 /// Prepares the method call request and builds the coroutine.
227 pub fn new(
228 session: &JmapSession,
229 http_auth: &SecretString,
230 args: JmapMailboxSetArgs,
231 ) -> Result<Self, JmapMailboxSetError> {
232 let account_id = session
233 .primary_accounts
234 .get(JMAP_MAIL_CAPABILITY)
235 .cloned()
236 .unwrap_or_default();
237 let api_url = &session.api_url;
238
239 let json_args = serde_json::to_value(MailboxSetRequest { account_id, args })
240 .map_err(JmapMailboxSetError::SerializeArgs)?;
241
242 let mut batch = JmapBatch::new();
243 batch.add("Mailbox/set", json_args);
244 let request = batch.into_request(vec![
245 JMAP_CORE_CAPABILITY.into(),
246 JMAP_MAIL_CAPABILITY.into(),
247 ]);
248
249 let send = JmapSend::new(http_auth, api_url, request)?;
250 Ok(Self {
251 state: State::Set(JmapSet::from_send(send)),
252 })
253 }
254}
255
256impl JmapCoroutine for JmapMailboxSet {
257 type Yield = JmapYield;
258 type Return = Result<JmapMailboxSetOutput, JmapMailboxSetError>;
259
260 fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
261 match &mut self.state {
262 State::Set(set) => {
263 let JmapSetOutput {
264 new_state,
265 created,
266 updated,
267 destroyed,
268 not_created,
269 not_updated,
270 not_destroyed,
271 keep_alive,
272 } = jmap_try!(set, arg);
273 let parse = |map: BTreeMap<String, serde_json::Value>| {
274 map.into_iter()
275 .map(|(k, v)| {
276 let e = serde_json::from_value(v)
277 .unwrap_or(JmapMailboxSetItemError::Unknown);
278 (k, e)
279 })
280 .collect()
281 };
282 JmapCoroutineState::Complete(Ok(JmapMailboxSetOutput {
283 new_state,
284 created,
285 updated,
286 destroyed,
287 not_created: parse(not_created),
288 not_updated: parse(not_updated),
289 not_destroyed: parse(not_destroyed),
290 keep_alive,
291 }))
292 }
293 }
294 }
295}
296
297enum State {
298 Set(JmapSet<JmapMailbox>),
299}
300
301#[derive(Serialize)]
302struct MailboxSetRequest {
303 #[serde(rename = "accountId")]
304 account_id: String,
305 #[serde(flatten)]
306 args: JmapMailboxSetArgs,
307}