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-negotiated, 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-negotiated, 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 =
77//!     ImapMessageStoreSilent::new(sequence_set, kind, flags, opts);
78//! let mut arg = None;
79//!
80//! loop {
81//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
82//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
83//!             stream.write_all(&bytes).unwrap();
84//!         }
85//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
86//!             let n = stream.read(&mut buf).unwrap();
87//!             arg = Some(&buf[..n]);
88//!         }
89//!         ImapCoroutineState::Complete(Ok(())) => break,
90//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
91//!     }
92//! }
93//! ```
94
95use core::{fmt, num::NonZeroU32};
96
97use alloc::{collections::BTreeMap, string::String, string::ToString, vec::Vec};
98
99use imap_codec::{
100    CommandCodec,
101    fragmentizer::Fragmentizer,
102    imap_types::{
103        command::{Command, CommandBody},
104        core::{TagGenerator, Vec1},
105        fetch::MessageDataItem,
106        flag::{Flag, StoreResponse, StoreType},
107        response::{Data, StatusKind, Tagged},
108        sequence::SequenceSet,
109    },
110};
111use log::trace;
112use thiserror::Error;
113
114use crate::{coroutine::*, imap_try, send::*};
115
116/// Failure causes during the IMAP STORE flow.
117#[derive(Clone, Debug, Error)]
118pub enum ImapMessageStoreError {
119    /// The server rejected the command with a NO response.
120    #[error("IMAP STORE failed: NO {0}")]
121    No(String),
122    /// The server rejected the command with a BAD response.
123    #[error("IMAP STORE failed: BAD {0}")]
124    Bad(String),
125    /// The server closed the session with an untagged BYE.
126    #[error("IMAP STORE failed: BYE {0}")]
127    Bye(String),
128    /// The exchange ended without a tagged response from the server.
129    #[error("IMAP STORE failed: server did not return a tagged response")]
130    MissingTagged,
131    /// The underlying send/receive exchange failed (EOF, decode, framing).
132    #[error("IMAP STORE failed: {0}")]
133    Send(#[from] ImapSendError),
134}
135
136/// Options for the IMAP STORE coroutines.
137#[derive(Clone, Debug, Default, Eq, PartialEq)]
138pub struct ImapMessageStoreOptions {
139    /// When `true`, send `UID STORE` and treat `sequence_set` as UIDs.
140    pub uid: bool,
141}
142
143/// Echo variant: server returns FETCH for each modified message.
144pub struct ImapMessageStore {
145    state: State,
146}
147
148impl ImapMessageStore {
149    /// Builds a STORE coroutine applying the `kind` flag change with
150    /// `flags` to the `sequence_set` messages.
151    pub fn new(
152        sequence_set: SequenceSet,
153        kind: StoreType,
154        flags: Vec<Flag<'static>>,
155        opts: ImapMessageStoreOptions,
156    ) -> Self {
157        let command = Command {
158            tag: TagGenerator::new().generate(),
159            body: CommandBody::Store {
160                modifiers: Default::default(),
161                sequence_set,
162                kind,
163                response: StoreResponse::Answer,
164                flags,
165                uid: opts.uid,
166            },
167        };
168
169        trace!("send IMAP command {command:?}");
170
171        let state = State::Send(ImapSend::new(CommandCodec::new(), command));
172
173        Self { state }
174    }
175}
176
177impl ImapCoroutine for ImapMessageStore {
178    type Yield = ImapYield;
179    type Return =
180        Result<BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>>, ImapMessageStoreError>;
181
182    fn resume(
183        &mut self,
184        fragmentizer: &mut Fragmentizer,
185        arg: Option<&[u8]>,
186    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
187        match &mut self.state {
188            State::Send(send) => {
189                let out = imap_try!(send, fragmentizer, arg);
190
191                if let Some(bye) = out.bye {
192                    let err = ImapMessageStoreError::Bye(bye.text.to_string());
193                    return ImapCoroutineState::Complete(Err(err));
194                }
195
196                let Some(Tagged { body, .. }) = out.tagged else {
197                    let err = ImapMessageStoreError::MissingTagged;
198                    return ImapCoroutineState::Complete(Err(err));
199                };
200
201                let mut data: BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>> =
202                    BTreeMap::new();
203                for res in out.data {
204                    if let Data::Fetch { seq, items } = res {
205                        data.insert(seq, items);
206                    }
207                }
208
209                match body.kind {
210                    StatusKind::Ok => ImapCoroutineState::Complete(Ok(data)),
211                    StatusKind::No => {
212                        let err = ImapMessageStoreError::No(body.text.to_string());
213                        ImapCoroutineState::Complete(Err(err))
214                    }
215                    StatusKind::Bad => {
216                        let err = ImapMessageStoreError::Bad(body.text.to_string());
217                        ImapCoroutineState::Complete(Err(err))
218                    }
219                }
220            }
221        }
222    }
223}
224
225/// Silent variant: server suppresses the FETCH echoes.
226pub struct ImapMessageStoreSilent {
227    state: State,
228}
229
230impl ImapMessageStoreSilent {
231    /// Builds a STORE.SILENT coroutine applying the `kind` flag change
232    /// with `flags` to the `sequence_set` messages, without FETCH echoes.
233    pub fn new(
234        sequence_set: SequenceSet,
235        kind: StoreType,
236        flags: Vec<Flag<'static>>,
237        opts: ImapMessageStoreOptions,
238    ) -> Self {
239        let command = Command {
240            tag: TagGenerator::new().generate(),
241            body: CommandBody::Store {
242                modifiers: Default::default(),
243                sequence_set,
244                kind,
245                response: StoreResponse::Silent,
246                flags,
247                uid: opts.uid,
248            },
249        };
250
251        trace!("send IMAP command {command:?}");
252
253        let state = State::Send(ImapSend::new(CommandCodec::new(), command));
254
255        Self { state }
256    }
257}
258
259impl ImapCoroutine for ImapMessageStoreSilent {
260    type Yield = ImapYield;
261    type Return = Result<(), ImapMessageStoreError>;
262
263    fn resume(
264        &mut self,
265        fragmentizer: &mut Fragmentizer,
266        arg: Option<&[u8]>,
267    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
268        match &mut self.state {
269            State::Send(send) => {
270                let out = imap_try!(send, fragmentizer, arg);
271
272                if let Some(bye) = out.bye {
273                    let err = ImapMessageStoreError::Bye(bye.text.to_string());
274                    return ImapCoroutineState::Complete(Err(err));
275                }
276
277                let Some(Tagged { body, .. }) = out.tagged else {
278                    let err = ImapMessageStoreError::MissingTagged;
279                    return ImapCoroutineState::Complete(Err(err));
280                };
281
282                match body.kind {
283                    StatusKind::Ok => ImapCoroutineState::Complete(Ok(())),
284                    StatusKind::No => {
285                        let err = ImapMessageStoreError::No(body.text.to_string());
286                        ImapCoroutineState::Complete(Err(err))
287                    }
288                    StatusKind::Bad => {
289                        let err = ImapMessageStoreError::Bad(body.text.to_string());
290                        ImapCoroutineState::Complete(Err(err))
291                    }
292                }
293            }
294        }
295    }
296}
297
298enum State {
299    Send(ImapSend<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, format, vec, vec::Vec};
315
316    use crate::rfc3501::store::*;
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    fn expect_wants_write_echo(
402        cor: &mut ImapMessageStore,
403        frag: &mut Fragmentizer,
404        arg: Option<&[u8]>,
405    ) -> Vec<u8> {
406        match cor.resume(frag, arg) {
407            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
408            state => panic!("expected WantsWrite, got {state:?}"),
409        }
410    }
411
412    fn expect_wants_read_echo(cor: &mut ImapMessageStore, frag: &mut Fragmentizer) {
413        match cor.resume(frag, None) {
414            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
415            state => panic!("expected WantsRead, got {state:?}"),
416        }
417    }
418
419    fn expect_complete_ok_echo(
420        cor: &mut ImapMessageStore,
421        frag: &mut Fragmentizer,
422        reply: &[u8],
423    ) -> BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>> {
424        match cor.resume(frag, Some(reply)) {
425            ImapCoroutineState::Complete(Ok(value)) => value,
426            state => panic!("expected Complete(Ok), got {state:?}"),
427        }
428    }
429
430    fn expect_complete_err_echo(
431        cor: &mut ImapMessageStore,
432        frag: &mut Fragmentizer,
433        reply: &[u8],
434    ) -> ImapMessageStoreError {
435        match cor.resume(frag, Some(reply)) {
436            ImapCoroutineState::Complete(Err(err)) => err,
437            state => panic!("expected Complete(Err), got {state:?}"),
438        }
439    }
440
441    fn expect_wants_write_silent(
442        cor: &mut ImapMessageStoreSilent,
443        frag: &mut Fragmentizer,
444        arg: Option<&[u8]>,
445    ) -> Vec<u8> {
446        match cor.resume(frag, arg) {
447            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
448            state => panic!("expected WantsWrite, got {state:?}"),
449        }
450    }
451
452    fn expect_wants_read_silent(cor: &mut ImapMessageStoreSilent, frag: &mut Fragmentizer) {
453        match cor.resume(frag, None) {
454            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
455            state => panic!("expected WantsRead, got {state:?}"),
456        }
457    }
458
459    fn expect_complete_ok_silent(
460        cor: &mut ImapMessageStoreSilent,
461        frag: &mut Fragmentizer,
462        reply: &[u8],
463    ) {
464        match cor.resume(frag, Some(reply)) {
465            ImapCoroutineState::Complete(Ok(())) => {}
466            state => panic!("expected Complete(Ok), got {state:?}"),
467        }
468    }
469
470    fn first_word(line: &str) -> &str {
471        line.split_whitespace()
472            .next()
473            .expect("first whitespace-separated token")
474    }
475}