Skip to main content

io_imap/rfc4315/
appenduid.rs

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