Skip to main content

io_email/flag/jmap/
store.rs

1//! JMAP flag-store coroutine wrapping Email/set with a per-id keyword
2//! patch (RFC 8621 ยง4.7).
3//!
4//! Add/Remove emit per-keyword patches; Set replaces the full bag.
5//! Mailbox is part of the shared signature but unused: JMAP keywords
6//! are global per email.
7//!
8//! # Example
9//!
10//! ```rust,ignore
11//! use io_email::{flag::FlagOp, flag::jmap::store::JmapFlagStore};
12//!
13//! client.run(JmapFlagStore::new(&session, &auth, "_", &["email-id"], &flags, FlagOp::Add)?)?;
14//! ```
15
16use alloc::{collections::BTreeMap, string::String};
17
18use io_jmap::{
19    coroutine::{JmapCoroutine, JmapCoroutineState, JmapYield},
20    rfc8620::JmapSession,
21    rfc8621::email::set::{
22        JmapEmailSet as InnerSet, JmapEmailSetArgs, JmapEmailSetError as InnerErr,
23    },
24};
25use log::trace;
26use secrecy::SecretString;
27use thiserror::Error;
28
29use crate::{
30    flag::types::{Flag, FlagOp},
31    jmap::convert::keyword_from,
32};
33
34/// Errors produced by [`JmapFlagStore`].
35#[derive(Debug, Error)]
36pub enum JmapFlagStoreError {
37    #[error(transparent)]
38    Set(#[from] InnerErr),
39    #[error("Email/set returned per-id failures: {0:?}")]
40    NotUpdated(alloc::vec::Vec<String>),
41}
42
43/// I/O-free coroutine applying a flag store across every id.
44pub struct JmapFlagStore {
45    inner: InnerSet,
46}
47
48impl JmapFlagStore {
49    pub fn new(
50        session: &JmapSession,
51        http_auth: &SecretString,
52        _mailbox: &str,
53        ids: &[&str],
54        flags: &[Flag],
55        op: FlagOp,
56    ) -> Result<Self, JmapFlagStoreError> {
57        trace!("prepare JMAP flag store ({op:?})");
58        let mut args = JmapEmailSetArgs::default();
59        for id in ids {
60            match op {
61                FlagOp::Add => {
62                    for flag in flags {
63                        args.set_keyword(*id, keyword_from(flag));
64                    }
65                }
66                FlagOp::Remove => {
67                    for flag in flags {
68                        args.unset_keyword(*id, keyword_from(flag));
69                    }
70                }
71                FlagOp::Set => {
72                    let bag: BTreeMap<String, bool> =
73                        flags.iter().map(|f| (keyword_from(f), true)).collect();
74                    args.replace_keywords(*id, bag);
75                }
76            }
77        }
78        Ok(Self {
79            inner: InnerSet::new(session, http_auth, args)?,
80        })
81    }
82}
83
84impl JmapCoroutine for JmapFlagStore {
85    type Yield = JmapYield;
86    type Return = Result<(), JmapFlagStoreError>;
87
88    fn resume(&mut self, bytes: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
89        match self.inner.resume(bytes) {
90            JmapCoroutineState::Yielded(y) => JmapCoroutineState::Yielded(y),
91            JmapCoroutineState::Complete(Ok(ok)) => {
92                if ok.not_updated.is_empty() {
93                    JmapCoroutineState::Complete(Ok(()))
94                } else {
95                    JmapCoroutineState::Complete(Err(JmapFlagStoreError::NotUpdated(
96                        ok.not_updated.into_keys().collect(),
97                    )))
98                }
99            }
100            JmapCoroutineState::Complete(Err(err)) => JmapCoroutineState::Complete(Err(err.into())),
101        }
102    }
103}