Skip to main content

io_imap/rfc3501/
copy.rs

1//! IMAP COPY coroutine surfacing the optional COPYUID triple.
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::copy::{ImapMessageCopy, ImapMessageCopyOptions},
15//! };
16//!
17//! // Ready stream needed (TCP-connected, TLS-negotiated, IMAP-authenticated)
18//! let mut stream = TcpStream::connect("localhost:143").unwrap();
19//!
20//! let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
21//! let mut buf = [0u8; 4096];
22//!
23//! let sequence_set = "1:3".try_into().unwrap();
24//! let mailbox = "Archive".try_into().unwrap();
25//! let opts = ImapMessageCopyOptions::default();
26//! let mut coroutine = ImapMessageCopy::new(sequence_set, mailbox, opts);
27//! let mut arg = None;
28//!
29//! let copyuid = loop {
30//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
31//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
32//!             stream.write_all(&bytes).unwrap();
33//!         }
34//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
35//!             let n = stream.read(&mut buf).unwrap();
36//!             arg = Some(&buf[..n]);
37//!         }
38//!         ImapCoroutineState::Complete(Ok(copyuid)) => break copyuid,
39//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
40//!     }
41//! };
42//!
43//! println!("{copyuid:?}");
44//! ```
45
46use core::fmt;
47
48use alloc::{string::String, string::ToString, vec::Vec};
49
50use imap_codec::{
51    CommandCodec,
52    fragmentizer::Fragmentizer,
53    imap_types::{
54        command::{Command, CommandBody},
55        core::TagGenerator,
56        extensions::uidplus::{UidElement, UidSet},
57        mailbox::Mailbox,
58        response::{Code, StatusKind, Tagged},
59        sequence::SequenceSet,
60    },
61};
62use log::trace;
63use thiserror::Error;
64
65use crate::{coroutine::*, imap_try, rfc3501::mailbox::encode_inplace, send::*};
66
67/// `(uid_validity, source UIDs, destination UIDs)` from COPYUID.
68pub type ImapCopyUid = Option<(u32, Vec<u32>, Vec<u32>)>;
69
70/// Failure causes during the IMAP COPY flow.
71#[derive(Clone, Debug, Error)]
72pub enum ImapMessageCopyError {
73    /// The server rejected the command with a NO response.
74    #[error("IMAP COPY failed: NO {0}")]
75    No(String),
76    /// The server rejected the command with a BAD response.
77    #[error("IMAP COPY failed: BAD {0}")]
78    Bad(String),
79    /// The server closed the session with an untagged BYE.
80    #[error("IMAP COPY failed: BYE {0}")]
81    Bye(String),
82    /// The exchange ended without a tagged response from the server.
83    #[error("IMAP COPY failed: server did not return a tagged response")]
84    MissingTagged,
85    /// The underlying send/receive exchange failed (EOF, decode, framing).
86    #[error("IMAP COPY failed: {0}")]
87    Send(#[from] ImapSendError),
88}
89
90/// Options for [`ImapMessageCopy::new`].
91#[derive(Clone, Debug, Default, Eq, PartialEq)]
92pub struct ImapMessageCopyOptions {
93    /// When `true`, send `UID COPY` and treat `sequence_set` as UIDs.
94    pub uid: bool,
95}
96
97/// I/O-free IMAP COPY coroutine.
98pub struct ImapMessageCopy {
99    state: State,
100}
101
102impl ImapMessageCopy {
103    /// Builds a COPY coroutine copying the `sequence_set` messages to
104    /// `mailbox`.
105    pub fn new(
106        sequence_set: SequenceSet,
107        mut mailbox: Mailbox<'static>,
108        opts: ImapMessageCopyOptions,
109    ) -> Self {
110        encode_inplace(&mut mailbox);
111
112        let command = Command {
113            tag: TagGenerator::new().generate(),
114            body: CommandBody::Copy {
115                sequence_set,
116                mailbox,
117                uid: opts.uid,
118            },
119        };
120
121        trace!("send IMAP command {command:?}");
122
123        let state = State::Send(ImapSend::new(CommandCodec::new(), command));
124
125        Self { state }
126    }
127}
128
129impl ImapCoroutine for ImapMessageCopy {
130    type Yield = ImapYield;
131    type Return = Result<ImapCopyUid, ImapMessageCopyError>;
132
133    fn resume(
134        &mut self,
135        fragmentizer: &mut Fragmentizer,
136        arg: Option<&[u8]>,
137    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
138        match &mut self.state {
139            State::Send(send) => {
140                let out = imap_try!(send, fragmentizer, arg);
141
142                if let Some(bye) = out.bye {
143                    let err = ImapMessageCopyError::Bye(bye.text.to_string());
144                    return ImapCoroutineState::Complete(Err(err));
145                }
146
147                let Some(Tagged { body, .. }) = out.tagged else {
148                    let err = ImapMessageCopyError::MissingTagged;
149                    return ImapCoroutineState::Complete(Err(err));
150                };
151
152                match body.kind {
153                    StatusKind::Ok => {
154                        let copyuid = if let Some(Code::CopyUid {
155                            uid_validity,
156                            source,
157                            destination,
158                        }) = body.code
159                        {
160                            Some((
161                                uid_validity.get(),
162                                uid_set_to_vec(source),
163                                uid_set_to_vec(destination),
164                            ))
165                        } else {
166                            None
167                        };
168                        ImapCoroutineState::Complete(Ok(copyuid))
169                    }
170                    StatusKind::No => {
171                        let err = ImapMessageCopyError::No(body.text.to_string());
172                        ImapCoroutineState::Complete(Err(err))
173                    }
174                    StatusKind::Bad => {
175                        let err = ImapMessageCopyError::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 copy"),
192        }
193    }
194}
195
196/// Expand a `UidSet` into a sorted `Vec<u32>`; shared with MOVE.
197pub(crate) fn uid_set_to_vec(uid_set: UidSet) -> Vec<u32> {
198    let mut uids = Vec::new();
199
200    for elem in uid_set.0 {
201        match elem {
202            UidElement::Single(uid) => uids.push(uid.get()),
203            UidElement::Range(start, end) => {
204                let (lo, hi) = if start <= end {
205                    (start.get(), end.get())
206                } else {
207                    (end.get(), start.get())
208                };
209                for uid in lo..=hi {
210                    uids.push(uid);
211                }
212            }
213        }
214    }
215
216    uids.sort_unstable();
217    uids
218}
219
220#[cfg(test)]
221mod tests {
222    use core::str;
223
224    use alloc::{borrow::ToOwned, format, vec};
225
226    use crate::rfc3501::copy::*;
227
228    #[test]
229    fn success_with_copyuid_returns_uids() {
230        let mut copy = ImapMessageCopy::new(
231            "1:3".try_into().expect("valid sequence set"),
232            "Archive".try_into().expect("valid mailbox"),
233            ImapMessageCopyOptions::default(),
234        );
235        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
236
237        let bytes = expect_wants_write(&mut copy, &mut frag, None);
238        let line = str::from_utf8(&bytes).expect("utf8 command");
239        let tag = first_word(line).to_owned();
240        assert!(line.contains("COPY 1:3 Archive"));
241
242        expect_wants_read(&mut copy, &mut frag);
243
244        let reply = format!("{tag} OK [COPYUID 1700 1:3 10:12] COPY completed\r\n");
245        let copyuid = expect_complete_ok(&mut copy, &mut frag, reply.as_bytes())
246            .expect("server returned COPYUID");
247        let (uid_validity, source, destination) = copyuid;
248        assert_eq!(1700, uid_validity);
249        assert_eq!(vec![1, 2, 3], source);
250        assert_eq!(vec![10, 11, 12], destination);
251    }
252
253    #[test]
254    fn uid_variant_sends_uid_copy() {
255        let mut copy = ImapMessageCopy::new(
256            "42".try_into().expect("valid sequence set"),
257            "Archive".try_into().expect("valid mailbox"),
258            ImapMessageCopyOptions { uid: true },
259        );
260        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
261
262        let bytes = expect_wants_write(&mut copy, &mut frag, None);
263        let line = str::from_utf8(&bytes).expect("utf8 command");
264        assert!(line.contains("UID COPY 42 Archive"));
265    }
266
267    #[test]
268    fn success_without_copyuid_returns_none() {
269        let mut copy = ImapMessageCopy::new(
270            "1".try_into().expect("valid sequence set"),
271            "Archive".try_into().expect("valid mailbox"),
272            ImapMessageCopyOptions::default(),
273        );
274        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
275
276        let bytes = expect_wants_write(&mut copy, &mut frag, None);
277        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
278
279        expect_wants_read(&mut copy, &mut frag);
280
281        let reply = format!("{tag} OK COPY completed\r\n");
282        let copyuid = expect_complete_ok(&mut copy, &mut frag, reply.as_bytes());
283        assert!(copyuid.is_none());
284    }
285
286    #[test]
287    fn tagged_no_returns_no_error() {
288        let mut copy = ImapMessageCopy::new(
289            "1".try_into().expect("valid sequence set"),
290            "Archive".try_into().expect("valid mailbox"),
291            ImapMessageCopyOptions::default(),
292        );
293        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
294
295        let bytes = expect_wants_write(&mut copy, &mut frag, None);
296        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
297
298        expect_wants_read(&mut copy, &mut frag);
299
300        let reply = format!("{tag} NO destination mailbox does not exist\r\n");
301        let err = expect_complete_err(&mut copy, &mut frag, reply.as_bytes());
302        let ImapMessageCopyError::No(text) = err else {
303            panic!("expected ImapMessageCopyError::No, got {err:?}");
304        };
305        assert_eq!(text, "destination mailbox does not exist");
306    }
307
308    #[test]
309    fn bye_returns_bye_error() {
310        let mut copy = ImapMessageCopy::new(
311            "1".try_into().expect("valid sequence set"),
312            "Archive".try_into().expect("valid mailbox"),
313            ImapMessageCopyOptions::default(),
314        );
315        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
316
317        let _ = expect_wants_write(&mut copy, &mut frag, None);
318        expect_wants_read(&mut copy, &mut frag);
319
320        let err = expect_complete_err(&mut copy, &mut frag, b"* BYE going down\r\n");
321        let ImapMessageCopyError::Bye(text) = err else {
322            panic!("expected ImapMessageCopyError::Bye, got {err:?}");
323        };
324        assert_eq!(text, "going down");
325    }
326
327    fn expect_wants_write(
328        cor: &mut ImapMessageCopy,
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 ImapMessageCopy, 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 ImapMessageCopy,
347        frag: &mut Fragmentizer,
348        reply: &[u8],
349    ) -> ImapCopyUid {
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 ImapMessageCopy,
358        frag: &mut Fragmentizer,
359        reply: &[u8],
360    ) -> ImapMessageCopyError {
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}