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}")]
75 No(String),
76 #[error("IMAP COPY failed: BAD {0}")]
78 Bad(String),
79 #[error("IMAP COPY failed: BYE {0}")]
81 Bye(String),
82 #[error("IMAP COPY failed: server did not return a tagged response")]
84 MissingTagged,
85 #[error("IMAP COPY failed: {0}")]
87 Send(#[from] ImapSendError),
88}
89
90#[derive(Clone, Debug, Default, Eq, PartialEq)]
92pub struct ImapMessageCopyOptions {
93 pub uid: bool,
95}
96
97pub struct ImapMessageCopy {
99 state: State,
100}
101
102impl ImapMessageCopy {
103 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
196pub(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}