Skip to main content

io_imap/rfc3501/
expunge.rs

1//! IMAP EXPUNGE coroutine returning the expunged sequence numbers.
2//!
3//! # Example
4//!
5//! ```rust,no_run
6//! use std::{
7//!     io::{Read, Write},
8//!     net::TcpStream,
9//! };
10//!
11//! use io_imap::{
12//!     codec::fragmentizer::Fragmentizer,
13//!     coroutine::{ImapCoroutine, ImapCoroutineState, ImapYield},
14//!     rfc3501::expunge::ImapMailboxExpunge,
15//! };
16//!
17//! // Ready stream needed (TCP-connected, TLS-negociated, IMAP-authenticated)
18//! let mut stream = TcpStream::connect("localhost:143").unwrap();
19//!
20//! let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
21//! let mut buf = [0u8; 4096];
22//!
23//! let mut coroutine = ImapMailboxExpunge::new();
24//! let mut arg = None;
25//!
26//! let expunged = loop {
27//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
28//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
29//!             stream.write_all(&bytes).unwrap();
30//!         }
31//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
32//!             let n = stream.read(&mut buf).unwrap();
33//!             arg = Some(&buf[..n]);
34//!         }
35//!         ImapCoroutineState::Complete(Ok(expunged)) => break expunged,
36//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
37//!     }
38//! };
39//!
40//! println!("{expunged:?}");
41//! ```
42
43use 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/// Failure causes during the IMAP EXPUNGE flow.
62#[derive(Clone, Debug, Error)]
63pub enum ImapMailboxExpungeError {
64    #[error("IMAP EXPUNGE failed: NO {0}")]
65    No(String),
66    #[error("IMAP EXPUNGE failed: BAD {0}")]
67    Bad(String),
68    #[error("IMAP EXPUNGE failed: BYE {0}")]
69    Bye(String),
70
71    #[error("IMAP EXPUNGE failed: server did not return a tagged response")]
72    MissingTagged,
73
74    #[error("IMAP EXPUNGE failed: {0}")]
75    Send(#[from] SendImapCommandError),
76}
77
78/// I/O-free IMAP EXPUNGE coroutine.
79pub struct ImapMailboxExpunge {
80    state: State,
81}
82
83impl ImapMailboxExpunge {
84    pub fn new() -> Self {
85        let command = Command {
86            tag: TagGenerator::new().generate(),
87            body: CommandBody::Expunge,
88        };
89
90        trace!("send IMAP command {command:?}");
91
92        let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
93
94        Self { state }
95    }
96}
97
98impl Default for ImapMailboxExpunge {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104impl ImapCoroutine for ImapMailboxExpunge {
105    type Yield = ImapYield;
106    type Return = Result<Vec<NonZeroU32>, ImapMailboxExpungeError>;
107
108    fn resume(
109        &mut self,
110        fragmentizer: &mut Fragmentizer,
111        arg: Option<&[u8]>,
112    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
113        loop {
114            trace!("expunge: {}", self.state);
115
116            match &mut self.state {
117                State::Send(send) => {
118                    let out = imap_try!(send, fragmentizer, arg);
119
120                    if let Some(bye) = out.bye {
121                        let err = ImapMailboxExpungeError::Bye(bye.text.to_string());
122                        return ImapCoroutineState::Complete(Err(err));
123                    }
124
125                    let Some(Tagged { body, .. }) = out.tagged else {
126                        let err = ImapMailboxExpungeError::MissingTagged;
127                        return ImapCoroutineState::Complete(Err(err));
128                    };
129
130                    let mut expunged = Vec::new();
131                    for data in out.data {
132                        if let Data::Expunge(seq) = data {
133                            expunged.push(seq);
134                        }
135                    }
136
137                    return match body.kind {
138                        StatusKind::Ok => ImapCoroutineState::Complete(Ok(expunged)),
139                        StatusKind::No => {
140                            let err = ImapMailboxExpungeError::No(body.text.to_string());
141                            ImapCoroutineState::Complete(Err(err))
142                        }
143                        StatusKind::Bad => {
144                            let err = ImapMailboxExpungeError::Bad(body.text.to_string());
145                            ImapCoroutineState::Complete(Err(err))
146                        }
147                    };
148                }
149            }
150        }
151    }
152}
153
154enum State {
155    Send(SendImapCommand<CommandCodec>),
156}
157
158impl fmt::Display for State {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        match self {
161            Self::Send(_) => f.write_str("send expunge"),
162        }
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use core::str;
169
170    use alloc::borrow::ToOwned;
171
172    use super::*;
173
174    #[test]
175    fn success_collects_expunged_seqs() {
176        let mut expunge = ImapMailboxExpunge::new();
177        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
178
179        let bytes = expect_wants_write(&mut expunge, &mut frag, None);
180        let line = str::from_utf8(&bytes).expect("utf8 command");
181        let tag = first_word(line).to_owned();
182        assert!(line.trim_end().ends_with("EXPUNGE"));
183
184        expect_wants_read(&mut expunge, &mut frag);
185
186        let reply =
187            format!("* 3 EXPUNGE\r\n* 3 EXPUNGE\r\n* 7 EXPUNGE\r\n{tag} OK EXPUNGE completed\r\n",);
188        let seqs = expect_complete_ok(&mut expunge, &mut frag, reply.as_bytes());
189        assert_eq!(3, seqs.len());
190        assert_eq!(3, seqs[0].get());
191        assert_eq!(3, seqs[1].get());
192        assert_eq!(7, seqs[2].get());
193    }
194
195    #[test]
196    fn empty_returns_empty_vec() {
197        let mut expunge = ImapMailboxExpunge::new();
198        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
199
200        let bytes = expect_wants_write(&mut expunge, &mut frag, None);
201        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
202
203        expect_wants_read(&mut expunge, &mut frag);
204
205        let reply = format!("{tag} OK EXPUNGE completed\r\n");
206        let seqs = expect_complete_ok(&mut expunge, &mut frag, reply.as_bytes());
207        assert!(seqs.is_empty());
208    }
209
210    #[test]
211    fn tagged_no_returns_no_error() {
212        let mut expunge = ImapMailboxExpunge::new();
213        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
214
215        let bytes = expect_wants_write(&mut expunge, &mut frag, None);
216        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
217
218        expect_wants_read(&mut expunge, &mut frag);
219
220        let reply = format!("{tag} NO mailbox is read-only\r\n");
221        let err = expect_complete_err(&mut expunge, &mut frag, reply.as_bytes());
222        let ImapMailboxExpungeError::No(text) = err else {
223            panic!("expected ImapMailboxExpungeError::No, got {err:?}");
224        };
225        assert_eq!(text, "mailbox is read-only");
226    }
227
228    #[test]
229    fn bye_returns_bye_error() {
230        let mut expunge = ImapMailboxExpunge::new();
231        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
232
233        let _ = expect_wants_write(&mut expunge, &mut frag, None);
234        expect_wants_read(&mut expunge, &mut frag);
235
236        let err = expect_complete_err(&mut expunge, &mut frag, b"* BYE going down\r\n");
237        let ImapMailboxExpungeError::Bye(text) = err else {
238            panic!("expected ImapMailboxExpungeError::Bye, got {err:?}");
239        };
240        assert_eq!(text, "going down");
241    }
242
243    // --- utils
244
245    fn expect_wants_write(
246        cor: &mut ImapMailboxExpunge,
247        frag: &mut Fragmentizer,
248        arg: Option<&[u8]>,
249    ) -> Vec<u8> {
250        match cor.resume(frag, arg) {
251            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
252            state => panic!("expected WantsWrite, got {state:?}"),
253        }
254    }
255
256    fn expect_wants_read(cor: &mut ImapMailboxExpunge, frag: &mut Fragmentizer) {
257        match cor.resume(frag, None) {
258            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
259            state => panic!("expected WantsRead, got {state:?}"),
260        }
261    }
262
263    fn expect_complete_ok(
264        cor: &mut ImapMailboxExpunge,
265        frag: &mut Fragmentizer,
266        reply: &[u8],
267    ) -> Vec<NonZeroU32> {
268        match cor.resume(frag, Some(reply)) {
269            ImapCoroutineState::Complete(Ok(value)) => value,
270            state => panic!("expected Complete(Ok), got {state:?}"),
271        }
272    }
273
274    fn expect_complete_err(
275        cor: &mut ImapMailboxExpunge,
276        frag: &mut Fragmentizer,
277        reply: &[u8],
278    ) -> ImapMailboxExpungeError {
279        match cor.resume(frag, Some(reply)) {
280            ImapCoroutineState::Complete(Err(err)) => err,
281            state => panic!("expected Complete(Err), got {state:?}"),
282        }
283    }
284
285    fn first_word(line: &str) -> &str {
286        line.split_whitespace()
287            .next()
288            .expect("first whitespace-separated token")
289    }
290}