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