1use 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
67pub type ImapCopyUid = Option<(u32, Vec<u32>, Vec<u32>)>;
69
70#[derive(Clone, Debug, Error)]
72pub enum ImapMessageCopyError {
73 #[error("IMAP COPY failed: NO {0}")]
74 No(String),
75 #[error("IMAP COPY failed: BAD {0}")]
76 Bad(String),
77 #[error("IMAP COPY failed: BYE {0}")]
78 Bye(String),
79
80 #[error("IMAP COPY failed: server did not return a tagged response")]
81 MissingTagged,
82
83 #[error("IMAP COPY failed: {0}")]
84 Send(#[from] SendImapCommandError),
85}
86
87#[derive(Clone, Debug, Default, Eq, PartialEq)]
89pub struct ImapMessageCopyOptions {
90 pub uid: bool,
92}
93
94pub struct ImapMessageCopy {
96 state: State,
97}
98
99impl ImapMessageCopy {
100 pub fn new(
101 sequence_set: SequenceSet,
102 mut mailbox: Mailbox<'static>,
103 opts: ImapMessageCopyOptions,
104 ) -> Self {
105 encode_inplace(&mut mailbox);
106
107 let command = Command {
108 tag: TagGenerator::new().generate(),
109 body: CommandBody::Copy {
110 sequence_set,
111 mailbox,
112 uid: opts.uid,
113 },
114 };
115
116 trace!("send IMAP command {command:?}");
117
118 let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
119
120 Self { state }
121 }
122}
123
124impl ImapCoroutine for ImapMessageCopy {
125 type Yield = ImapYield;
126 type Return = Result<ImapCopyUid, ImapMessageCopyError>;
127
128 fn resume(
129 &mut self,
130 fragmentizer: &mut Fragmentizer,
131 arg: Option<&[u8]>,
132 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
133 loop {
134 trace!("copy: {}", self.state);
135
136 match &mut self.state {
137 State::Send(send) => {
138 let out = imap_try!(send, fragmentizer, arg);
139
140 if let Some(bye) = out.bye {
141 let err = ImapMessageCopyError::Bye(bye.text.to_string());
142 return ImapCoroutineState::Complete(Err(err));
143 }
144
145 let Some(Tagged { body, .. }) = out.tagged else {
146 let err = ImapMessageCopyError::MissingTagged;
147 return ImapCoroutineState::Complete(Err(err));
148 };
149
150 return match body.kind {
151 StatusKind::Ok => {
152 let copyuid = if let Some(Code::CopyUid {
153 uid_validity,
154 source,
155 destination,
156 }) = body.code
157 {
158 Some((
159 uid_validity.get(),
160 uid_set_to_vec(source),
161 uid_set_to_vec(destination),
162 ))
163 } else {
164 None
165 };
166 ImapCoroutineState::Complete(Ok(copyuid))
167 }
168 StatusKind::No => {
169 let err = ImapMessageCopyError::No(body.text.to_string());
170 ImapCoroutineState::Complete(Err(err))
171 }
172 StatusKind::Bad => {
173 let err = ImapMessageCopyError::Bad(body.text.to_string());
174 ImapCoroutineState::Complete(Err(err))
175 }
176 };
177 }
178 }
179 }
180 }
181}
182
183enum State {
184 Send(SendImapCommand<CommandCodec>),
185}
186
187impl fmt::Display for State {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 match self {
190 Self::Send(_) => f.write_str("send copy"),
191 }
192 }
193}
194
195pub(crate) fn uid_set_to_vec(uid_set: UidSet) -> Vec<u32> {
197 let mut uids = Vec::new();
198
199 for elem in uid_set.0 {
200 match elem {
201 UidElement::Single(uid) => uids.push(uid.get()),
202 UidElement::Range(start, end) => {
203 let (lo, hi) = if start <= end {
204 (start.get(), end.get())
205 } else {
206 (end.get(), start.get())
207 };
208 for uid in lo..=hi {
209 uids.push(uid);
210 }
211 }
212 }
213 }
214
215 uids.sort_unstable();
216 uids
217}
218
219#[cfg(test)]
220mod tests {
221 use core::str;
222
223 use alloc::borrow::ToOwned;
224
225 use super::*;
226
227 #[test]
228 fn success_with_copyuid_returns_uids() {
229 let mut copy = ImapMessageCopy::new(
230 "1:3".try_into().expect("valid sequence set"),
231 "Archive".try_into().expect("valid mailbox"),
232 ImapMessageCopyOptions::default(),
233 );
234 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
235
236 let bytes = expect_wants_write(&mut copy, &mut frag, None);
237 let line = str::from_utf8(&bytes).expect("utf8 command");
238 let tag = first_word(line).to_owned();
239 assert!(line.contains("COPY 1:3 Archive"));
240
241 expect_wants_read(&mut copy, &mut frag);
242
243 let reply = format!("{tag} OK [COPYUID 1700 1:3 10:12] COPY completed\r\n");
244 let copyuid = expect_complete_ok(&mut copy, &mut frag, reply.as_bytes())
245 .expect("server returned COPYUID");
246 let (uid_validity, source, destination) = copyuid;
247 assert_eq!(1700, uid_validity);
248 assert_eq!(vec![1, 2, 3], source);
249 assert_eq!(vec![10, 11, 12], destination);
250 }
251
252 #[test]
253 fn uid_variant_sends_uid_copy() {
254 let mut copy = ImapMessageCopy::new(
255 "42".try_into().expect("valid sequence set"),
256 "Archive".try_into().expect("valid mailbox"),
257 ImapMessageCopyOptions { uid: true },
258 );
259 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
260
261 let bytes = expect_wants_write(&mut copy, &mut frag, None);
262 let line = str::from_utf8(&bytes).expect("utf8 command");
263 assert!(line.contains("UID COPY 42 Archive"));
264 }
265
266 #[test]
267 fn success_without_copyuid_returns_none() {
268 let mut copy = ImapMessageCopy::new(
269 "1".try_into().expect("valid sequence set"),
270 "Archive".try_into().expect("valid mailbox"),
271 ImapMessageCopyOptions::default(),
272 );
273 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
274
275 let bytes = expect_wants_write(&mut copy, &mut frag, None);
276 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
277
278 expect_wants_read(&mut copy, &mut frag);
279
280 let reply = format!("{tag} OK COPY completed\r\n");
281 let copyuid = expect_complete_ok(&mut copy, &mut frag, reply.as_bytes());
282 assert!(copyuid.is_none());
283 }
284
285 #[test]
286 fn tagged_no_returns_no_error() {
287 let mut copy = ImapMessageCopy::new(
288 "1".try_into().expect("valid sequence set"),
289 "Archive".try_into().expect("valid mailbox"),
290 ImapMessageCopyOptions::default(),
291 );
292 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
293
294 let bytes = expect_wants_write(&mut copy, &mut frag, None);
295 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
296
297 expect_wants_read(&mut copy, &mut frag);
298
299 let reply = format!("{tag} NO destination mailbox does not exist\r\n");
300 let err = expect_complete_err(&mut copy, &mut frag, reply.as_bytes());
301 let ImapMessageCopyError::No(text) = err else {
302 panic!("expected ImapMessageCopyError::No, got {err:?}");
303 };
304 assert_eq!(text, "destination mailbox does not exist");
305 }
306
307 #[test]
308 fn bye_returns_bye_error() {
309 let mut copy = ImapMessageCopy::new(
310 "1".try_into().expect("valid sequence set"),
311 "Archive".try_into().expect("valid mailbox"),
312 ImapMessageCopyOptions::default(),
313 );
314 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
315
316 let _ = expect_wants_write(&mut copy, &mut frag, None);
317 expect_wants_read(&mut copy, &mut frag);
318
319 let err = expect_complete_err(&mut copy, &mut frag, b"* BYE going down\r\n");
320 let ImapMessageCopyError::Bye(text) = err else {
321 panic!("expected ImapMessageCopyError::Bye, got {err:?}");
322 };
323 assert_eq!(text, "going down");
324 }
325
326 fn expect_wants_write(
329 cor: &mut ImapMessageCopy,
330 frag: &mut Fragmentizer,
331 arg: Option<&[u8]>,
332 ) -> Vec<u8> {
333 match cor.resume(frag, arg) {
334 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
335 state => panic!("expected WantsWrite, got {state:?}"),
336 }
337 }
338
339 fn expect_wants_read(cor: &mut ImapMessageCopy, frag: &mut Fragmentizer) {
340 match cor.resume(frag, None) {
341 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
342 state => panic!("expected WantsRead, got {state:?}"),
343 }
344 }
345
346 fn expect_complete_ok(
347 cor: &mut ImapMessageCopy,
348 frag: &mut Fragmentizer,
349 reply: &[u8],
350 ) -> ImapCopyUid {
351 match cor.resume(frag, Some(reply)) {
352 ImapCoroutineState::Complete(Ok(value)) => value,
353 state => panic!("expected Complete(Ok), got {state:?}"),
354 }
355 }
356
357 fn expect_complete_err(
358 cor: &mut ImapMessageCopy,
359 frag: &mut Fragmentizer,
360 reply: &[u8],
361 ) -> ImapMessageCopyError {
362 match cor.resume(frag, Some(reply)) {
363 ImapCoroutineState::Complete(Err(err)) => err,
364 state => panic!("expected Complete(Err), got {state:?}"),
365 }
366 }
367
368 fn first_word(line: &str) -> &str {
369 line.split_whitespace()
370 .next()
371 .expect("first whitespace-separated token")
372 }
373}