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-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 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::{SelectData, SelectFetch},
69    },
70    send::*,
71};
72
73/// Decoded EXAMINE response (alias of [`SelectData`]).
74pub type ExamineData = SelectData;
75/// Implicit FETCH item from a QRESYNC EXAMINE (alias of [`SelectFetch`]).
76pub type ExamineFetch = SelectFetch;
77
78/// Failure causes during the IMAP EXAMINE flow.
79#[derive(Clone, Debug, Error)]
80pub enum ImapMailboxExamineError {
81    #[error("IMAP EXAMINE failed: NO {0}")]
82    No(String),
83    #[error("IMAP EXAMINE failed: BAD {0}")]
84    Bad(String),
85    #[error("IMAP EXAMINE failed: BYE {0}")]
86    Bye(String),
87
88    #[error("IMAP EXAMINE failed: server did not return a tagged response")]
89    MissingTagged,
90
91    #[error("IMAP EXAMINE failed: {0}")]
92    Send(#[from] SendImapCommandError),
93}
94
95/// Options for [`ImapMailboxExamine::new`].
96#[derive(Clone, Debug, Default, Eq, PartialEq)]
97pub struct ImapMailboxExamineOptions {
98    /// SELECT/EXAMINE parameters (RFC 4466), e.g. CONDSTORE/QRESYNC.
99    pub parameters: Vec<SelectParameter>,
100}
101
102/// I/O-free IMAP EXAMINE coroutine.
103pub struct ImapMailboxExamine {
104    state: State,
105}
106
107impl ImapMailboxExamine {
108    pub fn new(mut mailbox: Mailbox<'static>, opts: ImapMailboxExamineOptions) -> Self {
109        encode_inplace(&mut mailbox);
110
111        let command = Command {
112            tag: TagGenerator::new().generate(),
113            body: CommandBody::Examine {
114                mailbox,
115                parameters: opts.parameters,
116            },
117        };
118
119        trace!("send IMAP command {command:?}");
120
121        let state = State::Send(SendImapCommand::new(CommandCodec::new(), command));
122
123        Self { state }
124    }
125}
126
127impl ImapCoroutine for ImapMailboxExamine {
128    type Yield = ImapYield;
129    type Return = Result<ExamineData, ImapMailboxExamineError>;
130
131    fn resume(
132        &mut self,
133        fragmentizer: &mut Fragmentizer,
134        arg: Option<&[u8]>,
135    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
136        loop {
137            trace!("examine: {}", self.state);
138
139            match &mut self.state {
140                State::Send(send) => {
141                    let out = imap_try!(send, fragmentizer, arg);
142
143                    if let Some(bye) = out.bye {
144                        let err = ImapMailboxExamineError::Bye(bye.text.to_string());
145                        return ImapCoroutineState::Complete(Err(err));
146                    }
147
148                    let Some(Tagged { body, .. }) = out.tagged else {
149                        let err = ImapMailboxExamineError::MissingTagged;
150                        return ImapCoroutineState::Complete(Err(err));
151                    };
152
153                    let mut output = ExamineData::default();
154
155                    for data in out.data {
156                        match data {
157                            Data::Flags(flags) => output.flags = Some(flags),
158                            Data::Exists(count) => output.exists = Some(count),
159                            Data::Recent(count) => output.recent = Some(count),
160                            Data::Fetch { seq, items } => {
161                                output.changed.push(ExamineFetch { seq, items });
162                            }
163                            Data::Vanished {
164                                earlier,
165                                known_uids,
166                            } if earlier => {
167                                output.vanished_earlier.extend(expand_uid_set(&known_uids));
168                            }
169                            _ => {}
170                        }
171                    }
172
173                    for StatusBody { kind, code, .. } in out.untagged {
174                        if let StatusKind::Ok = kind {
175                            match code {
176                                Some(Code::Unseen(seq)) => output.unseen = Some(seq),
177                                Some(Code::PermanentFlags(flags)) => {
178                                    output.permanent_flags = Some(flags)
179                                }
180                                Some(Code::UidNext(uid)) => output.uid_next = Some(uid),
181                                Some(Code::UidValidity(uid)) => output.uid_validity = Some(uid),
182                                Some(Code::HighestModSeq(modseq)) => {
183                                    output.highest_mod_seq = Some(modseq.get());
184                                }
185                                _ => {}
186                            }
187                        }
188                    }
189
190                    return match body.kind {
191                        StatusKind::Ok => ImapCoroutineState::Complete(Ok(output)),
192                        StatusKind::No => {
193                            let err = ImapMailboxExamineError::No(body.text.to_string());
194                            ImapCoroutineState::Complete(Err(err))
195                        }
196                        StatusKind::Bad => {
197                            let err = ImapMailboxExamineError::Bad(body.text.to_string());
198                            ImapCoroutineState::Complete(Err(err))
199                        }
200                    };
201                }
202            }
203        }
204    }
205}
206
207enum State {
208    Send(SendImapCommand<CommandCodec>),
209}
210
211impl fmt::Display for State {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        match self {
214            Self::Send(_) => f.write_str("send examine"),
215        }
216    }
217}
218
219/// Expand `VANISHED (EARLIER)` uid-set to concrete UIDs (RFC 7162 ยง3.2.10
220/// forbids `*`, so `u32::MAX` is a safe ceiling).
221fn expand_uid_set(uid_set: &SequenceSet) -> Vec<NonZeroU32> {
222    let max = NonZeroU32::new(u32::MAX).unwrap();
223    uid_set.iter(max).collect()
224}
225
226#[cfg(test)]
227mod tests {
228    use core::str;
229
230    use alloc::borrow::ToOwned;
231
232    use super::*;
233
234    #[test]
235    fn success_collects_response() {
236        let mut examine = ImapMailboxExamine::new(
237            "INBOX".try_into().expect("valid mailbox"),
238            ImapMailboxExamineOptions::default(),
239        );
240        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
241
242        let bytes = expect_wants_write(&mut examine, &mut frag, None);
243        let line = str::from_utf8(&bytes).expect("utf8 command");
244        let tag = first_word(line).to_owned();
245        assert!(line.contains("EXAMINE INBOX"));
246
247        expect_wants_read(&mut examine, &mut frag);
248
249        let reply = format!(
250            "* FLAGS (\\Seen)\r\n\
251             * 42 EXISTS\r\n\
252             * 7 RECENT\r\n\
253             * OK [UIDVALIDITY 1700] uid validity\r\n\
254             {tag} OK [READ-ONLY] EXAMINE completed\r\n",
255        );
256        let data = expect_complete_ok(&mut examine, &mut frag, reply.as_bytes());
257        assert_eq!(Some(42), data.exists);
258        assert_eq!(Some(7), data.recent);
259        assert_eq!(1700, data.uid_validity.expect("uid validity").get());
260        assert!(data.flags.is_some());
261    }
262
263    #[test]
264    fn tagged_no_returns_no_error() {
265        let mut examine = ImapMailboxExamine::new(
266            "INBOX".try_into().expect("valid mailbox"),
267            ImapMailboxExamineOptions::default(),
268        );
269        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
270
271        let bytes = expect_wants_write(&mut examine, &mut frag, None);
272        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
273
274        expect_wants_read(&mut examine, &mut frag);
275
276        let reply = format!("{tag} NO mailbox does not exist\r\n");
277        let err = expect_complete_err(&mut examine, &mut frag, reply.as_bytes());
278        let ImapMailboxExamineError::No(text) = err else {
279            panic!("expected ImapMailboxExamineError::No, got {err:?}");
280        };
281        assert_eq!(text, "mailbox does not exist");
282    }
283
284    #[test]
285    fn tagged_bad_returns_bad_error() {
286        let mut examine = ImapMailboxExamine::new(
287            "INBOX".try_into().expect("valid mailbox"),
288            ImapMailboxExamineOptions::default(),
289        );
290        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
291
292        let bytes = expect_wants_write(&mut examine, &mut frag, None);
293        let tag = first_word(str::from_utf8(&bytes).expect("utf8 command")).to_owned();
294
295        expect_wants_read(&mut examine, &mut frag);
296
297        let reply = format!("{tag} BAD EXAMINE syntax error\r\n");
298        let err = expect_complete_err(&mut examine, &mut frag, reply.as_bytes());
299        let ImapMailboxExamineError::Bad(text) = err else {
300            panic!("expected ImapMailboxExamineError::Bad, got {err:?}");
301        };
302        assert_eq!(text, "EXAMINE syntax error");
303    }
304
305    #[test]
306    fn bye_returns_bye_error() {
307        let mut examine = ImapMailboxExamine::new(
308            "INBOX".try_into().expect("valid mailbox"),
309            ImapMailboxExamineOptions::default(),
310        );
311        let mut frag = Fragmentizer::new(50 * 1024 * 1024);
312
313        let _ = expect_wants_write(&mut examine, &mut frag, None);
314        expect_wants_read(&mut examine, &mut frag);
315
316        let err = expect_complete_err(&mut examine, &mut frag, b"* BYE going down\r\n");
317        let ImapMailboxExamineError::Bye(text) = err else {
318            panic!("expected ImapMailboxExamineError::Bye, got {err:?}");
319        };
320        assert_eq!(text, "going down");
321    }
322
323    // --- utils
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}