Skip to main content

io_imap/rfc3501/
fetch.rs

1//! IMAP FETCH coroutines: range ([`ImapMessageFetch`]) and single-message
2//! ([`ImapMessageFetchFirst`]) variants.
3//!
4//! # Examples
5//!
6//! Range variant over an arbitrary `SequenceSet`:
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::fetch::{ImapMessageFetch, ImapMessageFetchOptions},
18//!     types::fetch::{Macro, MacroOrMessageDataItemNames},
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:*".try_into().unwrap();
28//! let items = MacroOrMessageDataItemNames::Macro(Macro::Full);
29//! let opts = ImapMessageFetchOptions::default();
30//! let mut coroutine = ImapMessageFetch::new(sequence_set, items, opts);
31//! let mut arg = None;
32//!
33//! let messages = loop {
34//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
35//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
36//!             stream.write_all(&bytes).unwrap();
37//!         }
38//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
39//!             let n = stream.read(&mut buf).unwrap();
40//!             arg = Some(&buf[..n]);
41//!         }
42//!         ImapCoroutineState::Complete(Ok(messages)) => break messages,
43//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
44//!     }
45//! };
46//!
47//! println!("{} message(s) fetched", messages.len());
48//! ```
49//!
50//! Single-message variant:
51//!
52//! ```rust,no_run
53//! use core::num::NonZeroU32;
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::fetch::{ImapMessageFetchFirst, ImapMessageFetchOptions},
63//!     types::fetch::{Macro, MacroOrMessageDataItemNames},
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 id = NonZeroU32::new(42).unwrap();
73//! let items = MacroOrMessageDataItemNames::Macro(Macro::Full);
74//! let opts = ImapMessageFetchOptions::default();
75//! let mut coroutine = ImapMessageFetchFirst::new(id, items, opts);
76//! let mut arg = None;
77//!
78//! let items = loop {
79//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
80//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
81//!             stream.write_all(&bytes).unwrap();
82//!         }
83//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
84//!             let n = stream.read(&mut buf).unwrap();
85//!             arg = Some(&buf[..n]);
86//!         }
87//!         ImapCoroutineState::Complete(Ok(items)) => break items,
88//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
89//!     }
90//! };
91//!
92//! println!("{items:?}");
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, FetchModifier},
104        core::{TagGenerator, Vec1},
105        fetch::{MacroOrMessageDataItemNames, MessageDataItem},
106        response::{Data, StatusKind, Tagged},
107        sequence::{SeqOrUid, SequenceSet},
108    },
109};
110use log::trace;
111use thiserror::Error;
112
113use crate::{coroutine::*, imap_try, send::*};
114
115/// Failure causes during the IMAP FETCH flow.
116#[derive(Clone, Debug, Error)]
117pub enum ImapMessageFetchError {
118    #[error("IMAP FETCH failed: NO {0}")]
119    No(String),
120    #[error("IMAP FETCH failed: BAD {0}")]
121    Bad(String),
122    #[error("IMAP FETCH failed: BYE {0}")]
123    Bye(String),
124
125    #[error("IMAP FETCH failed: server did not return a tagged response")]
126    MissingTagged,
127    #[error("IMAP FETCH failed: server did not return any data")]
128    MissingData,
129
130    #[error("IMAP FETCH failed: {0}")]
131    Send(#[from] SendImapCommandError),
132}
133
134/// Options for the IMAP FETCH coroutines.
135#[derive(Clone, Debug, Default, Eq, PartialEq)]
136pub struct ImapMessageFetchOptions {
137    /// When `true`, send `UID FETCH` and treat ids as UIDs.
138    pub uid: bool,
139    /// FETCH modifiers (RFC 4466): CONDSTORE `CHANGEDSINCE`, QRESYNC
140    /// `VANISHED`, ...
141    pub modifiers: Vec<FetchModifier>,
142}
143
144/// FETCH over an arbitrary sequence set.
145pub struct ImapMessageFetch {
146    state: State,
147}
148
149impl ImapMessageFetch {
150    pub fn new(
151        sequence_set: SequenceSet,
152        items: MacroOrMessageDataItemNames<'static>,
153        opts: ImapMessageFetchOptions,
154    ) -> Self {
155        let command = Command {
156            tag: TagGenerator::new().generate(),
157            body: CommandBody::Fetch {
158                modifiers: opts.modifiers,
159                sequence_set,
160                macro_or_item_names: items,
161                uid: opts.uid,
162            },
163        };
164
165        trace!("send IMAP command {command:?}");
166
167        let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
168
169        Self { state }
170    }
171}
172
173impl ImapCoroutine for ImapMessageFetch {
174    type Yield = ImapYield;
175    type Return =
176        Result<BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>>, ImapMessageFetchError>;
177
178    fn resume(
179        &mut self,
180        fragmentizer: &mut Fragmentizer,
181        arg: Option<&[u8]>,
182    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
183        loop {
184            trace!("fetch: {}", self.state);
185
186            match &mut self.state {
187                State::Send(send) => {
188                    let out = imap_try!(send, fragmentizer, arg);
189
190                    if let Some(bye) = out.bye {
191                        let err = ImapMessageFetchError::Bye(bye.text.to_string());
192                        return ImapCoroutineState::Complete(Err(err));
193                    }
194
195                    let Some(Tagged { body, .. }) = out.tagged else {
196                        let err = ImapMessageFetchError::MissingTagged;
197                        return ImapCoroutineState::Complete(Err(err));
198                    };
199
200                    let mut output: BTreeMap<NonZeroU32, Vec<MessageDataItem<'static>>> =
201                        BTreeMap::new();
202                    for data in out.data {
203                        if let Data::Fetch { seq, items } = data {
204                            output.entry(seq).or_default().extend(items.into_iter());
205                        }
206                    }
207
208                    return match body.kind {
209                        StatusKind::Ok => ImapCoroutineState::Complete(Ok(output
210                            .into_iter()
211                            .map(|(key, val)| (key, Vec1::unvalidated(val)))
212                            .collect())),
213                        StatusKind::No => {
214                            let err = ImapMessageFetchError::No(body.text.to_string());
215                            ImapCoroutineState::Complete(Err(err))
216                        }
217                        StatusKind::Bad => {
218                            let err = ImapMessageFetchError::Bad(body.text.to_string());
219                            ImapCoroutineState::Complete(Err(err))
220                        }
221                    };
222                }
223            }
224        }
225    }
226}
227
228/// FETCH restricted to a single message.
229pub struct ImapMessageFetchFirst {
230    state: State,
231}
232
233impl ImapMessageFetchFirst {
234    pub fn new(
235        id: NonZeroU32,
236        items: MacroOrMessageDataItemNames<'static>,
237        opts: ImapMessageFetchOptions,
238    ) -> Self {
239        let command = Command {
240            tag: TagGenerator::new().generate(),
241            body: CommandBody::Fetch {
242                modifiers: opts.modifiers,
243                sequence_set: SequenceSet::from(SeqOrUid::from(id)),
244                macro_or_item_names: items,
245                uid: opts.uid,
246            },
247        };
248
249        trace!("send IMAP command {command:?}");
250
251        let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
252
253        Self { state }
254    }
255}
256
257impl ImapCoroutine for ImapMessageFetchFirst {
258    type Yield = ImapYield;
259    type Return = Result<Vec1<MessageDataItem<'static>>, ImapMessageFetchError>;
260
261    fn resume(
262        &mut self,
263        fragmentizer: &mut Fragmentizer,
264        arg: Option<&[u8]>,
265    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
266        loop {
267            trace!("fetch first: {}", self.state);
268
269            match &mut self.state {
270                State::Send(send) => {
271                    let out = imap_try!(send, fragmentizer, arg);
272
273                    if let Some(bye) = out.bye {
274                        let err = ImapMessageFetchError::Bye(bye.text.to_string());
275                        return ImapCoroutineState::Complete(Err(err));
276                    }
277
278                    let Some(Tagged { body, .. }) = out.tagged else {
279                        let err = ImapMessageFetchError::MissingTagged;
280                        return ImapCoroutineState::Complete(Err(err));
281                    };
282
283                    let mut output = None;
284                    for data in out.data {
285                        if let Data::Fetch { items, .. } = data {
286                            output = Some(items);
287                        }
288                    }
289
290                    return match body.kind {
291                        StatusKind::Ok => match output {
292                            Some(items) => ImapCoroutineState::Complete(Ok(items)),
293                            None => ImapCoroutineState::Complete(Err(
294                                ImapMessageFetchError::MissingData,
295                            )),
296                        },
297                        StatusKind::No => {
298                            let err = ImapMessageFetchError::No(body.text.to_string());
299                            ImapCoroutineState::Complete(Err(err))
300                        }
301                        StatusKind::Bad => {
302                            let err = ImapMessageFetchError::Bad(body.text.to_string());
303                            ImapCoroutineState::Complete(Err(err))
304                        }
305                    };
306                }
307            }
308        }
309    }
310}
311
312enum State {
313    Send(SendImapCommand<CommandCodec>),
314}
315
316impl fmt::Display for State {
317    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318        match self {
319            Self::Send(_) => f.write_str("send fetch"),
320        }
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use core::str;
327
328    use alloc::borrow::ToOwned;
329
330    use super::*;
331
332    #[test]
333    fn fetch_success_groups_by_seq() {
334        let mut fetch = ImapMessageFetch::new(
335            "1:3".try_into().expect("valid sequence set"),
336            MacroOrMessageDataItemNames::Macro(imap_codec::imap_types::fetch::Macro::All),
337            ImapMessageFetchOptions::default(),
338        );
339        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
340
341        let bytes = expect_wants_write_fetch(&mut fetch, &mut frag, None);
342        let line = str::from_utf8(&bytes).expect("utf8 command");
343        let tag = first_word(line).to_owned();
344        assert!(line.contains("FETCH 1:3 "));
345
346        expect_wants_read_fetch(&mut fetch, &mut frag);
347
348        let reply =
349            format!("* 1 FETCH (UID 100)\r\n* 2 FETCH (UID 101)\r\n{tag} OK FETCH completed\r\n",);
350        let out = expect_complete_ok_fetch(&mut fetch, &mut frag, reply.as_bytes());
351        assert_eq!(2, out.len());
352    }
353
354    #[test]
355    fn uid_variant_sends_uid_fetch() {
356        let mut fetch = ImapMessageFetch::new(
357            "42".try_into().expect("valid sequence set"),
358            MacroOrMessageDataItemNames::Macro(imap_codec::imap_types::fetch::Macro::All),
359            ImapMessageFetchOptions {
360                uid: true,
361                ..Default::default()
362            },
363        );
364        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
365
366        let bytes = expect_wants_write_fetch(&mut fetch, &mut frag, None);
367        let line = str::from_utf8(&bytes).expect("utf8 command");
368        assert!(line.contains("UID FETCH 42 "));
369    }
370
371    #[test]
372    fn fetch_tagged_no_returns_no_error() {
373        let mut fetch = ImapMessageFetch::new(
374            "1".try_into().expect("valid sequence set"),
375            MacroOrMessageDataItemNames::Macro(imap_codec::imap_types::fetch::Macro::All),
376            ImapMessageFetchOptions::default(),
377        );
378        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
379
380        let bytes = expect_wants_write_fetch(&mut fetch, &mut frag, None);
381        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
382
383        expect_wants_read_fetch(&mut fetch, &mut frag);
384
385        let reply = format!("{tag} NO no mailbox selected\r\n");
386        let err = expect_complete_err_fetch(&mut fetch, &mut frag, reply.as_bytes());
387        let ImapMessageFetchError::No(text) = err else {
388            panic!("expected ImapMessageFetchError::No, got {err:?}");
389        };
390        assert_eq!(text, "no mailbox selected");
391    }
392
393    #[test]
394    fn fetch_bye_returns_bye_error() {
395        let mut fetch = ImapMessageFetch::new(
396            "1".try_into().expect("valid sequence set"),
397            MacroOrMessageDataItemNames::Macro(imap_codec::imap_types::fetch::Macro::All),
398            ImapMessageFetchOptions::default(),
399        );
400        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
401
402        let _ = expect_wants_write_fetch(&mut fetch, &mut frag, None);
403        expect_wants_read_fetch(&mut fetch, &mut frag);
404
405        let err = expect_complete_err_fetch(&mut fetch, &mut frag, b"* BYE going down\r\n");
406        let ImapMessageFetchError::Bye(text) = err else {
407            panic!("expected ImapMessageFetchError::Bye, got {err:?}");
408        };
409        assert_eq!(text, "going down");
410    }
411
412    #[test]
413    fn first_success_returns_items() {
414        let id = NonZeroU32::new(42).expect("non-zero");
415        let mut fetch = ImapMessageFetchFirst::new(
416            id,
417            MacroOrMessageDataItemNames::Macro(imap_codec::imap_types::fetch::Macro::All),
418            ImapMessageFetchOptions::default(),
419        );
420        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
421
422        let bytes = expect_wants_write_first(&mut fetch, &mut frag, None);
423        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
424
425        expect_wants_read_first(&mut fetch, &mut frag);
426
427        let reply = format!("* 42 FETCH (UID 100)\r\n{tag} OK FETCH completed\r\n");
428        let items = expect_complete_ok_first(&mut fetch, &mut frag, reply.as_bytes());
429        assert_eq!(1, items.as_ref().len());
430    }
431
432    #[test]
433    fn first_missing_data_returns_missing_data_error() {
434        let id = NonZeroU32::new(42).expect("non-zero");
435        let mut fetch = ImapMessageFetchFirst::new(
436            id,
437            MacroOrMessageDataItemNames::Macro(imap_codec::imap_types::fetch::Macro::All),
438            ImapMessageFetchOptions::default(),
439        );
440        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
441
442        let bytes = expect_wants_write_first(&mut fetch, &mut frag, None);
443        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
444
445        expect_wants_read_first(&mut fetch, &mut frag);
446
447        let reply = format!("{tag} OK FETCH completed\r\n");
448        let err = expect_complete_err_first(&mut fetch, &mut frag, reply.as_bytes());
449        assert!(matches!(err, ImapMessageFetchError::MissingData));
450    }
451
452    // --- utils
453
454    fn expect_wants_write_fetch(
455        cor: &mut ImapMessageFetch,
456        frag: &mut Fragmentizer,
457        arg: Option<&[u8]>,
458    ) -> Vec<u8> {
459        match cor.resume(frag, arg) {
460            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
461            state => panic!("expected WantsWrite, got {state:?}"),
462        }
463    }
464
465    fn expect_wants_read_fetch(cor: &mut ImapMessageFetch, frag: &mut Fragmentizer) {
466        match cor.resume(frag, None) {
467            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
468            state => panic!("expected WantsRead, got {state:?}"),
469        }
470    }
471
472    fn expect_complete_ok_fetch(
473        cor: &mut ImapMessageFetch,
474        frag: &mut Fragmentizer,
475        reply: &[u8],
476    ) -> BTreeMap<NonZeroU32, Vec1<MessageDataItem<'static>>> {
477        match cor.resume(frag, Some(reply)) {
478            ImapCoroutineState::Complete(Ok(value)) => value,
479            state => panic!("expected Complete(Ok), got {state:?}"),
480        }
481    }
482
483    fn expect_complete_err_fetch(
484        cor: &mut ImapMessageFetch,
485        frag: &mut Fragmentizer,
486        reply: &[u8],
487    ) -> ImapMessageFetchError {
488        match cor.resume(frag, Some(reply)) {
489            ImapCoroutineState::Complete(Err(err)) => err,
490            state => panic!("expected Complete(Err), got {state:?}"),
491        }
492    }
493
494    fn expect_wants_write_first(
495        cor: &mut ImapMessageFetchFirst,
496        frag: &mut Fragmentizer,
497        arg: Option<&[u8]>,
498    ) -> Vec<u8> {
499        match cor.resume(frag, arg) {
500            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
501            state => panic!("expected WantsWrite, got {state:?}"),
502        }
503    }
504
505    fn expect_wants_read_first(cor: &mut ImapMessageFetchFirst, frag: &mut Fragmentizer) {
506        match cor.resume(frag, None) {
507            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
508            state => panic!("expected WantsRead, got {state:?}"),
509        }
510    }
511
512    fn expect_complete_ok_first(
513        cor: &mut ImapMessageFetchFirst,
514        frag: &mut Fragmentizer,
515        reply: &[u8],
516    ) -> Vec1<MessageDataItem<'static>> {
517        match cor.resume(frag, Some(reply)) {
518            ImapCoroutineState::Complete(Ok(value)) => value,
519            state => panic!("expected Complete(Ok), got {state:?}"),
520        }
521    }
522
523    fn expect_complete_err_first(
524        cor: &mut ImapMessageFetchFirst,
525        frag: &mut Fragmentizer,
526        reply: &[u8],
527    ) -> ImapMessageFetchError {
528        match cor.resume(frag, Some(reply)) {
529            ImapCoroutineState::Complete(Err(err)) => err,
530            state => panic!("expected Complete(Err), got {state:?}"),
531        }
532    }
533
534    fn first_word(line: &str) -> &str {
535        line.split_whitespace()
536            .next()
537            .expect("first whitespace-separated token")
538    }
539}