1use alloc::{collections::BTreeMap, format, string::String, vec, vec::Vec};
59
60use secrecy::SecretString;
61use serde::{Deserialize, Serialize};
62use thiserror::Error;
63
64use crate::{
65 coroutine::*,
66 jmap_try,
67 rfc8620::{JMAP_CORE_CAPABILITY, request::JmapBatch, send::*, session::JmapSession, set::*},
68 rfc8621::{JMAP_MAIL_CAPABILITY, email::JmapEmail},
69};
70
71#[derive(Clone, Debug)]
74pub enum JmapEmailPatchOp {
75 SetKeyword(String),
77 UnsetKeyword(String),
79 ReplaceKeywords(BTreeMap<String, bool>),
81 AddToMailbox(String),
83 RemoveFromMailbox(String),
85 ReplaceMailboxIds(BTreeMap<String, bool>),
87}
88
89#[derive(Clone, Debug, Default)]
93pub struct JmapEmailPatch(pub Vec<JmapEmailPatchOp>);
94
95impl JmapEmailPatch {
96 pub fn set_keyword(mut self, keyword: impl Into<String>) -> Self {
98 self.0.push(JmapEmailPatchOp::SetKeyword(keyword.into()));
99 self
100 }
101
102 pub fn unset_keyword(mut self, keyword: impl Into<String>) -> Self {
104 self.0.push(JmapEmailPatchOp::UnsetKeyword(keyword.into()));
105 self
106 }
107
108 pub fn replace_keywords(mut self, keywords: BTreeMap<String, bool>) -> Self {
110 self.0.push(JmapEmailPatchOp::ReplaceKeywords(keywords));
111 self
112 }
113
114 pub fn add_to_mailbox(mut self, id: impl Into<String>) -> Self {
116 self.0.push(JmapEmailPatchOp::AddToMailbox(id.into()));
117 self
118 }
119
120 pub fn remove_from_mailbox(mut self, id: impl Into<String>) -> Self {
122 self.0.push(JmapEmailPatchOp::RemoveFromMailbox(id.into()));
123 self
124 }
125
126 pub fn replace_mailbox_ids(mut self, ids: BTreeMap<String, bool>) -> Self {
128 self.0.push(JmapEmailPatchOp::ReplaceMailboxIds(ids));
129 self
130 }
131}
132
133impl Serialize for JmapEmailPatch {
134 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
135 use serde::ser::SerializeMap;
136 let mut map = s.serialize_map(Some(self.0.len()))?;
137 for op in &self.0 {
138 match op {
139 JmapEmailPatchOp::SetKeyword(kw) => {
140 map.serialize_entry(&format!("keywords/{kw}"), &true)?
141 }
142 JmapEmailPatchOp::UnsetKeyword(kw) => {
143 map.serialize_entry(&format!("keywords/{kw}"), &Option::<bool>::None)?
144 }
145 JmapEmailPatchOp::ReplaceKeywords(kws) => map.serialize_entry("keywords", kws)?,
146 JmapEmailPatchOp::AddToMailbox(id) => {
147 map.serialize_entry(&format!("mailboxIds/{id}"), &true)?
148 }
149 JmapEmailPatchOp::RemoveFromMailbox(id) => {
150 map.serialize_entry(&format!("mailboxIds/{id}"), &Option::<bool>::None)?
151 }
152 JmapEmailPatchOp::ReplaceMailboxIds(ids) => {
153 map.serialize_entry("mailboxIds", ids)?
154 }
155 }
156 }
157 map.end()
158 }
159}
160
161#[derive(Clone, Debug, Deserialize)]
163#[serde(tag = "type", rename_all = "camelCase")]
164pub enum JmapEmailSetItemError {
165 TooManyKeywords {
167 description: Option<String>,
169 },
170 TooManyMailboxes {
172 description: Option<String>,
174 },
175 BlobNotFound {
177 description: Option<String>,
179 },
180 NotFound {
182 description: Option<String>,
184 },
185 InvalidPatch {
187 description: Option<String>,
189 },
190 WillDestroy {
193 description: Option<String>,
195 },
196 InvalidProperties {
198 description: Option<String>,
200 #[serde(default)]
202 properties: Vec<String>,
203 },
204 Singleton {
207 description: Option<String>,
209 },
210 #[serde(other)]
212 Unknown,
213}
214
215#[derive(Debug, Error)]
217pub enum JmapEmailSetError {
218 #[error("JMAP Email/set failed: {0}")]
220 Send(#[from] JmapSendError),
221 #[error("JMAP Email/set failed: serialize args: {0}")]
223 SerializeArgs(#[source] serde_json::Error),
224 #[error("JMAP Email/set failed: {0}")]
226 Set(#[from] JmapSetError),
227}
228
229#[derive(Clone, Debug, Default, Serialize)]
231#[serde(rename_all = "camelCase")]
232pub struct JmapEmailSetArgs {
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub create: Option<BTreeMap<String, JmapEmail>>,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub update: Option<BTreeMap<String, JmapEmailPatch>>,
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub destroy: Option<Vec<String>>,
242}
243
244impl JmapEmailSetArgs {
245 pub fn create(&mut self, client_id: impl Into<String>, email: JmapEmail) -> &mut Self {
247 self.create
248 .get_or_insert_with(Default::default)
249 .insert(client_id.into(), email);
250 self
251 }
252
253 pub fn destroy(&mut self, id: impl Into<String>) -> &mut Self {
255 self.destroy
256 .get_or_insert_with(Default::default)
257 .push(id.into());
258 self
259 }
260
261 pub fn set_keyword(&mut self, id: impl Into<String>, keyword: impl Into<String>) -> &mut Self {
263 self.patch(id)
264 .0
265 .push(JmapEmailPatchOp::SetKeyword(keyword.into()));
266 self
267 }
268
269 pub fn unset_keyword(
271 &mut self,
272 id: impl Into<String>,
273 keyword: impl Into<String>,
274 ) -> &mut Self {
275 self.patch(id)
276 .0
277 .push(JmapEmailPatchOp::UnsetKeyword(keyword.into()));
278 self
279 }
280
281 pub fn replace_keywords(
283 &mut self,
284 id: impl Into<String>,
285 keywords: BTreeMap<String, bool>,
286 ) -> &mut Self {
287 self.patch(id)
288 .0
289 .push(JmapEmailPatchOp::ReplaceKeywords(keywords));
290 self
291 }
292
293 pub fn add_to_mailbox(
295 &mut self,
296 id: impl Into<String>,
297 mailbox_id: impl Into<String>,
298 ) -> &mut Self {
299 self.patch(id)
300 .0
301 .push(JmapEmailPatchOp::AddToMailbox(mailbox_id.into()));
302 self
303 }
304
305 pub fn remove_from_mailbox(
307 &mut self,
308 id: impl Into<String>,
309 mailbox_id: impl Into<String>,
310 ) -> &mut Self {
311 self.patch(id)
312 .0
313 .push(JmapEmailPatchOp::RemoveFromMailbox(mailbox_id.into()));
314 self
315 }
316
317 pub fn replace_mailbox_ids(
320 &mut self,
321 id: impl Into<String>,
322 ids: BTreeMap<String, bool>,
323 ) -> &mut Self {
324 self.patch(id)
325 .0
326 .push(JmapEmailPatchOp::ReplaceMailboxIds(ids));
327 self
328 }
329
330 fn patch(&mut self, id: impl Into<String>) -> &mut JmapEmailPatch {
331 self.update
332 .get_or_insert_with(Default::default)
333 .entry(id.into())
334 .or_default()
335 }
336}
337
338#[derive(Clone, Debug)]
340pub struct JmapEmailSetOutput {
341 pub new_state: String,
343 pub created: BTreeMap<String, JmapEmail>,
345 pub updated: BTreeMap<String, Option<JmapEmail>>,
347 pub destroyed: Vec<String>,
349 pub not_created: BTreeMap<String, JmapEmailSetItemError>,
351 pub not_updated: BTreeMap<String, JmapEmailSetItemError>,
353 pub not_destroyed: BTreeMap<String, JmapEmailSetItemError>,
355 pub keep_alive: bool,
357}
358
359pub struct JmapEmailSet {
361 state: State,
362}
363
364impl JmapEmailSet {
365 pub fn new(
367 session: &JmapSession,
368 http_auth: &SecretString,
369 args: JmapEmailSetArgs,
370 ) -> Result<Self, JmapEmailSetError> {
371 let account_id = session
372 .primary_accounts
373 .get(JMAP_MAIL_CAPABILITY)
374 .cloned()
375 .unwrap_or_default();
376 let api_url = &session.api_url;
377
378 let json_args = serde_json::to_value(EmailSetRequest { account_id, args })
379 .map_err(JmapEmailSetError::SerializeArgs)?;
380
381 let mut batch = JmapBatch::new();
382 batch.add("Email/set", json_args);
383 let request = batch.into_request(vec![
384 JMAP_CORE_CAPABILITY.into(),
385 JMAP_MAIL_CAPABILITY.into(),
386 ]);
387
388 let send = JmapSend::new(http_auth, api_url, request)?;
389 Ok(Self {
390 state: State::Set(JmapSet::from_send(send)),
391 })
392 }
393}
394
395impl JmapCoroutine for JmapEmailSet {
396 type Yield = JmapYield;
397 type Return = Result<JmapEmailSetOutput, JmapEmailSetError>;
398
399 fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
400 match &mut self.state {
401 State::Set(set) => {
402 let JmapSetOutput {
403 new_state,
404 created,
405 updated,
406 destroyed,
407 not_created,
408 not_updated,
409 not_destroyed,
410 keep_alive,
411 } = jmap_try!(set, arg);
412 let parse = |map: BTreeMap<String, serde_json::Value>| {
413 map.into_iter()
414 .map(|(k, v)| {
415 let e =
416 serde_json::from_value(v).unwrap_or(JmapEmailSetItemError::Unknown);
417 (k, e)
418 })
419 .collect()
420 };
421 JmapCoroutineState::Complete(Ok(JmapEmailSetOutput {
422 new_state,
423 created,
424 updated,
425 destroyed,
426 not_created: parse(not_created),
427 not_updated: parse(not_updated),
428 not_destroyed: parse(not_destroyed),
429 keep_alive,
430 }))
431 }
432 }
433 }
434}
435
436enum State {
437 Set(JmapSet<JmapEmail>),
438}
439
440#[derive(Serialize)]
441struct EmailSetRequest {
442 #[serde(rename = "accountId")]
443 account_id: String,
444 #[serde(flatten)]
445 args: JmapEmailSetArgs,
446}