Skip to main content

io_imap/rfc3501/
lsub.rs

1//! IMAP LSUB coroutine returning subscribed mailbox rows.
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::lsub::ImapMailboxLsub,
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 reference = "".try_into().unwrap();
24//! let pattern = "*".try_into().unwrap();
25//! let mut coroutine = ImapMailboxLsub::new(reference, pattern);
26//! let mut arg = None;
27//!
28//! let mailboxes = loop {
29//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
30//!         ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
31//!             stream.write_all(&bytes).unwrap();
32//!         }
33//!         ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
34//!             let n = stream.read(&mut buf).unwrap();
35//!             arg = Some(&buf[..n]);
36//!         }
37//!         ImapCoroutineState::Complete(Ok(mailboxes)) => break mailboxes,
38//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
39//!     }
40//! };
41//!
42//! println!("{mailboxes:?}");
43//! ```
44
45use core::fmt;
46
47use alloc::{string::String, string::ToString, vec::Vec};
48
49use imap_codec::{
50    CommandCodec,
51    fragmentizer::Fragmentizer,
52    imap_types::{
53        command::{Command, CommandBody},
54        core::TagGenerator,
55        mailbox::{ListMailbox, Mailbox},
56        response::{Data, StatusKind, Tagged},
57    },
58};
59use log::trace;
60use thiserror::Error;
61
62use crate::{
63    coroutine::*,
64    imap_try,
65    rfc3501::{
66        list::ImapMailboxListing,
67        mailbox::{decode_inplace, encode_inplace},
68    },
69    send::*,
70};
71
72/// Failure causes during the IMAP LSUB flow.
73#[derive(Clone, Debug, Error)]
74pub enum ImapMailboxLsubError {
75    #[error("IMAP LSUB failed: NO {0}")]
76    No(String),
77    #[error("IMAP LSUB failed: BAD {0}")]
78    Bad(String),
79    #[error("IMAP LSUB failed: BYE {0}")]
80    Bye(String),
81
82    #[error("IMAP LSUB failed: server did not return a tagged response")]
83    MissingTagged,
84
85    #[error("IMAP LSUB failed: {0}")]
86    Send(#[from] SendImapCommandError),
87}
88
89/// I/O-free IMAP LSUB coroutine.
90pub struct ImapMailboxLsub {
91    state: State,
92}
93
94impl ImapMailboxLsub {
95    pub fn new(mut reference: Mailbox<'static>, mailbox_wildcard: ListMailbox<'static>) -> Self {
96        encode_inplace(&mut reference);
97
98        let command = Command {
99            tag: TagGenerator::new().generate(),
100            body: CommandBody::Lsub {
101                reference,
102                mailbox_wildcard,
103            },
104        };
105
106        trace!("send IMAP command {command:?}");
107
108        let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
109
110        Self { state }
111    }
112}
113
114impl ImapCoroutine for ImapMailboxLsub {
115    type Yield = ImapYield;
116    type Return = Result<ImapMailboxListing, ImapMailboxLsubError>;
117
118    fn resume(
119        &mut self,
120        fragmentizer: &mut Fragmentizer,
121        arg: Option<&[u8]>,
122    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
123        loop {
124            trace!("lsub: {}", self.state);
125
126            match &mut self.state {
127                State::Send(send) => {
128                    let out = imap_try!(send, fragmentizer, arg);
129
130                    if let Some(bye) = out.bye {
131                        let err = ImapMailboxLsubError::Bye(bye.text.to_string());
132                        return ImapCoroutineState::Complete(Err(err));
133                    }
134
135                    let Some(Tagged { body, .. }) = out.tagged else {
136                        let err = ImapMailboxLsubError::MissingTagged;
137                        return ImapCoroutineState::Complete(Err(err));
138                    };
139
140                    let mut mailboxes = Vec::new();
141                    for data in out.data {
142                        if let Data::Lsub {
143                            items,
144                            delimiter,
145                            mailbox,
146                        } = data
147                        {
148                            let mut mailbox = mailbox;
149                            decode_inplace(&mut mailbox);
150                            mailboxes.push((mailbox, delimiter, items));
151                        }
152                    }
153
154                    return match body.kind {
155                        StatusKind::Ok => ImapCoroutineState::Complete(Ok(mailboxes)),
156                        StatusKind::No => {
157                            let err = ImapMailboxLsubError::No(body.text.to_string());
158                            ImapCoroutineState::Complete(Err(err))
159                        }
160                        StatusKind::Bad => {
161                            let err = ImapMailboxLsubError::Bad(body.text.to_string());
162                            ImapCoroutineState::Complete(Err(err))
163                        }
164                    };
165                }
166            }
167        }
168    }
169}
170
171enum State {
172    Send(SendImapCommand<CommandCodec>),
173}
174
175impl fmt::Display for State {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        match self {
178            Self::Send(_) => f.write_str("send lsub"),
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use core::str;
186
187    use alloc::{borrow::ToOwned, vec::Vec};
188
189    use super::*;
190
191    #[test]
192    fn success_returns_rows() {
193        let mut lsub = ImapMailboxLsub::new(
194            "".try_into().expect("valid reference"),
195            "*".try_into().expect("valid pattern"),
196        );
197        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
198
199        let bytes = expect_wants_write(&mut lsub, &mut frag, None);
200        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
201
202        expect_wants_read(&mut lsub, &mut frag);
203
204        let reply = format!("* LSUB () \"/\" INBOX\r\n{tag} OK LSUB completed\r\n");
205        let rows = expect_complete_ok(&mut lsub, &mut frag, reply.as_bytes());
206        assert_eq!(1, rows.len());
207    }
208
209    #[test]
210    fn tagged_no_returns_no_error() {
211        let mut lsub = ImapMailboxLsub::new(
212            "".try_into().expect("valid reference"),
213            "*".try_into().expect("valid pattern"),
214        );
215        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
216
217        let bytes = expect_wants_write(&mut lsub, &mut frag, None);
218        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
219
220        expect_wants_read(&mut lsub, &mut frag);
221
222        let reply = format!("{tag} NO not allowed\r\n");
223        let err = expect_complete_err(&mut lsub, &mut frag, reply.as_bytes());
224        let ImapMailboxLsubError::No(text) = err else {
225            panic!("expected ImapMailboxLsubError::No, got {err:?}");
226        };
227        assert_eq!(text, "not allowed");
228    }
229
230    #[test]
231    fn bye_returns_bye_error() {
232        let mut lsub = ImapMailboxLsub::new(
233            "".try_into().expect("valid reference"),
234            "*".try_into().expect("valid pattern"),
235        );
236        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
237
238        let _ = expect_wants_write(&mut lsub, &mut frag, None);
239        expect_wants_read(&mut lsub, &mut frag);
240
241        let err = expect_complete_err(&mut lsub, &mut frag, b"* BYE going down\r\n");
242        let ImapMailboxLsubError::Bye(text) = err else {
243            panic!("expected ImapMailboxLsubError::Bye, got {err:?}");
244        };
245        assert_eq!(text, "going down");
246    }
247
248    // --- utils
249
250    fn expect_wants_write(
251        cor: &mut ImapMailboxLsub,
252        frag: &mut Fragmentizer,
253        arg: Option<&[u8]>,
254    ) -> Vec<u8> {
255        match cor.resume(frag, arg) {
256            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
257            state => panic!("expected WantsWrite, got {state:?}"),
258        }
259    }
260
261    fn expect_wants_read(cor: &mut ImapMailboxLsub, frag: &mut Fragmentizer) {
262        match cor.resume(frag, None) {
263            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
264            state => panic!("expected WantsRead, got {state:?}"),
265        }
266    }
267
268    fn expect_complete_ok(
269        cor: &mut ImapMailboxLsub,
270        frag: &mut Fragmentizer,
271        reply: &[u8],
272    ) -> ImapMailboxListing {
273        match cor.resume(frag, Some(reply)) {
274            ImapCoroutineState::Complete(Ok(value)) => value,
275            state => panic!("expected Complete(Ok), got {state:?}"),
276        }
277    }
278
279    fn expect_complete_err(
280        cor: &mut ImapMailboxLsub,
281        frag: &mut Fragmentizer,
282        reply: &[u8],
283    ) -> ImapMailboxLsubError {
284        match cor.resume(frag, Some(reply)) {
285            ImapCoroutineState::Complete(Err(err)) => err,
286            state => panic!("expected Complete(Err), got {state:?}"),
287        }
288    }
289
290    fn first_word(line: &str) -> &str {
291        line.split_whitespace()
292            .next()
293            .expect("first whitespace-separated token")
294    }
295}