Skip to main content

io_imap/rfc3501/
append.rs

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