1use core::{fmt, num::NonZeroU32};
44
45use alloc::{string::String, string::ToString, vec::Vec};
46
47use imap_codec::{
48 CommandCodec,
49 fragmentizer::Fragmentizer,
50 imap_types::{
51 command::{Command, CommandBody},
52 core::TagGenerator,
53 response::{Data, StatusKind, Tagged},
54 },
55};
56use log::trace;
57use thiserror::Error;
58
59use crate::{coroutine::*, imap_try, send::*};
60
61#[derive(Clone, Debug, Error)]
63pub enum ImapMailboxExpungeError {
64 #[error("IMAP EXPUNGE failed: NO {0}")]
66 No(String),
67 #[error("IMAP EXPUNGE failed: BAD {0}")]
69 Bad(String),
70 #[error("IMAP EXPUNGE failed: BYE {0}")]
72 Bye(String),
73 #[error("IMAP EXPUNGE failed: server did not return a tagged response")]
75 MissingTagged,
76 #[error("IMAP EXPUNGE failed: {0}")]
78 Send(#[from] ImapSendError),
79}
80
81pub struct ImapMailboxExpunge {
83 state: State,
84}
85
86impl ImapMailboxExpunge {
87 pub fn new() -> Self {
90 let command = Command {
91 tag: TagGenerator::new().generate(),
92 body: CommandBody::Expunge,
93 };
94
95 trace!("send IMAP command {command:?}");
96
97 let state = State::Send(ImapSend::new(CommandCodec::new(), command));
98
99 Self { state }
100 }
101}
102
103impl Default for ImapMailboxExpunge {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109impl ImapCoroutine for ImapMailboxExpunge {
110 type Yield = ImapYield;
111 type Return = Result<Vec<NonZeroU32>, ImapMailboxExpungeError>;
112
113 fn resume(
114 &mut self,
115 fragmentizer: &mut Fragmentizer,
116 arg: Option<&[u8]>,
117 ) -> ImapCoroutineState<Self::Yield, Self::Return> {
118 match &mut self.state {
119 State::Send(send) => {
120 let out = imap_try!(send, fragmentizer, arg);
121
122 if let Some(bye) = out.bye {
123 let err = ImapMailboxExpungeError::Bye(bye.text.to_string());
124 return ImapCoroutineState::Complete(Err(err));
125 }
126
127 let Some(Tagged { body, .. }) = out.tagged else {
128 let err = ImapMailboxExpungeError::MissingTagged;
129 return ImapCoroutineState::Complete(Err(err));
130 };
131
132 let mut expunged = Vec::new();
133 for data in out.data {
134 if let Data::Expunge(seq) = data {
135 expunged.push(seq);
136 }
137 }
138
139 match body.kind {
140 StatusKind::Ok => ImapCoroutineState::Complete(Ok(expunged)),
141 StatusKind::No => {
142 let err = ImapMailboxExpungeError::No(body.text.to_string());
143 ImapCoroutineState::Complete(Err(err))
144 }
145 StatusKind::Bad => {
146 let err = ImapMailboxExpungeError::Bad(body.text.to_string());
147 ImapCoroutineState::Complete(Err(err))
148 }
149 }
150 }
151 }
152 }
153}
154
155enum State {
156 Send(ImapSend<CommandCodec>),
157}
158
159impl fmt::Display for State {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 match self {
162 Self::Send(_) => f.write_str("send expunge"),
163 }
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use core::str;
170
171 use alloc::{borrow::ToOwned, format};
172
173 use crate::rfc3501::expunge::*;
174
175 #[test]
176 fn success_collects_expunged_seqs() {
177 let mut expunge = ImapMailboxExpunge::new();
178 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
179
180 let bytes = expect_wants_write(&mut expunge, &mut frag, None);
181 let line = str::from_utf8(&bytes).expect("utf8 command");
182 let tag = first_word(line).to_owned();
183 assert!(line.trim_end().ends_with("EXPUNGE"));
184
185 expect_wants_read(&mut expunge, &mut frag);
186
187 let reply =
188 format!("* 3 EXPUNGE\r\n* 3 EXPUNGE\r\n* 7 EXPUNGE\r\n{tag} OK EXPUNGE completed\r\n",);
189 let seqs = expect_complete_ok(&mut expunge, &mut frag, reply.as_bytes());
190 assert_eq!(3, seqs.len());
191 assert_eq!(3, seqs[0].get());
192 assert_eq!(3, seqs[1].get());
193 assert_eq!(7, seqs[2].get());
194 }
195
196 #[test]
197 fn empty_returns_empty_vec() {
198 let mut expunge = ImapMailboxExpunge::new();
199 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
200
201 let bytes = expect_wants_write(&mut expunge, &mut frag, None);
202 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
203
204 expect_wants_read(&mut expunge, &mut frag);
205
206 let reply = format!("{tag} OK EXPUNGE completed\r\n");
207 let seqs = expect_complete_ok(&mut expunge, &mut frag, reply.as_bytes());
208 assert!(seqs.is_empty());
209 }
210
211 #[test]
212 fn tagged_no_returns_no_error() {
213 let mut expunge = ImapMailboxExpunge::new();
214 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
215
216 let bytes = expect_wants_write(&mut expunge, &mut frag, None);
217 let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
218
219 expect_wants_read(&mut expunge, &mut frag);
220
221 let reply = format!("{tag} NO mailbox is read-only\r\n");
222 let err = expect_complete_err(&mut expunge, &mut frag, reply.as_bytes());
223 let ImapMailboxExpungeError::No(text) = err else {
224 panic!("expected ImapMailboxExpungeError::No, got {err:?}");
225 };
226 assert_eq!(text, "mailbox is read-only");
227 }
228
229 #[test]
230 fn bye_returns_bye_error() {
231 let mut expunge = ImapMailboxExpunge::new();
232 let mut frag = Fragmentizer::new(50 * 1024 * 1024);
233
234 let _ = expect_wants_write(&mut expunge, &mut frag, None);
235 expect_wants_read(&mut expunge, &mut frag);
236
237 let err = expect_complete_err(&mut expunge, &mut frag, b"* BYE going down\r\n");
238 let ImapMailboxExpungeError::Bye(text) = err else {
239 panic!("expected ImapMailboxExpungeError::Bye, got {err:?}");
240 };
241 assert_eq!(text, "going down");
242 }
243
244 fn expect_wants_write(
245 cor: &mut ImapMailboxExpunge,
246 frag: &mut Fragmentizer,
247 arg: Option<&[u8]>,
248 ) -> Vec<u8> {
249 match cor.resume(frag, arg) {
250 ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
251 state => panic!("expected WantsWrite, got {state:?}"),
252 }
253 }
254
255 fn expect_wants_read(cor: &mut ImapMailboxExpunge, frag: &mut Fragmentizer) {
256 match cor.resume(frag, None) {
257 ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
258 state => panic!("expected WantsRead, got {state:?}"),
259 }
260 }
261
262 fn expect_complete_ok(
263 cor: &mut ImapMailboxExpunge,
264 frag: &mut Fragmentizer,
265 reply: &[u8],
266 ) -> Vec<NonZeroU32> {
267 match cor.resume(frag, Some(reply)) {
268 ImapCoroutineState::Complete(Ok(value)) => value,
269 state => panic!("expected Complete(Ok), got {state:?}"),
270 }
271 }
272
273 fn expect_complete_err(
274 cor: &mut ImapMailboxExpunge,
275 frag: &mut Fragmentizer,
276 reply: &[u8],
277 ) -> ImapMailboxExpungeError {
278 match cor.resume(frag, Some(reply)) {
279 ImapCoroutineState::Complete(Err(err)) => err,
280 state => panic!("expected Complete(Err), got {state:?}"),
281 }
282 }
283
284 fn first_word(line: &str) -> &str {
285 line.split_whitespace()
286 .next()
287 .expect("first whitespace-separated token")
288 }
289}