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