Skip to main content

io_imap/rfc3501/
fetch_stream.rs

1//! IMAP FETCH body-stream coroutine: fetches one message body and streams
2//! it straight to the caller's sink instead of buffering it whole.
3//!
4//! Targets a single sequence number or UID and requests `BODY.PEEK[]` only
5//! (peek so syncing does not set `\Seen`). The body literal bypasses the
6//! [`Fragmentizer`] entirely: the coroutine feeds it the framing lines one
7//! at a time, hands the announced octets to the caller via
8//! [`ImapMessageFetchStreamYield::BodyChunk`] /
9//! [`ImapMessageFetchStreamYield::WantsStream`], then resumes line parsing
10//! for the tagged response.
11//!
12//! # Example
13//!
14//! ```rust,no_run
15//! use core::num::NonZeroU32;
16//! use std::{
17//!     io::{self, Read, Write},
18//!     net::TcpStream,
19//! };
20//!
21//! use io_imap::{
22//!     codec::fragmentizer::Fragmentizer,
23//!     coroutine::{ImapCoroutine, ImapCoroutineState},
24//!     rfc3501::fetch_stream::{
25//!         ImapMessageFetchStream, ImapMessageFetchStreamYield,
26//!     },
27//! };
28//!
29//! // Ready stream needed (TCP-connected, TLS-negotiated, IMAP-authenticated)
30//! let mut stream = TcpStream::connect("localhost:143").unwrap();
31//!
32//! let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
33//! let mut buf = [0u8; 4096];
34//! let mut sink = Vec::new();
35//!
36//! let id = NonZeroU32::new(42).unwrap();
37//! let mut coroutine = ImapMessageFetchStream::new(id, true);
38//! let mut arg = None;
39//!
40//! loop {
41//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
42//!         ImapCoroutineState::Yielded(
43//!             ImapMessageFetchStreamYield::WantsWrite(bytes),
44//!         ) => {
45//!             stream.write_all(&bytes).unwrap();
46//!         }
47//!         ImapCoroutineState::Yielded(
48//!             ImapMessageFetchStreamYield::WantsRead,
49//!         ) => {
50//!             let n = stream.read(&mut buf).unwrap();
51//!             arg = Some(&buf[..n]);
52//!         }
53//!         ImapCoroutineState::Yielded(
54//!             ImapMessageFetchStreamYield::BodyChunk(bytes),
55//!         ) => {
56//!             sink.write_all(&bytes).unwrap();
57//!         }
58//!         ImapCoroutineState::Yielded(
59//!             ImapMessageFetchStreamYield::WantsStream { len },
60//!         ) => {
61//!             let mut body = (&mut stream).take(len as u64);
62//!             io::copy(&mut body, &mut sink).unwrap();
63//!         }
64//!         ImapCoroutineState::Complete(Ok(())) => break,
65//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
66//!     }
67//! }
68//!
69//! println!("fetched {} body octets", sink.len());
70//! ```
71
72use core::{fmt, num::NonZeroU32};
73
74use alloc::{string::String, string::ToString, vec, vec::Vec};
75
76use imap_codec::{
77    CommandCodec, ResponseCodec,
78    encode::Encoder,
79    fragmentizer::{FragmentInfo, Fragmentizer},
80    imap_types::{
81        command::{Command, CommandBody},
82        core::TagGenerator,
83        fetch::{MacroOrMessageDataItemNames, MessageDataItemName},
84        response::{Response, Status, StatusKind},
85        sequence::{SeqOrUid, SequenceSet},
86    },
87};
88use log::{debug, trace};
89use thiserror::Error;
90
91use crate::coroutine::*;
92
93/// Failure causes during the IMAP FETCH body-stream flow.
94#[derive(Clone, Debug, Error)]
95pub enum ImapMessageFetchStreamError {
96    /// The server rejected the command with a NO response.
97    #[error("IMAP FETCH failed: NO {0}")]
98    No(String),
99    /// The server rejected the command with a BAD response.
100    #[error("IMAP FETCH failed: BAD {0}")]
101    Bad(String),
102    /// The server closed the session with an untagged BYE.
103    #[error("IMAP FETCH failed: BYE {0}")]
104    Bye(String),
105    /// The exchange ended without a tagged response from the server.
106    #[error("IMAP FETCH failed: server did not return a tagged response")]
107    MissingTagged,
108    /// The socket reached EOF before the declared body octets were all
109    /// streamed.
110    #[error("IMAP FETCH failed: stream ended before the declared body length")]
111    ShortBody,
112    /// A literal announcement appeared in the response trailer, where
113    /// only plain lines are expected.
114    #[error("IMAP FETCH failed: unexpected literal in response trailer")]
115    UnexpectedLiteral,
116}
117
118/// Yield variants from the FETCH body-stream coroutine.
119#[derive(Debug)]
120pub enum ImapMessageFetchStreamYield {
121    /// The caller reads from its stream and resumes with the bytes.
122    WantsRead,
123    /// The caller writes the given bytes to its stream and resumes.
124    WantsWrite(Vec<u8>),
125    /// Body octets the coroutine already read past the header line; the
126    /// caller writes them to its sink.
127    BodyChunk(Vec<u8>),
128    /// Read exactly `len` octets off the socket straight into the sink;
129    /// resume with `None` on success or `Some(&[])` if the socket ran short.
130    WantsStream {
131        /// Number of body octets left to stream.
132        len: u32,
133    },
134}
135
136/// I/O-free IMAP FETCH coroutine streaming one message body.
137pub struct ImapMessageFetchStream {
138    state: State,
139    command: Option<Vec<u8>>,
140    pending: Vec<u8>,
141    remaining: u32,
142    stream_pending: bool,
143    codec: ResponseCodec,
144}
145
146impl ImapMessageFetchStream {
147    /// Builds a FETCH coroutine streaming the `BODY.PEEK[]` of message
148    /// `id`; when `uid` is `true`, sends `UID FETCH`.
149    pub fn new(id: NonZeroU32, uid: bool) -> Self {
150        let command = Command {
151            tag: TagGenerator::new().generate(),
152            body: CommandBody::Fetch {
153                sequence_set: SequenceSet::from(SeqOrUid::from(id)),
154                macro_or_item_names: MacroOrMessageDataItemNames::MessageDataItemNames(vec![
155                    MessageDataItemName::BodyExt {
156                        section: None,
157                        partial: None,
158                        peek: true,
159                    },
160                ]),
161                uid,
162                modifiers: Vec::new(),
163            },
164        };
165
166        trace!("send IMAP command {command:?}");
167
168        let command = CommandCodec::new().encode(&command).dump();
169
170        Self {
171            state: State::SendCommand,
172            command: Some(command),
173            pending: Vec::new(),
174            remaining: 0,
175            stream_pending: false,
176            codec: ResponseCodec::new(),
177        }
178    }
179}
180
181impl ImapCoroutine for ImapMessageFetchStream {
182    type Yield = ImapMessageFetchStreamYield;
183    type Return = Result<(), ImapMessageFetchStreamError>;
184
185    fn resume(
186        &mut self,
187        fragmentizer: &mut Fragmentizer,
188        mut arg: Option<&[u8]>,
189    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
190        loop {
191            match self.state {
192                State::SendCommand => {
193                    let command = self.command.take().expect("command sent once");
194                    self.state = State::Header;
195                    debug!("{}", self.state);
196                    return ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsWrite(
197                        command,
198                    ));
199                }
200                State::Header => {
201                    if let Some(bytes) = arg.take() {
202                        if bytes.is_empty() {
203                            let err = ImapMessageFetchStreamError::MissingTagged;
204                            return ImapCoroutineState::Complete(Err(err));
205                        }
206                        self.pending.extend_from_slice(bytes);
207                    }
208
209                    loop {
210                        let Some(nl) = self.pending.iter().position(|&b| b == b'\n') else {
211                            return ImapCoroutineState::Yielded(
212                                ImapMessageFetchStreamYield::WantsRead,
213                            );
214                        };
215
216                        let line: Vec<u8> = self.pending.drain(..=nl).collect();
217                        fragmentizer.enqueue_bytes(&line);
218
219                        match fragmentizer.progress() {
220                            // NOTE: the FETCH line announces the body literal:
221                            // take its length and stream the body next.
222                            Some(FragmentInfo::Line {
223                                announcement: Some(announcement),
224                                ..
225                            }) => {
226                                self.remaining = announcement.length;
227                                self.state = State::Stream;
228                                debug!("{}", self.state);
229                                break;
230                            }
231                            // NOTE: a complete line without literal: a tagged
232                            // status (FETCH of a missing id returns OK with no
233                            // body), a BYE, or an untagged response we ignore.
234                            Some(FragmentInfo::Line {
235                                announcement: None, ..
236                            }) => {
237                                if let Some(result) = self.decode_terminal(fragmentizer) {
238                                    return result;
239                                }
240                            }
241                            _ => {}
242                        }
243                    }
244                }
245                State::Stream => {
246                    if self.remaining == 0 {
247                        // NOTE: drop the bypassed literal and resume line
248                        // parsing for the response trailer.
249                        fragmentizer.skip_message();
250                        self.state = State::Trailer;
251                        debug!("{}", self.state);
252                        continue;
253                    }
254
255                    if !self.pending.is_empty() {
256                        let take = (self.remaining as usize).min(self.pending.len());
257                        let chunk: Vec<u8> = self.pending.drain(..take).collect();
258                        self.remaining -= take as u32;
259                        return ImapCoroutineState::Yielded(
260                            ImapMessageFetchStreamYield::BodyChunk(chunk),
261                        );
262                    }
263
264                    if self.stream_pending {
265                        self.stream_pending = false;
266                        if matches!(arg.take(), Some(&[])) {
267                            let err = ImapMessageFetchStreamError::ShortBody;
268                            return ImapCoroutineState::Complete(Err(err));
269                        }
270                        self.remaining = 0;
271                        continue;
272                    }
273
274                    self.stream_pending = true;
275                    return ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsStream {
276                        len: self.remaining,
277                    });
278                }
279                State::Trailer => {
280                    if let Some(bytes) = arg.take() {
281                        if bytes.is_empty() {
282                            let err = ImapMessageFetchStreamError::MissingTagged;
283                            return ImapCoroutineState::Complete(Err(err));
284                        }
285                        self.pending.extend_from_slice(bytes);
286                    }
287
288                    loop {
289                        let Some(nl) = self.pending.iter().position(|&b| b == b'\n') else {
290                            return ImapCoroutineState::Yielded(
291                                ImapMessageFetchStreamYield::WantsRead,
292                            );
293                        };
294
295                        let line: Vec<u8> = self.pending.drain(..=nl).collect();
296                        fragmentizer.enqueue_bytes(&line);
297
298                        match fragmentizer.progress() {
299                            Some(FragmentInfo::Line {
300                                announcement: Some(_),
301                                ..
302                            }) => {
303                                let err = ImapMessageFetchStreamError::UnexpectedLiteral;
304                                return ImapCoroutineState::Complete(Err(err));
305                            }
306                            // NOTE: the literal close `)` and any other
307                            // untagged line are skipped; only the tagged
308                            // status and BYE terminate.
309                            Some(FragmentInfo::Line {
310                                announcement: None, ..
311                            }) => {
312                                if let Some(result) = self.decode_terminal(fragmentizer) {
313                                    return result;
314                                }
315                            }
316                            _ => {}
317                        }
318                    }
319                }
320            }
321        }
322    }
323}
324
325impl ImapMessageFetchStream {
326    /// Decodes the completed message in `fragmentizer`.
327    ///
328    /// Returns `Some` for a terminal tagged status or BYE; `None` for
329    /// undecodable or untagged lines that should be skipped (the literal
330    /// close `)`, stray untagged data).
331    fn decode_terminal(
332        &self,
333        fragmentizer: &Fragmentizer,
334    ) -> Option<
335        ImapCoroutineState<ImapMessageFetchStreamYield, Result<(), ImapMessageFetchStreamError>>,
336    > {
337        match fragmentizer.decode_message(&self.codec) {
338            Ok(Response::Status(Status::Tagged(tagged))) => {
339                let text = tagged.body.text.to_string();
340                let result = match tagged.body.kind {
341                    StatusKind::Ok => Ok(()),
342                    StatusKind::No => Err(ImapMessageFetchStreamError::No(text)),
343                    StatusKind::Bad => Err(ImapMessageFetchStreamError::Bad(text)),
344                };
345                Some(ImapCoroutineState::Complete(result))
346            }
347            Ok(Response::Status(Status::Bye(bye))) => {
348                let err = ImapMessageFetchStreamError::Bye(bye.text.to_string());
349                Some(ImapCoroutineState::Complete(Err(err)))
350            }
351            _ => None,
352        }
353    }
354}
355
356#[derive(Clone, Copy)]
357enum State {
358    SendCommand,
359    Header,
360    Stream,
361    Trailer,
362}
363
364impl fmt::Display for State {
365    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366        match self {
367            Self::SendCommand => f.write_str("send fetch command"),
368            Self::Header => f.write_str("parse fetch header"),
369            Self::Stream => f.write_str("stream body"),
370            Self::Trailer => f.write_str("parse fetch trailer"),
371        }
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use core::str;
378
379    use alloc::{borrow::ToOwned, format};
380
381    use crate::rfc3501::fetch_stream::*;
382
383    #[test]
384    fn streams_body_in_one_read() {
385        let mut cor = ImapMessageFetchStream::new(NonZeroU32::new(1).unwrap(), true);
386        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
387
388        let cmd = expect_wants_write(&mut cor, &mut frag, None);
389        let line = str::from_utf8(&cmd).expect("utf8 command");
390        let tag = first_word(line).to_owned();
391        assert!(line.contains("UID FETCH 1 BODY.PEEK[]"));
392
393        expect_wants_read(&mut cor, &mut frag, None);
394
395        // NOTE: header, whole body and trailer arrive together.
396        let reply = format!("* 1 FETCH (BODY[] {{5}}\r\nhello)\r\n{tag} OK FETCH completed\r\n");
397        let chunk = expect_body_chunk(&mut cor, &mut frag, Some(reply.as_bytes()));
398        assert_eq!(chunk, b"hello");
399
400        // NOTE: no socket bytes left to stream; the trailer completes
401        // from pending.
402        expect_complete_ok(&mut cor, &mut frag, None);
403    }
404
405    #[test]
406    fn streams_body_via_wants_stream() {
407        let mut cor = ImapMessageFetchStream::new(NonZeroU32::new(9).unwrap(), false);
408        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
409
410        let cmd = expect_wants_write(&mut cor, &mut frag, None);
411        let line = str::from_utf8(&cmd).expect("utf8 command");
412        let tag = first_word(line).to_owned();
413        assert!(line.contains("FETCH 9 BODY.PEEK[]"));
414        assert!(!line.contains("UID"));
415
416        expect_wants_read(&mut cor, &mut frag, None);
417
418        // NOTE: only the header line arrives: the body must be streamed.
419        let len = expect_wants_stream(&mut cor, &mut frag, Some(b"* 9 FETCH (BODY[] {12}\r\n"));
420        assert_eq!(len, 12);
421
422        // NOTE: the caller streamed all 12 octets: resume clean, then
423        // read the trailer.
424        expect_wants_read(&mut cor, &mut frag, None);
425
426        let reply = format!(")\r\n{tag} OK FETCH completed\r\n");
427        expect_complete_ok(&mut cor, &mut frag, Some(reply.as_bytes()));
428    }
429
430    #[test]
431    fn partial_body_in_header_read_chunks_then_streams() {
432        let mut cor = ImapMessageFetchStream::new(NonZeroU32::new(1).unwrap(), true);
433        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
434
435        let cmd = expect_wants_write(&mut cor, &mut frag, None);
436        let tag = first_word(str::from_utf8(&cmd).expect("utf8 command")).to_owned();
437
438        expect_wants_read(&mut cor, &mut frag, None);
439
440        // NOTE: header line plus the first 3 of 5 body octets.
441        let chunk = expect_body_chunk(&mut cor, &mut frag, Some(b"* 1 FETCH (BODY[] {5}\r\nhel"));
442        assert_eq!(chunk, b"hel");
443
444        // NOTE: remaining 2 octets streamed off the socket.
445        let len = expect_wants_stream(&mut cor, &mut frag, None);
446        assert_eq!(len, 2);
447
448        expect_wants_read(&mut cor, &mut frag, None);
449
450        let reply = format!(")\r\n{tag} OK done\r\n");
451        expect_complete_ok(&mut cor, &mut frag, Some(reply.as_bytes()));
452    }
453
454    #[test]
455    fn missing_message_returns_ok_without_body() {
456        let mut cor = ImapMessageFetchStream::new(NonZeroU32::new(7).unwrap(), true);
457        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
458
459        let cmd = expect_wants_write(&mut cor, &mut frag, None);
460        let tag = first_word(str::from_utf8(&cmd).expect("utf8 command")).to_owned();
461
462        expect_wants_read(&mut cor, &mut frag, None);
463
464        // NOTE: no untagged FETCH: the id did not exist.
465        let reply = format!("{tag} OK FETCH completed\r\n");
466        expect_complete_ok(&mut cor, &mut frag, Some(reply.as_bytes()));
467    }
468
469    #[test]
470    fn tagged_no_returns_no_error() {
471        let mut cor = ImapMessageFetchStream::new(NonZeroU32::new(7).unwrap(), true);
472        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
473
474        let cmd = expect_wants_write(&mut cor, &mut frag, None);
475        let tag = first_word(str::from_utf8(&cmd).expect("utf8 command")).to_owned();
476
477        expect_wants_read(&mut cor, &mut frag, None);
478
479        let reply = format!("{tag} NO mailbox not selected\r\n");
480        let err = expect_complete_err(&mut cor, &mut frag, Some(reply.as_bytes()));
481        let ImapMessageFetchStreamError::No(text) = err else {
482            panic!("expected ImapMessageFetchStreamError::No, got {err:?}");
483        };
484        assert_eq!(text, "mailbox not selected");
485    }
486
487    #[test]
488    fn short_stream_returns_short_body() {
489        let mut cor = ImapMessageFetchStream::new(NonZeroU32::new(1).unwrap(), true);
490        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
491
492        let _ = expect_wants_write(&mut cor, &mut frag, None);
493        expect_wants_read(&mut cor, &mut frag, None);
494        let _ = expect_wants_stream(&mut cor, &mut frag, Some(b"* 1 FETCH (BODY[] {12}\r\n"));
495
496        // NOTE: socket EOF mid-body: the caller signals a short read.
497        let err = expect_complete_err(&mut cor, &mut frag, Some(&[]));
498        assert!(matches!(err, ImapMessageFetchStreamError::ShortBody));
499    }
500
501    fn expect_wants_write(
502        cor: &mut ImapMessageFetchStream,
503        frag: &mut Fragmentizer,
504        arg: Option<&[u8]>,
505    ) -> Vec<u8> {
506        match cor.resume(frag, arg) {
507            ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsWrite(bytes)) => bytes,
508            state => panic!("expected WantsWrite, got {state:?}"),
509        }
510    }
511
512    fn expect_wants_read(
513        cor: &mut ImapMessageFetchStream,
514        frag: &mut Fragmentizer,
515        arg: Option<&[u8]>,
516    ) {
517        match cor.resume(frag, arg) {
518            ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsRead) => {}
519            state => panic!("expected WantsRead, got {state:?}"),
520        }
521    }
522
523    fn expect_body_chunk(
524        cor: &mut ImapMessageFetchStream,
525        frag: &mut Fragmentizer,
526        arg: Option<&[u8]>,
527    ) -> Vec<u8> {
528        match cor.resume(frag, arg) {
529            ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::BodyChunk(bytes)) => bytes,
530            state => panic!("expected BodyChunk, got {state:?}"),
531        }
532    }
533
534    fn expect_wants_stream(
535        cor: &mut ImapMessageFetchStream,
536        frag: &mut Fragmentizer,
537        arg: Option<&[u8]>,
538    ) -> u32 {
539        match cor.resume(frag, arg) {
540            ImapCoroutineState::Yielded(ImapMessageFetchStreamYield::WantsStream { len }) => len,
541            state => panic!("expected WantsStream, got {state:?}"),
542        }
543    }
544
545    fn expect_complete_ok(
546        cor: &mut ImapMessageFetchStream,
547        frag: &mut Fragmentizer,
548        arg: Option<&[u8]>,
549    ) {
550        match cor.resume(frag, arg) {
551            ImapCoroutineState::Complete(Ok(())) => {}
552            state => panic!("expected Complete(Ok), got {state:?}"),
553        }
554    }
555
556    fn expect_complete_err(
557        cor: &mut ImapMessageFetchStream,
558        frag: &mut Fragmentizer,
559        arg: Option<&[u8]>,
560    ) -> ImapMessageFetchStreamError {
561        match cor.resume(frag, arg) {
562            ImapCoroutineState::Complete(Err(err)) => err,
563            state => panic!("expected Complete(Err), got {state:?}"),
564        }
565    }
566
567    fn first_word(line: &str) -> &str {
568        line.split_whitespace()
569            .next()
570            .expect("first whitespace-separated token")
571    }
572}