Skip to main content

io_imap/rfc3501/
examine.rs

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