Skip to main content

io_imap/rfc3501/
store.rs

1//! IMAP STORE coroutines: echo ([`ImapMessageStore`]) and silent
2//! ([`ImapMessageStoreSilent`]) variants.
3//!
4//! # Examples
5//!
6//! Echo variant (server returns updated FETCH items per message):
7//!
8//! ```rust,no_run
9//! use std::{
10//!     io::{Read, Write},
11//!     net::TcpStream,
12//! };
13//!
14//! use io_imap::{
15//!     codec::fragmentizer::Fragmentizer,
16//!     coroutine::{ImapCoroutine, ImapCoroutineState, ImapYield},
17//!     rfc3501::store::{ImapMessageStore, ImapMessageStoreOptions},
18//!     types::flag::{Flag, StoreType},
19//! };
20//!
21//! // Ready stream needed (TCP-connected, TLS-negociated, IMAP-authenticated)
22//! let mut stream = TcpStream::connect("localhost:143").unwrap();
23//!
24//! let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
25//! let mut buf = [0u8; 4096];
26//!
27//! let sequence_set = "1:3".try_into().unwrap();
28//! let kind = StoreType::Add;
29//! let flags = vec![Flag::Seen];
30//! let opts = ImapMessageStoreOptions::default();
31//! let mut coroutine = ImapMessageStore::new(sequence_set, kind, flags, opts);
32//! let mut arg = None;
33//!
34//! let updated = loop {
35//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
36//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
37//!             stream.write_all(&bytes).unwrap();
38//!         }
39//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
40//!             let n = stream.read(&mut buf).unwrap();
41//!             arg = Some(&buf[..n]);
42//!         }
43//!         ImapCoroutineState::Complete(Ok(updated)) => break updated,
44//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
45//!     }
46//! };
47//!
48//! println!("{updated:?}");
49//! ```
50//!
51//! Silent variant (`STORE.SILENT`, no FETCH echoes):
52//!
53//! ```rust,no_run
54//! use std::{
55//!     io::{Read, Write},
56//!     net::TcpStream,
57//! };
58//!
59//! use io_imap::{
60//!     codec::fragmentizer::Fragmentizer,
61//!     coroutine::{ImapCoroutine, ImapCoroutineState, ImapYield},
62//!     rfc3501::store::{ImapMessageStoreOptions, ImapMessageStoreSilent},
63//!     types::flag::{Flag, StoreType},
64//! };
65//!
66//! // Ready stream needed (TCP-connected, TLS-negociated, IMAP-authenticated)
67//! let mut stream = TcpStream::connect("localhost:143").unwrap();
68//!
69//! let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
70//! let mut buf = [0u8; 4096];
71//!
72//! let sequence_set = "1:3".try_into().unwrap();
73//! let kind = StoreType::Add;
74//! let flags = vec![Flag::Seen];
75//! let opts = ImapMessageStoreOptions::default();
76//! let mut coroutine = ImapMessageStoreSilent::new(sequence_set, kind, flags, opts);
77//! let mut arg = None;
78//!
79//! loop {
80//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
81//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
82//!             stream.write_all(&bytes).unwrap();
83//!         }
84//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
85//!             let n = stream.read(&mut buf).unwrap();
86//!             arg = Some(&buf[..n]);
87//!         }
88//!         ImapCoroutineState::Complete(Ok(())) => break,
89//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
90//!     }
91//! }
92//! ```
93
94use core::{fmt, num::NonZeroU32};
95
96use alloc::{collections::BTreeMap, string::String, string::ToString, vec::Vec};
97
98use imap_codec::{
99    CommandCodec,
100    fragmentizer::Fragmentizer,
101    imap_types::{
102        command::{Command, CommandBody},
103        core::{TagGenerator, Vec1},
104        fetch::MessageDataItem,
105        flag::{Flag, StoreResponse, StoreType},
106        response::{Data, StatusKind, Tagged},
107        sequence::SequenceSet,
108    },
109};
110use log::trace;
111use thiserror::Error;
112
113use crate::{coroutine::*, imap_try, send::*};
114
115/// Failure causes during the IMAP STORE flow.
116#[derive(Clone, Debug, Error)]
117pub enum ImapMessageStoreError {
118    #[error("IMAP STORE failed: NO {0}")]
119    No(String),
120    #[error("IMAP STORE failed: BAD {0}")]
121    Bad(String),
122    #[error("IMAP STORE failed: BYE {0}")]
123    Bye(String),
124
125    #[error("IMAP STORE failed: server did not return a tagged response")]
126    MissingTagged,
127
128    #[error("IMAP STORE failed: {0}")]
129    Send(#[from] SendImapCommandError),
130}
131
132/// Options for the IMAP STORE coroutines.
133#[derive(Clone, Debug, Default, Eq, PartialEq)]
134pub struct ImapMessageStoreOptions {
135    /// When `true`, send `UID STORE` and treat `sequence_set` as UIDs.
136    pub uid: bool,
137}
138
139/// Echo variant: server returns FETCH for each modified message.
140pub struct ImapMessageStore {
141    state: State,
142}
143
144impl ImapMessageStore {
145    pub fn new(
146        sequence_set: SequenceSet,
147        kind: StoreType,
148        flags: Vec<Flag<'static>>,
149        opts: ImapMessageStoreOptions,
150    ) -> Self {
151        let command = Command {
152            tag: TagGenerator::new().generate(),
153            body: CommandBody::Store {
154                modifiers: Default::default(),
155                sequence_set,
156                kind,
157                response: StoreResponse::Answer,
158                flags,
159                uid: opts.uid,
160            },
161        };
162
163        trace!("send IMAP command {command:?}");
164
165        let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
166
167        Self { state }
168    }
169}
170
171impl ImapCoroutine for ImapMessageStore {
172    type Yield = ImapYield;
173    type Return =
174        Result<BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>>, ImapMessageStoreError>;
175
176    fn resume(
177        &mut self,
178        fragmentizer: &mut Fragmentizer,
179        arg: Option<&[u8]>,
180    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
181        loop {
182            trace!("store: {}", self.state);
183
184            match &mut self.state {
185                State::Send(send) => {
186                    let out = imap_try!(send, fragmentizer, arg);
187
188                    if let Some(bye) = out.bye {
189                        let err = ImapMessageStoreError::Bye(bye.text.to_string());
190                        return ImapCoroutineState::Complete(Err(err));
191                    }
192
193                    let Some(Tagged { body, .. }) = out.tagged else {
194                        let err = ImapMessageStoreError::MissingTagged;
195                        return ImapCoroutineState::Complete(Err(err));
196                    };
197
198                    let mut data: BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>> =
199                        BTreeMap::new();
200                    for res in out.data {
201                        if let Data::Fetch { seq, items } = res {
202                            data.insert(seq, items);
203                        }
204                    }
205
206                    return match body.kind {
207                        StatusKind::Ok => ImapCoroutineState::Complete(Ok(data)),
208                        StatusKind::No => {
209                            let err = ImapMessageStoreError::No(body.text.to_string());
210                            ImapCoroutineState::Complete(Err(err))
211                        }
212                        StatusKind::Bad => {
213                            let err = ImapMessageStoreError::Bad(body.text.to_string());
214                            ImapCoroutineState::Complete(Err(err))
215                        }
216                    };
217                }
218            }
219        }
220    }
221}
222
223/// Silent variant: server suppresses the FETCH echoes.
224pub struct ImapMessageStoreSilent {
225    state: State,
226}
227
228impl ImapMessageStoreSilent {
229    pub fn new(
230        sequence_set: SequenceSet,
231        kind: StoreType,
232        flags: Vec<Flag<'static>>,
233        opts: ImapMessageStoreOptions,
234    ) -> Self {
235        let command = Command {
236            tag: TagGenerator::new().generate(),
237            body: CommandBody::Store {
238                modifiers: Default::default(),
239                sequence_set,
240                kind,
241                response: StoreResponse::Silent,
242                flags,
243                uid: opts.uid,
244            },
245        };
246
247        trace!("send IMAP command {command:?}");
248
249        let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
250
251        Self { state }
252    }
253}
254
255impl ImapCoroutine for ImapMessageStoreSilent {
256    type Yield = ImapYield;
257    type Return = Result<(), ImapMessageStoreError>;
258
259    fn resume(
260        &mut self,
261        fragmentizer: &mut Fragmentizer,
262        arg: Option<&[u8]>,
263    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
264        loop {
265            trace!("store silent: {}", self.state);
266
267            match &mut self.state {
268                State::Send(send) => {
269                    let out = imap_try!(send, fragmentizer, arg);
270
271                    if let Some(bye) = out.bye {
272                        let err = ImapMessageStoreError::Bye(bye.text.to_string());
273                        return ImapCoroutineState::Complete(Err(err));
274                    }
275
276                    let Some(Tagged { body, .. }) = out.tagged else {
277                        let err = ImapMessageStoreError::MissingTagged;
278                        return ImapCoroutineState::Complete(Err(err));
279                    };
280
281                    return match body.kind {
282                        StatusKind::Ok => ImapCoroutineState::Complete(Ok(())),
283                        StatusKind::No => {
284                            let err = ImapMessageStoreError::No(body.text.to_string());
285                            ImapCoroutineState::Complete(Err(err))
286                        }
287                        StatusKind::Bad => {
288                            let err = ImapMessageStoreError::Bad(body.text.to_string());
289                            ImapCoroutineState::Complete(Err(err))
290                        }
291                    };
292                }
293            }
294        }
295    }
296}
297
298enum State {
299    Send(SendImapCommand<CommandCodec>),
300}
301
302impl fmt::Display for State {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        match self {
305            Self::Send(_) => f.write_str("send store"),
306        }
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use core::str;
313
314    use alloc::{borrow::ToOwned, vec, vec::Vec};
315
316    use super::*;
317
318    fn flags() -> Vec<Flag<'static>> {
319        vec![Flag::Seen]
320    }
321
322    #[test]
323    fn echo_success_returns_map() {
324        let mut store = ImapMessageStore::new(
325            "1".try_into().expect("valid sequence set"),
326            StoreType::Add,
327            flags(),
328            ImapMessageStoreOptions::default(),
329        );
330        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
331
332        let bytes = expect_wants_write_echo(&mut store, &mut frag, None);
333        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
334
335        expect_wants_read_echo(&mut store, &mut frag);
336
337        let reply = format!("* 1 FETCH (FLAGS (\\Seen))\r\n{tag} OK STORE completed\r\n");
338        let map = expect_complete_ok_echo(&mut store, &mut frag, reply.as_bytes());
339        assert_eq!(1, map.len());
340    }
341
342    #[test]
343    fn echo_uid_variant_sends_uid_store() {
344        let mut store = ImapMessageStore::new(
345            "42".try_into().expect("valid sequence set"),
346            StoreType::Add,
347            flags(),
348            ImapMessageStoreOptions { uid: true },
349        );
350        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
351
352        let bytes = expect_wants_write_echo(&mut store, &mut frag, None);
353        let line = str::from_utf8(&bytes).expect("utf8 command");
354        assert!(line.contains("UID STORE 42 "));
355    }
356
357    #[test]
358    fn silent_success_returns_ok() {
359        let mut store = ImapMessageStoreSilent::new(
360            "1".try_into().expect("valid sequence set"),
361            StoreType::Add,
362            flags(),
363            ImapMessageStoreOptions::default(),
364        );
365        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
366
367        let bytes = expect_wants_write_silent(&mut store, &mut frag, None);
368        let line = str::from_utf8(&bytes).expect("utf8 command");
369        let tag = first_word(line).to_owned();
370        assert!(line.contains("STORE 1 +FLAGS.SILENT "));
371
372        expect_wants_read_silent(&mut store, &mut frag);
373
374        let reply = format!("{tag} OK STORE completed\r\n");
375        expect_complete_ok_silent(&mut store, &mut frag, reply.as_bytes());
376    }
377
378    #[test]
379    fn echo_tagged_no_returns_no_error() {
380        let mut store = ImapMessageStore::new(
381            "1".try_into().expect("valid sequence set"),
382            StoreType::Add,
383            flags(),
384            ImapMessageStoreOptions::default(),
385        );
386        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
387
388        let bytes = expect_wants_write_echo(&mut store, &mut frag, None);
389        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
390
391        expect_wants_read_echo(&mut store, &mut frag);
392
393        let reply = format!("{tag} NO mailbox is read-only\r\n");
394        let err = expect_complete_err_echo(&mut store, &mut frag, reply.as_bytes());
395        let ImapMessageStoreError::No(text) = err else {
396            panic!("expected ImapMessageStoreError::No, got {err:?}");
397        };
398        assert_eq!(text, "mailbox is read-only");
399    }
400
401    // --- utils
402
403    fn expect_wants_write_echo(
404        cor: &mut ImapMessageStore,
405        frag: &mut Fragmentizer,
406        arg: Option<&[u8]>,
407    ) -> Vec<u8> {
408        match cor.resume(frag, arg) {
409            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
410            state => panic!("expected WantsWrite, got {state:?}"),
411        }
412    }
413
414    fn expect_wants_read_echo(cor: &mut ImapMessageStore, frag: &mut Fragmentizer) {
415        match cor.resume(frag, None) {
416            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
417            state => panic!("expected WantsRead, got {state:?}"),
418        }
419    }
420
421    fn expect_complete_ok_echo(
422        cor: &mut ImapMessageStore,
423        frag: &mut Fragmentizer,
424        reply: &[u8],
425    ) -> BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>> {
426        match cor.resume(frag, Some(reply)) {
427            ImapCoroutineState::Complete(Ok(value)) => value,
428            state => panic!("expected Complete(Ok), got {state:?}"),
429        }
430    }
431
432    fn expect_complete_err_echo(
433        cor: &mut ImapMessageStore,
434        frag: &mut Fragmentizer,
435        reply: &[u8],
436    ) -> ImapMessageStoreError {
437        match cor.resume(frag, Some(reply)) {
438            ImapCoroutineState::Complete(Err(err)) => err,
439            state => panic!("expected Complete(Err), got {state:?}"),
440        }
441    }
442
443    fn expect_wants_write_silent(
444        cor: &mut ImapMessageStoreSilent,
445        frag: &mut Fragmentizer,
446        arg: Option<&[u8]>,
447    ) -> Vec<u8> {
448        match cor.resume(frag, arg) {
449            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
450            state => panic!("expected WantsWrite, got {state:?}"),
451        }
452    }
453
454    fn expect_wants_read_silent(cor: &mut ImapMessageStoreSilent, frag: &mut Fragmentizer) {
455        match cor.resume(frag, None) {
456            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
457            state => panic!("expected WantsRead, got {state:?}"),
458        }
459    }
460
461    fn expect_complete_ok_silent(
462        cor: &mut ImapMessageStoreSilent,
463        frag: &mut Fragmentizer,
464        reply: &[u8],
465    ) {
466        match cor.resume(frag, Some(reply)) {
467            ImapCoroutineState::Complete(Ok(())) => {}
468            state => panic!("expected Complete(Ok), got {state:?}"),
469        }
470    }
471
472    fn first_word(line: &str) -> &str {
473        line.split_whitespace()
474            .next()
475            .expect("first whitespace-separated token")
476    }
477}