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