Skip to main content

io_imap/rfc3501/
append.rs

1//! IMAP APPEND coroutine returning the EXISTS count and APPENDUID pair.
2//!
3//! Buffered: the whole message is held in memory. For large messages, stream
4//! it with [`super::append_stream::ImapMessageAppendStream`] instead.
5//!
6//! # Example
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::append::{ImapMessageAppend, ImapMessageAppendOptions},
18//! };
19//!
20//! // Ready stream needed (TCP-connected, TLS-negotiated, IMAP-authenticated)
21//! let mut stream = TcpStream::connect("localhost:143").unwrap();
22//!
23//! let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
24//! let mut buf = [0u8; 4096];
25//!
26//! let message = b"From: a@b\r\nSubject: hi\r\n\r\nhello".to_vec();
27//! let mailbox = "INBOX".try_into().unwrap();
28//! let opts = ImapMessageAppendOptions::default();
29//! let mut coroutine = ImapMessageAppend::new(mailbox, message, opts);
30//! let mut arg = None;
31//!
32//! let (exists, appenduid) = loop {
33//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
34//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
35//!             stream.write_all(&bytes).unwrap();
36//!         }
37//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
38//!             let n = stream.read(&mut buf).unwrap();
39//!             arg = Some(&buf[..n]);
40//!         }
41//!         ImapCoroutineState::Complete(Ok(out)) => break out,
42//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
43//!     }
44//! };
45//!
46//! println!("exists={exists:?} appenduid={appenduid:?}");
47//! ```
48
49use core::fmt;
50
51use alloc::{string::String, string::ToString, vec::Vec};
52
53use imap_codec::{
54    CommandCodec,
55    fragmentizer::Fragmentizer,
56    imap_types::{
57        command::{Command, CommandBody},
58        core::{Literal, TagGenerator},
59        datetime::DateTime,
60        extensions::binary::LiteralOrLiteral8,
61        flag::Flag,
62        mailbox::Mailbox,
63        response::{Code, Data, StatusKind, Tagged},
64    },
65};
66use log::trace;
67use thiserror::Error;
68
69use crate::{coroutine::*, imap_try, rfc3501::mailbox::encode_inplace, send::*};
70
71/// `(EXISTS count, APPENDUID (uid_validity, uid))`.
72pub type ImapMessageAppendOutput = (Option<u32>, Option<(u32, u32)>);
73
74/// Failure causes during the IMAP APPEND flow.
75#[derive(Clone, Debug, Error)]
76pub enum ImapMessageAppendError {
77    /// The server rejected the command with a NO response.
78    #[error("IMAP APPEND failed: NO {0}")]
79    No(String),
80    /// The server rejected the command with a BAD response.
81    #[error("IMAP APPEND failed: BAD {0}")]
82    Bad(String),
83    /// The server closed the session with an untagged BYE.
84    #[error("IMAP APPEND failed: BYE {0}")]
85    Bye(String),
86    /// The exchange ended without a tagged response from the server.
87    #[error("IMAP APPEND failed: server did not return a tagged response")]
88    MissingTagged,
89    /// The underlying send/receive exchange failed (EOF, decode, framing).
90    #[error("IMAP APPEND failed: {0}")]
91    Send(#[from] ImapSendError),
92}
93
94/// Options shared by the buffered and streaming APPEND coroutines.
95#[derive(Clone, Debug, Default, Eq, PartialEq)]
96pub struct ImapMessageAppendOptions {
97    /// Flags set on the appended message. Defaults to none.
98    pub flags: Vec<Flag<'static>>,
99    /// Internal date set on the appended message. Defaults to the
100    /// server reception time.
101    pub date: Option<DateTime>,
102    /// Send a non-synchronising literal (`{N+}`) instead of waiting for the
103    /// server continuation.
104    ///
105    /// Requires LITERAL+ / LITERAL-, and forfeits early rejection, so it is
106    /// best kept for small messages. Defaults to a synchronising `{N}`
107    /// literal.
108    pub non_sync: bool,
109}
110
111/// I/O-free buffered IMAP APPEND coroutine.
112pub struct ImapMessageAppend {
113    state: State,
114}
115
116impl ImapMessageAppend {
117    /// Builds an APPEND coroutine appending the whole `message` to
118    /// `mailbox`.
119    pub fn new(
120        mut mailbox: Mailbox<'static>,
121        message: Vec<u8>,
122        opts: ImapMessageAppendOptions,
123    ) -> Self {
124        encode_inplace(&mut mailbox);
125
126        let literal = if opts.non_sync {
127            Literal::unvalidated_non_sync(message)
128        } else {
129            Literal::unvalidated(message)
130        };
131
132        let command = Command {
133            tag: TagGenerator::new().generate(),
134            body: CommandBody::Append {
135                mailbox,
136                flags: opts.flags,
137                date: opts.date,
138                message: LiteralOrLiteral8::Literal(literal),
139            },
140        };
141
142        trace!("send IMAP command {command:?}");
143
144        let state = State::Send(ImapSend::new(CommandCodec::new(), command));
145
146        Self { state }
147    }
148}
149
150impl ImapCoroutine for ImapMessageAppend {
151    type Yield = ImapYield;
152    type Return = Result<ImapMessageAppendOutput, ImapMessageAppendError>;
153
154    fn resume(
155        &mut self,
156        fragmentizer: &mut Fragmentizer,
157        arg: Option<&[u8]>,
158    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
159        match &mut self.state {
160            State::Send(send) => {
161                let out = imap_try!(send, fragmentizer, arg);
162
163                if let Some(bye) = out.bye {
164                    let err = ImapMessageAppendError::Bye(bye.text.to_string());
165                    return ImapCoroutineState::Complete(Err(err));
166                }
167
168                let Some(Tagged { body, .. }) = out.tagged else {
169                    let err = ImapMessageAppendError::MissingTagged;
170                    return ImapCoroutineState::Complete(Err(err));
171                };
172
173                let mut exists = None;
174
175                for data in out.data {
176                    if let Data::Exists(seq) = data {
177                        exists = Some(seq);
178                    }
179                }
180
181                match body.kind {
182                    StatusKind::Ok => {
183                        let appenduid =
184                            if let Some(Code::AppendUid { uid_validity, uid }) = body.code {
185                                Some((uid_validity.get(), uid.get()))
186                            } else {
187                                None
188                            };
189                        ImapCoroutineState::Complete(Ok((exists, appenduid)))
190                    }
191                    StatusKind::No => {
192                        let err = ImapMessageAppendError::No(body.text.to_string());
193                        ImapCoroutineState::Complete(Err(err))
194                    }
195                    StatusKind::Bad => {
196                        let err = ImapMessageAppendError::Bad(body.text.to_string());
197                        ImapCoroutineState::Complete(Err(err))
198                    }
199                }
200            }
201        }
202    }
203}
204
205enum State {
206    Send(ImapSend<CommandCodec>),
207}
208
209impl fmt::Display for State {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        match self {
212            Self::Send(_) => f.write_str("send append"),
213        }
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use core::str;
220
221    use alloc::{borrow::ToOwned, format};
222
223    use crate::rfc3501::append::*;
224
225    #[test]
226    fn sync_success_with_appenduid_returns_pair() {
227        let mut append = ImapMessageAppend::new(
228            "INBOX".try_into().expect("valid mailbox"),
229            b"hi".to_vec(),
230            ImapMessageAppendOptions::default(),
231        );
232        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
233
234        let header = expect_wants_write(&mut append, &mut frag, None);
235        let line = str::from_utf8(&header).expect("utf8 header");
236        let tag = first_word(line).to_owned();
237        assert!(line.contains("APPEND INBOX"));
238        assert!(line.ends_with("{2}\r\n"));
239
240        // NOTE: synchronising literal: wait for `+`, then send the
241        // body inline.
242        expect_wants_read(&mut append, &mut frag, None);
243        let body = expect_wants_write(&mut append, &mut frag, Some(b"+ go\r\n"));
244        assert_eq!(body, b"hi\r\n");
245        expect_wants_read(&mut append, &mut frag, None);
246
247        let reply = format!("* 1 EXISTS\r\n{tag} OK [APPENDUID 1700000000 7] APPEND completed\r\n");
248        let (exists, appenduid) =
249            expect_complete_ok(&mut append, &mut frag, Some(reply.as_bytes()));
250        assert_eq!(Some(1), exists);
251        assert_eq!(Some((1700000000, 7)), appenduid);
252    }
253
254    #[test]
255    fn non_sync_sends_body_inline() {
256        let mut append = ImapMessageAppend::new(
257            "INBOX".try_into().expect("valid mailbox"),
258            b"hi".to_vec(),
259            ImapMessageAppendOptions {
260                non_sync: true,
261                ..Default::default()
262            },
263        );
264        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
265
266        let header = expect_wants_write(&mut append, &mut frag, None);
267        let line = str::from_utf8(&header).expect("utf8 header");
268        let tag = first_word(line).to_owned();
269        assert!(line.contains("{2+}\r\nhi\r\n"));
270
271        expect_wants_read(&mut append, &mut frag, None);
272
273        let reply = format!("{tag} OK APPEND completed\r\n");
274        let (exists, appenduid) =
275            expect_complete_ok(&mut append, &mut frag, Some(reply.as_bytes()));
276        assert!(exists.is_none());
277        assert!(appenduid.is_none());
278    }
279
280    #[test]
281    fn tagged_no_returns_no_error() {
282        let mut append = ImapMessageAppend::new(
283            "INBOX".try_into().expect("valid mailbox"),
284            b"hi".to_vec(),
285            ImapMessageAppendOptions::default(),
286        );
287        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
288
289        let header = expect_wants_write(&mut append, &mut frag, None);
290        let tag = first_word(str::from_utf8(&header).expect("utf8 header")).to_owned();
291
292        // NOTE: the server rejects at the continuation point.
293        expect_wants_read(&mut append, &mut frag, None);
294
295        let reply = format!("{tag} NO mailbox is read-only\r\n");
296        let err = expect_complete_err(&mut append, &mut frag, Some(reply.as_bytes()));
297        let ImapMessageAppendError::No(text) = err else {
298            panic!("expected ImapMessageAppendError::No, got {err:?}");
299        };
300        assert_eq!(text, "mailbox is read-only");
301    }
302
303    #[test]
304    fn bye_returns_bye_error() {
305        let mut append = ImapMessageAppend::new(
306            "INBOX".try_into().expect("valid mailbox"),
307            b"hi".to_vec(),
308            ImapMessageAppendOptions::default(),
309        );
310        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
311
312        let _ = expect_wants_write(&mut append, &mut frag, None);
313        expect_wants_read(&mut append, &mut frag, None);
314
315        let err = expect_complete_err(&mut append, &mut frag, Some(b"* BYE shutting down\r\n"));
316        let ImapMessageAppendError::Bye(text) = err else {
317            panic!("expected ImapMessageAppendError::Bye, got {err:?}");
318        };
319        assert_eq!(text, "shutting down");
320    }
321
322    fn expect_wants_write(
323        cor: &mut ImapMessageAppend,
324        frag: &mut Fragmentizer,
325        arg: Option<&[u8]>,
326    ) -> Vec<u8> {
327        match cor.resume(frag, arg) {
328            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
329            state => panic!("expected WantsWrite, got {state:?}"),
330        }
331    }
332
333    fn expect_wants_read(cor: &mut ImapMessageAppend, frag: &mut Fragmentizer, arg: Option<&[u8]>) {
334        match cor.resume(frag, arg) {
335            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
336            state => panic!("expected WantsRead, got {state:?}"),
337        }
338    }
339
340    fn expect_complete_ok(
341        cor: &mut ImapMessageAppend,
342        frag: &mut Fragmentizer,
343        arg: Option<&[u8]>,
344    ) -> ImapMessageAppendOutput {
345        match cor.resume(frag, arg) {
346            ImapCoroutineState::Complete(Ok(value)) => value,
347            state => panic!("expected Complete(Ok), got {state:?}"),
348        }
349    }
350
351    fn expect_complete_err(
352        cor: &mut ImapMessageAppend,
353        frag: &mut Fragmentizer,
354        arg: Option<&[u8]>,
355    ) -> ImapMessageAppendError {
356        match cor.resume(frag, arg) {
357            ImapCoroutineState::Complete(Err(err)) => err,
358            state => panic!("expected Complete(Err), got {state:?}"),
359        }
360    }
361
362    fn first_word(line: &str) -> &str {
363        line.split_whitespace()
364            .next()
365            .expect("first whitespace-separated token")
366    }
367}