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