Skip to main content

io_imap/rfc3501/
status.rs

1//! IMAP STATUS coroutine returning the requested status items.
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::status::ImapMailboxStatus,
15//!     types::status::StatusDataItemName,
16//! };
17//!
18//! // Ready stream needed (TCP-connected, TLS-negociated, IMAP-authenticated)
19//! let mut stream = TcpStream::connect("localhost:143").unwrap();
20//!
21//! let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
22//! let mut buf = [0u8; 4096];
23//!
24//! let mailbox = "INBOX".try_into().unwrap();
25//! let item_names = vec![StatusDataItemName::Messages, StatusDataItemName::Recent];
26//! let mut coroutine = ImapMailboxStatus::new(mailbox, item_names);
27//! let mut arg = None;
28//!
29//! let items = loop {
30//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
31//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
32//!             stream.write_all(&bytes).unwrap();
33//!         }
34//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
35//!             let n = stream.read(&mut buf).unwrap();
36//!             arg = Some(&buf[..n]);
37//!         }
38//!         ImapCoroutineState::Complete(Ok(items)) => break items,
39//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
40//!     }
41//! };
42//!
43//! println!("{items:?}");
44//! ```
45
46use core::fmt;
47
48use alloc::{borrow::Cow, 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        mailbox::Mailbox,
57        response::{Data, StatusKind, Tagged},
58        status::{StatusDataItem, StatusDataItemName},
59    },
60};
61use log::trace;
62use thiserror::Error;
63
64use crate::{coroutine::*, imap_try, rfc3501::mailbox::encode_inplace, send::*};
65
66/// Failure causes during the IMAP STATUS flow.
67#[derive(Clone, Debug, Error)]
68pub enum ImapMailboxStatusError {
69    #[error("IMAP STATUS failed: NO {0}")]
70    No(String),
71    #[error("IMAP STATUS failed: BAD {0}")]
72    Bad(String),
73    #[error("IMAP STATUS failed: BYE {0}")]
74    Bye(String),
75
76    #[error("IMAP STATUS failed: server did not return a tagged response")]
77    MissingTagged,
78
79    #[error("IMAP STATUS failed: {0}")]
80    Send(#[from] SendImapCommandError),
81}
82
83/// I/O-free IMAP STATUS coroutine.
84pub struct ImapMailboxStatus {
85    state: State,
86}
87
88impl ImapMailboxStatus {
89    pub fn new(
90        mut mailbox: Mailbox<'static>,
91        item_names: impl Into<Cow<'static, [StatusDataItemName]>>,
92    ) -> Self {
93        encode_inplace(&mut mailbox);
94
95        let command = Command {
96            tag: TagGenerator::new().generate(),
97            body: CommandBody::Status {
98                mailbox,
99                item_names: item_names.into(),
100            },
101        };
102
103        trace!("send IMAP command {command:?}");
104
105        let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
106
107        Self { state }
108    }
109}
110
111impl ImapCoroutine for ImapMailboxStatus {
112    type Yield = ImapYield;
113    type Return = Result<Vec<StatusDataItem>, ImapMailboxStatusError>;
114
115    fn resume(
116        &mut self,
117        fragmentizer: &mut Fragmentizer,
118        arg: Option<&[u8]>,
119    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
120        loop {
121            trace!("status: {}", self.state);
122
123            match &mut self.state {
124                State::Send(send) => {
125                    let out = imap_try!(send, fragmentizer, arg);
126
127                    if let Some(bye) = out.bye {
128                        let err = ImapMailboxStatusError::Bye(bye.text.to_string());
129                        return ImapCoroutineState::Complete(Err(err));
130                    }
131
132                    let Some(Tagged { body, .. }) = out.tagged else {
133                        let err = ImapMailboxStatusError::MissingTagged;
134                        return ImapCoroutineState::Complete(Err(err));
135                    };
136
137                    let mut items = Vec::new();
138                    for data in out.data {
139                        if let Data::Status {
140                            mailbox: _,
141                            items: status_items,
142                        } = data
143                        {
144                            items.extend(status_items.into_owned());
145                        }
146                    }
147
148                    return match body.kind {
149                        StatusKind::Ok => ImapCoroutineState::Complete(Ok(items)),
150                        StatusKind::No => {
151                            let err = ImapMailboxStatusError::No(body.text.to_string());
152                            ImapCoroutineState::Complete(Err(err))
153                        }
154                        StatusKind::Bad => {
155                            let err = ImapMailboxStatusError::Bad(body.text.to_string());
156                            ImapCoroutineState::Complete(Err(err))
157                        }
158                    };
159                }
160            }
161        }
162    }
163}
164
165enum State {
166    Send(SendImapCommand<CommandCodec>),
167}
168
169impl fmt::Display for State {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        match self {
172            Self::Send(_) => f.write_str("send status"),
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use core::str;
180
181    use alloc::{borrow::ToOwned, vec, vec::Vec};
182
183    use super::*;
184
185    fn items() -> Vec<StatusDataItemName> {
186        vec![StatusDataItemName::Messages, StatusDataItemName::Recent]
187    }
188
189    #[test]
190    fn success_returns_items() {
191        let mut status =
192            ImapMailboxStatus::new("INBOX".try_into().expect("valid mailbox"), items());
193        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
194
195        let bytes = expect_wants_write(&mut status, &mut frag, None);
196        let line = str::from_utf8(&bytes).expect("utf8 command");
197        let tag = first_word(line).to_owned();
198        assert!(line.contains("STATUS INBOX"));
199
200        expect_wants_read(&mut status, &mut frag);
201
202        let reply =
203            format!("* STATUS INBOX (MESSAGES 42 RECENT 3)\r\n{tag} OK STATUS completed\r\n");
204        let out = expect_complete_ok(&mut status, &mut frag, reply.as_bytes());
205        assert_eq!(2, out.len());
206    }
207
208    #[test]
209    fn tagged_no_returns_no_error() {
210        let mut status =
211            ImapMailboxStatus::new("INBOX".try_into().expect("valid mailbox"), items());
212        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
213
214        let bytes = expect_wants_write(&mut status, &mut frag, None);
215        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
216
217        expect_wants_read(&mut status, &mut frag);
218
219        let reply = format!("{tag} NO mailbox does not exist\r\n");
220        let err = expect_complete_err(&mut status, &mut frag, reply.as_bytes());
221        let ImapMailboxStatusError::No(text) = err else {
222            panic!("expected ImapMailboxStatusError::No, got {err:?}");
223        };
224        assert_eq!(text, "mailbox does not exist");
225    }
226
227    #[test]
228    fn bye_returns_bye_error() {
229        let mut status =
230            ImapMailboxStatus::new("INBOX".try_into().expect("valid mailbox"), items());
231        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
232
233        let _ = expect_wants_write(&mut status, &mut frag, None);
234        expect_wants_read(&mut status, &mut frag);
235
236        let err = expect_complete_err(&mut status, &mut frag, b"* BYE going down\r\n");
237        let ImapMailboxStatusError::Bye(text) = err else {
238            panic!("expected ImapMailboxStatusError::Bye, got {err:?}");
239        };
240        assert_eq!(text, "going down");
241    }
242
243    // --- utils
244
245    fn expect_wants_write(
246        cor: &mut ImapMailboxStatus,
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 ImapMailboxStatus, 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 ImapMailboxStatus,
265        frag: &mut Fragmentizer,
266        reply: &[u8],
267    ) -> Vec<StatusDataItem> {
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 ImapMailboxStatus,
276        frag: &mut Fragmentizer,
277        reply: &[u8],
278    ) -> ImapMailboxStatusError {
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}