Skip to main content

io_email/flag/imap/
store.rs

1//! IMAP flag-store coroutine: optional SELECT then UID STORE (RFC
2//! 3501 ยง6.4.6).
3//!
4//! `auto_select = false` skips SELECT for sync engines that
5//! pre-select per batch. Shared by add/set/delete via [`FlagOp`].
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! use io_email::{flag::FlagOp, flag::imap::store::ImapFlagStore};
11//!
12//! client.run(ImapFlagStore::new("INBOX", &["12"], &flags, FlagOp::Add, true)?)?;
13//! ```
14
15use alloc::{string::String, vec::Vec};
16use core::mem;
17
18use io_imap::{
19    codec::fragmentizer::Fragmentizer,
20    coroutine::{ImapCoroutine, ImapCoroutineState, ImapYield},
21    rfc3501::{
22        select::{ImapMailboxSelect, ImapMailboxSelectError, ImapMailboxSelectOptions},
23        store::{ImapMessageStore, ImapMessageStoreError, ImapMessageStoreOptions},
24    },
25    types::flag::StoreType,
26};
27use log::trace;
28use thiserror::Error;
29
30use crate::{
31    flag::types::{Flag, FlagOp},
32    imap::convert::{InvalidMailboxName, InvalidUidSet, flag_from, parse_mailbox, parse_uids},
33};
34
35/// Errors produced by [`ImapFlagStore`].
36#[derive(Debug, Error)]
37pub enum ImapFlagStoreError {
38    #[error(transparent)]
39    Select(#[from] ImapMailboxSelectError),
40    #[error(transparent)]
41    Store(#[from] ImapMessageStoreError),
42    #[error("invalid IMAP mailbox `{0}`")]
43    InvalidMailbox(String),
44    #[error("empty UID set")]
45    EmptyUidSet,
46    #[error("invalid message UID `{0}`")]
47    InvalidUid(String),
48    #[error("coroutine was resumed after completion")]
49    ResumedAfterDone,
50}
51
52impl From<InvalidMailboxName> for ImapFlagStoreError {
53    fn from(err: InvalidMailboxName) -> Self {
54        Self::InvalidMailbox(err.0)
55    }
56}
57
58impl From<InvalidUidSet> for ImapFlagStoreError {
59    fn from(err: InvalidUidSet) -> Self {
60        match err {
61            InvalidUidSet::Empty => Self::EmptyUidSet,
62            InvalidUidSet::Invalid(s) => Self::InvalidUid(s),
63        }
64    }
65}
66
67/// I/O-free coroutine adding/setting/removing flags on a UID set.
68pub struct ImapFlagStore {
69    state: State,
70}
71
72impl ImapFlagStore {
73    /// `op` picks the STORE variant. When `auto_select` is set the
74    /// mailbox is SELECTed first.
75    pub fn new(
76        mailbox: &str,
77        ids: &[&str],
78        flags: &[Flag],
79        op: FlagOp,
80        auto_select: bool,
81    ) -> Result<Self, ImapFlagStoreError> {
82        trace!("prepare IMAP flag store (auto_select={auto_select} op={op:?})");
83        let mbox = parse_mailbox(mailbox)?;
84        let sequence_set = parse_uids(ids)?;
85        let imap_flags: Vec<_> = flags.iter().map(flag_from).collect();
86        let kind = match op {
87            FlagOp::Add => StoreType::Add,
88            FlagOp::Set => StoreType::Replace,
89            FlagOp::Remove => StoreType::Remove,
90        };
91
92        let store = ImapMessageStore::new(
93            sequence_set,
94            kind,
95            imap_flags,
96            ImapMessageStoreOptions { uid: true },
97        );
98        let state = if auto_select {
99            State::Selecting {
100                select: ImapMailboxSelect::new(mbox, ImapMailboxSelectOptions::default()),
101                store,
102            }
103        } else {
104            State::Storing(store)
105        };
106        Ok(Self { state })
107    }
108}
109
110// NOTE: Selecting carries both coroutines; the size delta vs Storing
111// is bounded by ImapMailboxSelect. Boxing isn't worth it for
112// short-lived per-op state machines.
113#[allow(clippy::large_enum_variant)]
114enum State {
115    Selecting {
116        select: ImapMailboxSelect,
117        store: ImapMessageStore,
118    },
119    Storing(ImapMessageStore),
120    Done,
121}
122
123impl ImapCoroutine for ImapFlagStore {
124    type Yield = ImapYield;
125    type Return = Result<(), ImapFlagStoreError>;
126
127    fn resume(
128        &mut self,
129        fragmentizer: &mut Fragmentizer,
130        mut bytes: Option<&[u8]>,
131    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
132        loop {
133            match mem::replace(&mut self.state, State::Done) {
134                State::Selecting { mut select, store } => {
135                    match select.resume(fragmentizer, bytes.take()) {
136                        ImapCoroutineState::Yielded(yielded) => {
137                            self.state = State::Selecting { select, store };
138                            return ImapCoroutineState::Yielded(yielded);
139                        }
140                        ImapCoroutineState::Complete(Ok(_)) => {
141                            self.state = State::Storing(store);
142                        }
143                        ImapCoroutineState::Complete(Err(err)) => {
144                            return ImapCoroutineState::Complete(Err(err.into()));
145                        }
146                    }
147                }
148                State::Storing(mut store) => match store.resume(fragmentizer, bytes.take()) {
149                    ImapCoroutineState::Yielded(yielded) => {
150                        self.state = State::Storing(store);
151                        return ImapCoroutineState::Yielded(yielded);
152                    }
153                    ImapCoroutineState::Complete(Ok(_)) => {
154                        return ImapCoroutineState::Complete(Ok(()));
155                    }
156                    ImapCoroutineState::Complete(Err(err)) => {
157                        return ImapCoroutineState::Complete(Err(err.into()));
158                    }
159                },
160                State::Done => {
161                    return ImapCoroutineState::Complete(Err(ImapFlagStoreError::ResumedAfterDone));
162                }
163            }
164        }
165    }
166}