Skip to main content

io_imap/rfc3501/
append_stream.rs

1//! IMAP APPEND coroutine streaming the message body and returning the EXISTS
2//! count and APPENDUID pair.
3//!
4//! The body is never held whole: [`ImapMessageAppendStream::new`] takes only
5//! the octet count (IMAP declares it up front in the literal), and the
6//! coroutine yields [`ImapMessageAppendStreamYield::WantsStream`] so the
7//! caller pumps the bytes straight from its own source to the socket. Use
8//! [`super::append::ImapMessageAppend`] when the whole message already fits
9//! in memory.
10//!
11//! # Example
12//!
13//! ```rust,no_run
14//! use std::{
15//!     io::{self, Read, Write},
16//!     net::TcpStream,
17//! };
18//!
19//! use io_imap::{
20//!     codec::fragmentizer::Fragmentizer,
21//!     coroutine::{ImapCoroutine, ImapCoroutineState},
22//!     rfc3501::{
23//!         append::ImapMessageAppendOptions,
24//!         append_stream::{
25//!             ImapMessageAppendStream, ImapMessageAppendStreamYield,
26//!         },
27//!     },
28//! };
29//!
30//! // Ready stream needed (TCP-connected, TLS-negotiated, IMAP-authenticated)
31//! let mut stream = TcpStream::connect("localhost:143").unwrap();
32//!
33//! let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
34//! let mut buf = [0u8; 4096];
35//!
36//! let message: &[u8] = b"From: a@b\r\nSubject: hi\r\n\r\nhello";
37//! let mut body = message;
38//! let mailbox = "INBOX".try_into().unwrap();
39//! let opts = ImapMessageAppendOptions::default();
40//! let len = message.len() as u32;
41//! let mut coroutine = ImapMessageAppendStream::new(mailbox, len, opts);
42//! let mut arg = None;
43//!
44//! let (exists, appenduid) = loop {
45//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
46//!         ImapCoroutineState::Yielded(
47//!             ImapMessageAppendStreamYield::WantsWrite(bytes),
48//!         ) => {
49//!             stream.write_all(&bytes).unwrap();
50//!         }
51//!         ImapCoroutineState::Yielded(
52//!             ImapMessageAppendStreamYield::WantsRead,
53//!         ) => {
54//!             let n = stream.read(&mut buf).unwrap();
55//!             arg = Some(&buf[..n]);
56//!         }
57//!         ImapCoroutineState::Yielded(
58//!             ImapMessageAppendStreamYield::WantsStream,
59//!         ) => {
60//!             io::copy(&mut body, &mut stream).unwrap();
61//!         }
62//!         ImapCoroutineState::Complete(Ok(out)) => break out,
63//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
64//!     }
65//! };
66//!
67//! println!("exists={exists:?} appenduid={appenduid:?}");
68//! ```
69
70use core::fmt;
71
72use alloc::{format, string::String, string::ToString, vec::Vec};
73
74use imap_codec::{
75    CommandCodec,
76    encode::{Encoder, Fragment},
77    fragmentizer::Fragmentizer,
78    imap_types::{
79        command::{Command, CommandBody},
80        core::{Literal, TagGenerator},
81        extensions::binary::LiteralOrLiteral8,
82        mailbox::Mailbox,
83        response::{Code, Data, StatusKind, Tagged},
84    },
85};
86use log::{debug, trace};
87use thiserror::Error;
88
89use crate::{
90    coroutine::*,
91    imap_try,
92    rfc3501::{
93        append::{ImapMessageAppendOptions, ImapMessageAppendOutput},
94        mailbox::encode_inplace,
95    },
96    send::*,
97};
98
99/// Failure causes during the IMAP APPEND streaming flow.
100#[derive(Clone, Debug, Error)]
101pub enum ImapMessageAppendStreamError {
102    /// The server rejected the command with a NO response.
103    #[error("IMAP APPEND failed: NO {0}")]
104    No(String),
105    /// The server rejected the command with a BAD response.
106    #[error("IMAP APPEND failed: BAD {0}")]
107    Bad(String),
108    /// The server closed the session with an untagged BYE.
109    #[error("IMAP APPEND failed: BYE {0}")]
110    Bye(String),
111    /// The exchange ended without a tagged response from the server.
112    #[error("IMAP APPEND failed: server did not return a tagged response")]
113    MissingTagged,
114    /// The message source delivered fewer octets than the declared
115    /// literal length.
116    #[error("IMAP APPEND failed: message source delivered fewer octets than declared")]
117    ShortMessage,
118    /// The underlying send/receive exchange failed (EOF, decode, framing).
119    #[error("IMAP APPEND failed: {0}")]
120    Send(#[from] ImapSendError),
121}
122
123/// Yield variants from the streaming APPEND coroutine.
124#[derive(Debug)]
125pub enum ImapMessageAppendStreamYield {
126    /// The caller reads from its stream and resumes with the bytes.
127    WantsRead,
128    /// The caller writes the given bytes to its stream and resumes.
129    WantsWrite(Vec<u8>),
130    /// Stream exactly the declared message octets to the server, then resume
131    /// with `None` on success or `Some(&[])` if the source ran short.
132    WantsStream,
133}
134
135impl From<ImapYield> for ImapMessageAppendStreamYield {
136    fn from(yielded: ImapYield) -> Self {
137        match yielded {
138            ImapYield::WantsRead => Self::WantsRead,
139            ImapYield::WantsWrite(bytes) => Self::WantsWrite(bytes),
140        }
141    }
142}
143
144/// I/O-free IMAP APPEND coroutine streaming the message body.
145pub struct ImapMessageAppendStream {
146    state: State,
147    header: Option<Vec<u8>>,
148    crlf: Option<Vec<u8>>,
149    command: Command<'static>,
150    non_sync: bool,
151    stream_pending: bool,
152}
153
154impl ImapMessageAppendStream {
155    /// Builds a streaming APPEND coroutine appending a `len`-octet message
156    /// to `mailbox`.
157    ///
158    /// The body bytes never pass through the coroutine: it yields
159    /// [`ImapMessageAppendStreamYield::WantsStream`] and the caller pumps
160    /// exactly `len` octets from its own source to the socket.
161    pub fn new(mut mailbox: Mailbox<'static>, len: u32, opts: ImapMessageAppendOptions) -> Self {
162        encode_inplace(&mut mailbox);
163
164        // NOTE: build the request line through imap-codec with an empty
165        // literal, then splice in the real octet count: streaming keeps
166        // the message body out of the encoder so it never lands in
167        // memory whole.
168        let command = Command {
169            tag: TagGenerator::new().generate(),
170            body: CommandBody::Append {
171                mailbox,
172                flags: opts.flags,
173                date: opts.date,
174                message: LiteralOrLiteral8::Literal(Literal::unvalidated_non_sync(Vec::new())),
175            },
176        };
177
178        trace!("send IMAP command {command:?}");
179
180        let fragments: Vec<Fragment> = CommandCodec::new().encode(&command).collect();
181
182        // NOTE: the message literal is the last literal fragment: the lines
183        // before it form the request header, the line after it the
184        // command-closing CRLF that follows the streamed body. The empty
185        // literal itself is dropped.
186        let last = fragments
187            .iter()
188            .rposition(|fragment| matches!(fragment, Fragment::Literal { .. }))
189            .expect("APPEND always encodes a message literal");
190
191        let mut header = Vec::new();
192        let mut crlf = Vec::new();
193
194        for (index, fragment) in fragments.into_iter().enumerate() {
195            match fragment {
196                Fragment::Line { data } if index < last => header.extend(data),
197                Fragment::Line { data } => crlf.extend(data),
198                // NOTE: a mailbox literal (rare) precedes the message one and
199                // belongs inline in the header.
200                Fragment::Literal { data, .. } if index < last => header.extend(data),
201                Fragment::Literal { .. } => {}
202            }
203        }
204
205        // NOTE: imap-codec emitted the empty literal header as
206        // `{0+}\r\n`; rewrite it with the real count, synchronising
207        // unless `non_sync` was requested.
208        const EMPTY_LITERAL: &[u8] = b"{0+}\r\n";
209        debug_assert!(header.ends_with(EMPTY_LITERAL));
210        header.truncate(header.len() - EMPTY_LITERAL.len());
211
212        if opts.non_sync {
213            header.extend_from_slice(format!("{{{len}+}}\r\n").as_bytes());
214        } else {
215            header.extend_from_slice(format!("{{{len}}}\r\n").as_bytes());
216        }
217
218        Self {
219            state: State::WriteHeader,
220            header: Some(header),
221            crlf: Some(crlf),
222            command,
223            non_sync: opts.non_sync,
224            stream_pending: false,
225        }
226    }
227}
228
229impl ImapCoroutine for ImapMessageAppendStream {
230    type Yield = ImapMessageAppendStreamYield;
231    type Return = Result<ImapMessageAppendOutput, ImapMessageAppendStreamError>;
232
233    fn resume(
234        &mut self,
235        fragmentizer: &mut Fragmentizer,
236        arg: Option<&[u8]>,
237    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
238        loop {
239            match &mut self.state {
240                State::WriteHeader => {
241                    let header = self.header.take().expect("header written once");
242
243                    // NOTE: synchronising literals wait for the server
244                    // `+` before the body; non-synchronising ones stream
245                    // straight away.
246                    self.state = if self.non_sync {
247                        State::Stream
248                    } else {
249                        State::Continuation(ImapSend::receive(self.command.clone()))
250                    };
251                    debug!("{}", self.state);
252
253                    return ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsWrite(
254                        header,
255                    ));
256                }
257                State::Continuation(recv) => {
258                    let out = imap_try!(recv, fragmentizer, arg);
259
260                    if let Some(bye) = out.bye {
261                        let err = ImapMessageAppendStreamError::Bye(bye.text.to_string());
262                        return ImapCoroutineState::Complete(Err(err));
263                    }
264
265                    // NOTE: a tagged response before the continuation
266                    // means the server refused the append up front.
267                    if let Some(Tagged { body, .. }) = out.tagged {
268                        let err = match body.kind {
269                            StatusKind::No => {
270                                ImapMessageAppendStreamError::No(body.text.to_string())
271                            }
272                            _ => ImapMessageAppendStreamError::Bad(body.text.to_string()),
273                        };
274                        return ImapCoroutineState::Complete(Err(err));
275                    }
276
277                    self.state = State::Stream;
278                    debug!("{}", self.state);
279                }
280                State::Stream => {
281                    if self.stream_pending {
282                        self.stream_pending = false;
283
284                        if matches!(arg, Some(&[])) {
285                            let err = ImapMessageAppendStreamError::ShortMessage;
286                            return ImapCoroutineState::Complete(Err(err));
287                        }
288
289                        self.state = State::WriteCrlf;
290                        debug!("{}", self.state);
291                        continue;
292                    }
293
294                    self.stream_pending = true;
295                    return ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsStream);
296                }
297                State::WriteCrlf => {
298                    let crlf = self.crlf.take().expect("crlf written once");
299                    self.state = State::Recv(ImapSend::receive(self.command.clone()));
300                    debug!("{}", self.state);
301                    return ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsWrite(
302                        crlf,
303                    ));
304                }
305                State::Recv(recv) => {
306                    let out = imap_try!(recv, fragmentizer, arg);
307
308                    if let Some(bye) = out.bye {
309                        let err = ImapMessageAppendStreamError::Bye(bye.text.to_string());
310                        return ImapCoroutineState::Complete(Err(err));
311                    }
312
313                    let Some(Tagged { body, .. }) = out.tagged else {
314                        let err = ImapMessageAppendStreamError::MissingTagged;
315                        return ImapCoroutineState::Complete(Err(err));
316                    };
317
318                    let mut exists = None;
319
320                    for data in out.data {
321                        if let Data::Exists(seq) = data {
322                            exists = Some(seq);
323                        }
324                    }
325
326                    return match body.kind {
327                        StatusKind::Ok => {
328                            let appenduid =
329                                if let Some(Code::AppendUid { uid_validity, uid }) = body.code {
330                                    Some((uid_validity.get(), uid.get()))
331                                } else {
332                                    None
333                                };
334                            ImapCoroutineState::Complete(Ok((exists, appenduid)))
335                        }
336                        StatusKind::No => {
337                            let err = ImapMessageAppendStreamError::No(body.text.to_string());
338                            ImapCoroutineState::Complete(Err(err))
339                        }
340                        StatusKind::Bad => {
341                            let err = ImapMessageAppendStreamError::Bad(body.text.to_string());
342                            ImapCoroutineState::Complete(Err(err))
343                        }
344                    };
345                }
346            }
347        }
348    }
349}
350
351enum State {
352    WriteHeader,
353    Continuation(ImapSend<CommandCodec>),
354    Stream,
355    WriteCrlf,
356    Recv(ImapSend<CommandCodec>),
357}
358
359impl fmt::Display for State {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        match self {
362            Self::WriteHeader => f.write_str("write append header"),
363            Self::Continuation(_) => f.write_str("await continuation"),
364            Self::Stream => f.write_str("stream message"),
365            Self::WriteCrlf => f.write_str("write append crlf"),
366            Self::Recv(_) => f.write_str("receive append response"),
367        }
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use core::str;
374
375    use alloc::borrow::ToOwned;
376
377    use crate::rfc3501::append_stream::*;
378
379    #[test]
380    fn sync_success_with_appenduid_returns_pair() {
381        let mut append = ImapMessageAppendStream::new(
382            "INBOX".try_into().expect("valid mailbox"),
383            15,
384            ImapMessageAppendOptions::default(),
385        );
386        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
387
388        let header = expect_wants_write(&mut append, &mut frag, None);
389        let line = str::from_utf8(&header).expect("utf8 header");
390        let tag = first_word(line).to_owned();
391        assert!(line.contains("APPEND INBOX"));
392        assert!(line.ends_with("{15}\r\n"));
393
394        expect_wants_read(&mut append, &mut frag, None);
395        expect_wants_stream(
396            &mut append,
397            &mut frag,
398            Some(b"+ Ready for literal data\r\n"),
399        );
400
401        let crlf = expect_wants_write(&mut append, &mut frag, None);
402        assert_eq!(crlf, b"\r\n");
403
404        expect_wants_read(&mut append, &mut frag, None);
405
406        let reply =
407            format!("* 42 EXISTS\r\n{tag} OK [APPENDUID 1700000000 7] APPEND completed\r\n");
408        let (exists, appenduid) =
409            expect_complete_ok(&mut append, &mut frag, Some(reply.as_bytes()));
410        assert_eq!(Some(42), exists);
411        assert_eq!(Some((1700000000, 7)), appenduid);
412    }
413
414    #[test]
415    fn non_sync_streams_without_continuation() {
416        let mut append = ImapMessageAppendStream::new(
417            "INBOX".try_into().expect("valid mailbox"),
418            15,
419            ImapMessageAppendOptions {
420                non_sync: true,
421                ..Default::default()
422            },
423        );
424        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
425
426        let header = expect_wants_write(&mut append, &mut frag, None);
427        let line = str::from_utf8(&header).expect("utf8 header");
428        let tag = first_word(line).to_owned();
429        assert!(line.ends_with("{15+}\r\n"));
430
431        // NOTE: no continuation read: the body streams straight away.
432        expect_wants_stream(&mut append, &mut frag, None);
433        let crlf = expect_wants_write(&mut append, &mut frag, None);
434        assert_eq!(crlf, b"\r\n");
435
436        expect_wants_read(&mut append, &mut frag, None);
437
438        let reply = format!("{tag} OK APPEND completed\r\n");
439        expect_complete_ok(&mut append, &mut frag, Some(reply.as_bytes()));
440    }
441
442    #[test]
443    fn continuation_no_returns_no_error() {
444        let mut append = ImapMessageAppendStream::new(
445            "INBOX".try_into().expect("valid mailbox"),
446            15,
447            ImapMessageAppendOptions::default(),
448        );
449        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
450
451        let header = expect_wants_write(&mut append, &mut frag, None);
452        let tag = first_word(str::from_utf8(&header).expect("utf8 header")).to_owned();
453
454        expect_wants_read(&mut append, &mut frag, None);
455
456        let reply = format!("{tag} NO over quota\r\n");
457        let err = expect_complete_err(&mut append, &mut frag, Some(reply.as_bytes()));
458        let ImapMessageAppendStreamError::No(text) = err else {
459            panic!("expected ImapMessageAppendStreamError::No, got {err:?}");
460        };
461        assert_eq!(text, "over quota");
462    }
463
464    #[test]
465    fn short_stream_returns_short_message_error() {
466        let mut append = ImapMessageAppendStream::new(
467            "INBOX".try_into().expect("valid mailbox"),
468            15,
469            ImapMessageAppendOptions::default(),
470        );
471        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
472
473        let _ = expect_wants_write(&mut append, &mut frag, None);
474        expect_wants_read(&mut append, &mut frag, None);
475        expect_wants_stream(&mut append, &mut frag, Some(b"+ go\r\n"));
476
477        // NOTE: an empty slice signals a short source.
478        let err = expect_complete_err(&mut append, &mut frag, Some(&[]));
479        assert!(matches!(err, ImapMessageAppendStreamError::ShortMessage));
480    }
481
482    #[test]
483    fn bye_returns_bye_error() {
484        let mut append = ImapMessageAppendStream::new(
485            "INBOX".try_into().expect("valid mailbox"),
486            15,
487            ImapMessageAppendOptions::default(),
488        );
489        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
490
491        let _ = expect_wants_write(&mut append, &mut frag, None);
492        expect_wants_read(&mut append, &mut frag, None);
493        expect_wants_stream(&mut append, &mut frag, Some(b"+ go\r\n"));
494        let _ = expect_wants_write(&mut append, &mut frag, None);
495        expect_wants_read(&mut append, &mut frag, None);
496
497        let err = expect_complete_err(&mut append, &mut frag, Some(b"* BYE shutting down\r\n"));
498        let ImapMessageAppendStreamError::Bye(text) = err else {
499            panic!("expected ImapMessageAppendStreamError::Bye, got {err:?}");
500        };
501        assert_eq!(text, "shutting down");
502    }
503
504    fn expect_wants_write(
505        cor: &mut ImapMessageAppendStream,
506        frag: &mut Fragmentizer,
507        arg: Option<&[u8]>,
508    ) -> Vec<u8> {
509        match cor.resume(frag, arg) {
510            ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsWrite(bytes)) => bytes,
511            state => panic!("expected WantsWrite, got {state:?}"),
512        }
513    }
514
515    fn expect_wants_read(
516        cor: &mut ImapMessageAppendStream,
517        frag: &mut Fragmentizer,
518        arg: Option<&[u8]>,
519    ) {
520        match cor.resume(frag, arg) {
521            ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsRead) => {}
522            state => panic!("expected WantsRead, got {state:?}"),
523        }
524    }
525
526    fn expect_wants_stream(
527        cor: &mut ImapMessageAppendStream,
528        frag: &mut Fragmentizer,
529        arg: Option<&[u8]>,
530    ) {
531        match cor.resume(frag, arg) {
532            ImapCoroutineState::Yielded(ImapMessageAppendStreamYield::WantsStream) => {}
533            state => panic!("expected WantsStream, got {state:?}"),
534        }
535    }
536
537    fn expect_complete_ok(
538        cor: &mut ImapMessageAppendStream,
539        frag: &mut Fragmentizer,
540        arg: Option<&[u8]>,
541    ) -> ImapMessageAppendOutput {
542        match cor.resume(frag, arg) {
543            ImapCoroutineState::Complete(Ok(value)) => value,
544            state => panic!("expected Complete(Ok), got {state:?}"),
545        }
546    }
547
548    fn expect_complete_err(
549        cor: &mut ImapMessageAppendStream,
550        frag: &mut Fragmentizer,
551        arg: Option<&[u8]>,
552    ) -> ImapMessageAppendStreamError {
553        match cor.resume(frag, arg) {
554            ImapCoroutineState::Complete(Err(err)) => err,
555            state => panic!("expected Complete(Err), got {state:?}"),
556        }
557    }
558
559    fn first_word(line: &str) -> &str {
560        line.split_whitespace()
561            .next()
562            .expect("first whitespace-separated token")
563    }
564}