Skip to main content

io_imap/rfc3501/
select.rs

1//! IMAP SELECT coroutine; accepts SELECT parameters (RFC 4466) to opt
2//! into CONDSTORE/QRESYNC extras.
3//!
4//! # Example
5//!
6//! ```rust,no_run
7//! use std::{
8//!     io::{Read, Write},
9//!     net::TcpStream,
10//! };
11//!
12//! use io_imap::{
13//!     codec::fragmentizer::Fragmentizer,
14//!     coroutine::{ImapCoroutine, ImapCoroutineState, ImapYield},
15//!     rfc3501::select::{ImapMailboxSelect, ImapMailboxSelectOptions},
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 opts = ImapMailboxSelectOptions::default();
26//! let mut coroutine = ImapMailboxSelect::new(mailbox, opts);
27//! let mut arg = None;
28//!
29//! let data = 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(data)) => break data,
39//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
40//!     }
41//! };
42//!
43//! println!("{data:?}");
44//! ```
45
46use core::{fmt, num::NonZeroU32};
47
48use alloc::{string::String, string::ToString, vec::Vec};
49
50use imap_codec::{
51    CommandCodec,
52    fragmentizer::Fragmentizer,
53    imap_types::{
54        command::{Command, CommandBody, SelectParameter},
55        core::{TagGenerator, Vec1},
56        fetch::MessageDataItem,
57        flag::{Flag, FlagPerm},
58        mailbox::Mailbox,
59        response::{Code, Data, StatusBody, StatusKind, Tagged},
60        sequence::SequenceSet,
61    },
62};
63use log::trace;
64use thiserror::Error;
65
66use crate::{coroutine::*, imap_try, rfc3501::mailbox::encode_inplace, send::*};
67
68/// Failure causes during the IMAP SELECT flow.
69#[derive(Clone, Debug, Error)]
70pub enum ImapMailboxSelectError {
71    #[error("IMAP SELECT failed: NO {0}")]
72    No(String),
73    #[error("IMAP SELECT failed: BAD {0}")]
74    Bad(String),
75    #[error("IMAP SELECT failed: BYE {0}")]
76    Bye(String),
77
78    #[error("IMAP SELECT failed: server did not return a tagged response")]
79    MissingTagged,
80
81    #[error("IMAP SELECT failed: {0}")]
82    Send(#[from] SendImapCommandError),
83}
84
85/// Decoded SELECT (or EXAMINE) response. CONDSTORE/QRESYNC extras
86/// (`highest_mod_seq`, `vanished_earlier`, `changed`) stay empty on the
87/// base call.
88#[derive(Clone, Debug, Default)]
89pub struct SelectData {
90    pub flags: Option<Vec<Flag<'static>>>,
91    pub exists: Option<u32>,
92    pub recent: Option<u32>,
93    pub unseen: Option<NonZeroU32>,
94    pub permanent_flags: Option<Vec<FlagPerm<'static>>>,
95    pub uid_next: Option<NonZeroU32>,
96    pub uid_validity: Option<NonZeroU32>,
97    pub highest_mod_seq: Option<u64>,
98    pub vanished_earlier: Vec<NonZeroU32>,
99    pub changed: Vec<SelectFetch>,
100}
101
102/// Implicit FETCH returned during a QRESYNC SELECT.
103#[derive(Clone, Debug)]
104pub struct SelectFetch {
105    pub seq: NonZeroU32,
106    pub items: Vec1<MessageDataItem<'static>>,
107}
108
109/// Options for [`ImapMailboxSelect::new`].
110#[derive(Clone, Debug, Default, Eq, PartialEq)]
111pub struct ImapMailboxSelectOptions {
112    /// SELECT/EXAMINE parameters (RFC 4466), e.g. CONDSTORE/QRESYNC.
113    pub parameters: Vec<SelectParameter>,
114}
115
116/// I/O-free IMAP SELECT coroutine.
117pub struct ImapMailboxSelect {
118    state: State,
119}
120
121impl ImapMailboxSelect {
122    pub fn new(mut mailbox: Mailbox<'static>, opts: ImapMailboxSelectOptions) -> Self {
123        encode_inplace(&mut mailbox);
124
125        let command = Command {
126            tag: TagGenerator::new().generate(),
127            body: CommandBody::Select {
128                mailbox,
129                parameters: opts.parameters,
130            },
131        };
132
133        trace!("send IMAP command {command:?}");
134
135        let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
136
137        Self { state }
138    }
139}
140
141impl ImapCoroutine for ImapMailboxSelect {
142    type Yield = ImapYield;
143    type Return = Result<SelectData, ImapMailboxSelectError>;
144
145    fn resume(
146        &mut self,
147        fragmentizer: &mut Fragmentizer,
148        arg: Option<&[u8]>,
149    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
150        loop {
151            trace!("select: {}", self.state);
152
153            match &mut self.state {
154                State::Send(send) => {
155                    let out = imap_try!(send, fragmentizer, arg);
156
157                    if let Some(bye) = out.bye {
158                        let err = ImapMailboxSelectError::Bye(bye.text.to_string());
159                        return ImapCoroutineState::Complete(Err(err));
160                    }
161
162                    let Some(Tagged { body, .. }) = out.tagged else {
163                        let err = ImapMailboxSelectError::MissingTagged;
164                        return ImapCoroutineState::Complete(Err(err));
165                    };
166
167                    let mut output = SelectData::default();
168
169                    for data in out.data {
170                        match data {
171                            Data::Flags(flags) => output.flags = Some(flags),
172                            Data::Exists(count) => output.exists = Some(count),
173                            Data::Recent(count) => output.recent = Some(count),
174                            Data::Fetch { seq, items } => {
175                                output.changed.push(SelectFetch { seq, items });
176                            }
177                            Data::Vanished {
178                                earlier,
179                                known_uids,
180                            } if earlier => {
181                                output.vanished_earlier.extend(expand_uid_set(&known_uids));
182                            }
183                            _ => {}
184                        }
185                    }
186
187                    for StatusBody { kind, code, .. } in out.untagged {
188                        if let StatusKind::Ok = kind {
189                            match code {
190                                Some(Code::Unseen(seq)) => output.unseen = Some(seq),
191                                Some(Code::PermanentFlags(flags)) => {
192                                    output.permanent_flags = Some(flags)
193                                }
194                                Some(Code::UidNext(uid)) => output.uid_next = Some(uid),
195                                Some(Code::UidValidity(uid)) => output.uid_validity = Some(uid),
196                                Some(Code::HighestModSeq(modseq)) => {
197                                    output.highest_mod_seq = Some(modseq.get());
198                                }
199                                _ => {}
200                            }
201                        }
202                    }
203
204                    return match body.kind {
205                        StatusKind::Ok => ImapCoroutineState::Complete(Ok(output)),
206                        StatusKind::No => {
207                            let err = ImapMailboxSelectError::No(body.text.to_string());
208                            ImapCoroutineState::Complete(Err(err))
209                        }
210                        StatusKind::Bad => {
211                            let err = ImapMailboxSelectError::Bad(body.text.to_string());
212                            ImapCoroutineState::Complete(Err(err))
213                        }
214                    };
215                }
216            }
217        }
218    }
219}
220
221enum State {
222    Send(SendImapCommand<CommandCodec>),
223}
224
225impl fmt::Display for State {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        match self {
228            Self::Send(_) => f.write_str("send select"),
229        }
230    }
231}
232
233/// Expand `VANISHED (EARLIER)` uid-set to concrete UIDs (RFC 7162 ยง3.2.10
234/// forbids `*`, so `u32::MAX` is a safe ceiling).
235fn expand_uid_set(uid_set: &SequenceSet) -> Vec<NonZeroU32> {
236    let max = NonZeroU32::new(u32::MAX).unwrap();
237    uid_set.iter(max).collect()
238}
239
240#[cfg(test)]
241mod tests {
242    use core::str;
243
244    use alloc::{borrow::ToOwned, vec::Vec};
245
246    use super::*;
247
248    #[test]
249    fn success_collects_response() {
250        let mut select = ImapMailboxSelect::new(
251            "INBOX".try_into().expect("valid mailbox"),
252            ImapMailboxSelectOptions::default(),
253        );
254        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
255
256        let bytes = expect_wants_write(&mut select, &mut frag, None);
257        let line = str::from_utf8(&bytes).expect("utf8 command");
258        let tag = first_word(line).to_owned();
259        assert!(line.contains("SELECT INBOX"));
260
261        expect_wants_read(&mut select, &mut frag);
262
263        let reply = format!(
264            "* FLAGS (\\Seen)\r\n\
265             * 42 EXISTS\r\n\
266             * 7 RECENT\r\n\
267             * OK [UIDVALIDITY 1700] uid validity\r\n\
268             {tag} OK [READ-WRITE] SELECT completed\r\n",
269        );
270        let data = expect_complete_ok(&mut select, &mut frag, reply.as_bytes());
271        assert_eq!(Some(42), data.exists);
272        assert_eq!(Some(7), data.recent);
273        assert_eq!(1700, data.uid_validity.expect("uid validity").get());
274    }
275
276    #[test]
277    fn tagged_no_returns_no_error() {
278        let mut select = ImapMailboxSelect::new(
279            "INBOX".try_into().expect("valid mailbox"),
280            ImapMailboxSelectOptions::default(),
281        );
282        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
283
284        let bytes = expect_wants_write(&mut select, &mut frag, None);
285        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
286
287        expect_wants_read(&mut select, &mut frag);
288
289        let reply = format!("{tag} NO mailbox does not exist\r\n");
290        let err = expect_complete_err(&mut select, &mut frag, reply.as_bytes());
291        let ImapMailboxSelectError::No(text) = err else {
292            panic!("expected ImapMailboxSelectError::No, got {err:?}");
293        };
294        assert_eq!(text, "mailbox does not exist");
295    }
296
297    #[test]
298    fn bye_returns_bye_error() {
299        let mut select = ImapMailboxSelect::new(
300            "INBOX".try_into().expect("valid mailbox"),
301            ImapMailboxSelectOptions::default(),
302        );
303        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
304
305        let _ = expect_wants_write(&mut select, &mut frag, None);
306        expect_wants_read(&mut select, &mut frag);
307
308        let err = expect_complete_err(&mut select, &mut frag, b"* BYE going down\r\n");
309        let ImapMailboxSelectError::Bye(text) = err else {
310            panic!("expected ImapMailboxSelectError::Bye, got {err:?}");
311        };
312        assert_eq!(text, "going down");
313    }
314
315    // --- utils
316
317    fn expect_wants_write(
318        cor: &mut ImapMailboxSelect,
319        frag: &mut Fragmentizer,
320        arg: Option<&[u8]>,
321    ) -> Vec<u8> {
322        match cor.resume(frag, arg) {
323            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => bytes,
324            state => panic!("expected WantsWrite, got {state:?}"),
325        }
326    }
327
328    fn expect_wants_read(cor: &mut ImapMailboxSelect, frag: &mut Fragmentizer) {
329        match cor.resume(frag, None) {
330            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {}
331            state => panic!("expected WantsRead, got {state:?}"),
332        }
333    }
334
335    fn expect_complete_ok(
336        cor: &mut ImapMailboxSelect,
337        frag: &mut Fragmentizer,
338        reply: &[u8],
339    ) -> SelectData {
340        match cor.resume(frag, Some(reply)) {
341            ImapCoroutineState::Complete(Ok(value)) => value,
342            state => panic!("expected Complete(Ok), got {state:?}"),
343        }
344    }
345
346    fn expect_complete_err(
347        cor: &mut ImapMailboxSelect,
348        frag: &mut Fragmentizer,
349        reply: &[u8],
350    ) -> ImapMailboxSelectError {
351        match cor.resume(frag, Some(reply)) {
352            ImapCoroutineState::Complete(Err(err)) => err,
353            state => panic!("expected Complete(Err), got {state:?}"),
354        }
355    }
356
357    fn first_word(line: &str) -> &str {
358        line.split_whitespace()
359            .next()
360            .expect("first whitespace-separated token")
361    }
362}