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-negotiated, 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    /// The server rejected the APPEND command with a NO response.
79    #[error("IMAP APPEND failed: NO {0}")]
80    No(String),
81    /// The server rejected the APPEND command with a BAD response.
82    #[error("IMAP APPEND failed: BAD {0}")]
83    Bad(String),
84    /// The server closed the connection with a BYE response.
85    #[error("IMAP APPEND failed: BYE {0}")]
86    Bye(String),
87    /// The server never answered with a tagged response.
88    #[error("IMAP APPEND failed: server did not return a tagged response")]
89    MissingTagged,
90    /// The underlying send sub-coroutine failed.
91    #[error("IMAP APPEND failed: {0}")]
92    Send(#[from] ImapSendError),
93}
94
95/// Options for [`ImapAppendUid::new`].
96#[derive(Clone, Debug, Default, Eq, PartialEq)]
97pub struct ImapAppendUidOptions {
98    /// Flags set on the appended message; defaults to none.
99    pub flags: Vec<Flag<'static>>,
100    /// Internal date of the appended message; defaults to the moment
101    /// the server receives it.
102    pub date: Option<DateTime>,
103}
104
105/// I/O-free IMAP APPEND coroutine returning the APPENDUID pair.
106pub struct ImapAppendUid {
107    state: State,
108}
109
110impl ImapAppendUid {
111    /// Creates a coroutine that APPENDs `message` to `mailbox` and
112    /// returns the APPENDUID (uidvalidity, uid) pair when present.
113    pub fn new(
114        mut mailbox: Mailbox<'static>,
115        message: LiteralOrLiteral8<'static>,
116        opts: ImapAppendUidOptions,
117    ) -> Self {
118        encode_inplace(&mut mailbox);
119
120        let command = Command {
121            tag: TagGenerator::new().generate(),
122            body: CommandBody::Append {
123                mailbox,
124                flags: opts.flags,
125                date: opts.date,
126                message,
127            },
128        };
129
130        trace!("send IMAP command {command:?}");
131
132        let state = State::Send(ImapSend::new(CommandCodec::new(), command));
133
134        Self { state }
135    }
136}
137
138impl ImapCoroutine for ImapAppendUid {
139    type Yield = ImapYield;
140    type Return = Result<Option<(NonZeroU32, NonZeroU32)>, ImapAppendUidError>;
141
142    fn resume(
143        &mut self,
144        fragmentizer: &mut Fragmentizer,
145        arg: Option<&[u8]>,
146    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
147        match &mut self.state {
148            State::Send(send) => {
149                let out = imap_try!(send, fragmentizer, arg);
150
151                if let Some(bye) = out.bye {
152                    let err = ImapAppendUidError::Bye(bye.text.to_string());
153                    return ImapCoroutineState::Complete(Err(err));
154                }
155
156                let Some(Tagged { body, .. }) = out.tagged else {
157                    let err = ImapAppendUidError::MissingTagged;
158                    return ImapCoroutineState::Complete(Err(err));
159                };
160
161                match body.kind {
162                    StatusKind::Ok => {
163                        let pair = if let Some(Code::AppendUid { uid_validity, uid }) = body.code {
164                            Some((uid_validity, uid))
165                        } else {
166                            None
167                        };
168                        ImapCoroutineState::Complete(Ok(pair))
169                    }
170                    StatusKind::No => {
171                        let err = ImapAppendUidError::No(body.text.to_string());
172                        ImapCoroutineState::Complete(Err(err))
173                    }
174                    StatusKind::Bad => {
175                        let err = ImapAppendUidError::Bad(body.text.to_string());
176                        ImapCoroutineState::Complete(Err(err))
177                    }
178                }
179            }
180        }
181    }
182}
183
184enum State {
185    Send(ImapSend<CommandCodec>),
186}
187
188impl fmt::Display for State {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        match self {
191            Self::Send(_) => f.write_str("send append"),
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use core::str;
199
200    use alloc::{borrow::ToOwned, format, vec::Vec};
201
202    use imap_codec::imap_types::core::Literal;
203
204    use crate::rfc4315::appenduid::*;
205
206    #[test]
207    fn success_with_appenduid_returns_pair() {
208        let message = LiteralOrLiteral8::Literal(Literal::unvalidated_non_sync(b"x"));
209        let mut append = ImapAppendUid::new(
210            "INBOX".try_into().expect("valid mailbox"),
211            message,
212            ImapAppendUidOptions::default(),
213        );
214        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
215
216        let bytes = expect_wants_write(&mut append, &mut frag, None);
217        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
218
219        expect_wants_read(&mut append, &mut frag);
220
221        let reply = format!("{tag} OK [APPENDUID 1700000000 7] APPEND completed\r\n");
222        let pair = expect_complete_ok(&mut append, &mut frag, reply.as_bytes())
223            .expect("APPENDUID returned");
224        assert_eq!(1700000000, pair.0.get());
225        assert_eq!(7, pair.1.get());
226    }
227
228    #[test]
229    fn success_without_appenduid_returns_none() {
230        let message = LiteralOrLiteral8::Literal(Literal::unvalidated_non_sync(b"x"));
231        let mut append = ImapAppendUid::new(
232            "INBOX".try_into().expect("valid mailbox"),
233            message,
234            ImapAppendUidOptions::default(),
235        );
236        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
237
238        let bytes = expect_wants_write(&mut append, &mut frag, None);
239        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
240
241        expect_wants_read(&mut append, &mut frag);
242
243        let reply = format!("{tag} OK APPEND completed\r\n");
244        let pair = expect_complete_ok(&mut append, &mut frag, reply.as_bytes());
245        assert!(pair.is_none());
246    }
247
248    #[test]
249    fn tagged_no_returns_no_error() {
250        let message = LiteralOrLiteral8::Literal(Literal::unvalidated_non_sync(b"x"));
251        let mut append = ImapAppendUid::new(
252            "INBOX".try_into().expect("valid mailbox"),
253            message,
254            ImapAppendUidOptions::default(),
255        );
256        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
257
258        let bytes = expect_wants_write(&mut append, &mut frag, None);
259        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
260
261        expect_wants_read(&mut append, &mut frag);
262
263        let reply = format!("{tag} NO mailbox is read-only\r\n");
264        let err = expect_complete_err(&mut append, &mut frag, reply.as_bytes());
265        let ImapAppendUidError::No(text) = err else {
266            panic!("expected ImapAppendUidError::No, got {err:?}");
267        };
268        assert_eq!(text, "mailbox is read-only");
269    }
270
271    #[test]
272    fn bye_returns_bye_error() {
273        let message = LiteralOrLiteral8::Literal(Literal::unvalidated_non_sync(b"x"));
274        let mut append = ImapAppendUid::new(
275            "INBOX".try_into().expect("valid mailbox"),
276            message,
277            ImapAppendUidOptions::default(),
278        );
279        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
280
281        let _ = expect_wants_write(&mut append, &mut frag, None);
282        expect_wants_read(&mut append, &mut frag);
283
284        let err = expect_complete_err(&mut append, &mut frag, b"* BYE going down\r\n");
285        let ImapAppendUidError::Bye(text) = err else {
286            panic!("expected ImapAppendUidError::Bye, got {err:?}");
287        };
288        assert_eq!(text, "going down");
289    }
290
291    fn expect_wants_write(
292        cor: &mut ImapAppendUid,
293        frag: &mut Fragmentizer,
294        arg: Option<&[u8]>,
295    ) -> Vec<u8> {
296        match cor.resume(frag, arg) {
297            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
298            state => panic!("expected WantsWrite, got {state:?}"),
299        }
300    }
301
302    fn expect_wants_read(cor: &mut ImapAppendUid, frag: &mut Fragmentizer) {
303        match cor.resume(frag, None) {
304            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
305            state => panic!("expected WantsRead, got {state:?}"),
306        }
307    }
308
309    fn expect_complete_ok(
310        cor: &mut ImapAppendUid,
311        frag: &mut Fragmentizer,
312        reply: &[u8],
313    ) -> Option<(NonZeroU32, NonZeroU32)> {
314        match cor.resume(frag, Some(reply)) {
315            ImapCoroutineState::Complete(Ok(value)) => value,
316            state => panic!("expected Complete(Ok), got {state:?}"),
317        }
318    }
319
320    fn expect_complete_err(
321        cor: &mut ImapAppendUid,
322        frag: &mut Fragmentizer,
323        reply: &[u8],
324    ) -> ImapAppendUidError {
325        match cor.resume(frag, Some(reply)) {
326            ImapCoroutineState::Complete(Err(err)) => err,
327            state => panic!("expected Complete(Err), got {state:?}"),
328        }
329    }
330
331    fn first_word(line: &str) -> &str {
332        line.split_whitespace()
333            .next()
334            .expect("first whitespace-separated token")
335    }
336}