Skip to main content

io_imap/
watch.rs

1//! IMAP single-mailbox watcher: IDLE (RFC 2177) for the wake signal,
2//! EXAMINE (QRESYNC) (RFC 7162) for UID-keyed deltas, with a client-side
3//! fallback for servers that do not advertise QRESYNC.
4//!
5//! The mailbox is opened with EXAMINE, not SELECT, so the session is
6//! **read-only**: the watcher never writes (no flag changes, no expunge),
7//! and it avoids SELECT's `\Recent` reset on every re-open.
8//!
9//! QRESYNC: <https://www.rfc-editor.org/rfc/rfc7162>
10//!
11//! ```text
12//! EXAMINE (CONDSTORE) → FETCH 1:* (UID FLAGS) [seed shadow]
13//!     → IDLE → EXAMINE (QRESYNC) → emit deltas → IDLE → ...
14//! ```
15//!
16//! Without QRESYNC the server cannot report what changed, so each wake
17//! re-reads the whole mailbox and the deltas are diffed locally against
18//! the same shadow. The emitted events are identical; only the cost
19//! differs, and it scales with the mailbox rather than with the change.
20//!
21//! ```text
22//! EXAMINE → FETCH 1:* (UID FLAGS) [seed shadow]
23//!     → IDLE → EXAMINE → FETCH 1:* (UID FLAGS) → diff → emit deltas → IDLE → ...
24//! ```
25//!
26//! Both paths re-EXAMINE before resyncing, so a UIDVALIDITY change ends
27//! the watch rather than keying deltas on UIDs that now mean something
28//! else. The caller reconnects and rebaselines.
29//!
30//! Connection is dedicated. Flip the shared [`AtomicBool`] to wind
31//! down cleanly.
32//!
33//! # Example
34//!
35//! ```rust,no_run
36//! use core::sync::atomic::AtomicBool;
37//! use std::{
38//!     io::{Read, Write},
39//!     net::TcpStream,
40//!     sync::Arc,
41//! };
42//!
43//! use io_imap::{
44//!     codec::fragmentizer::Fragmentizer,
45//!     coroutine::{ImapCoroutine, ImapCoroutineState},
46//!     types::response::Capability,
47//!     watch::{ImapMailboxWatch, ImapMailboxWatchYield},
48//! };
49//!
50//! // Ready stream needed (TCP-connected, TLS-negotiated, IMAP-authenticated)
51//! let mut stream = TcpStream::connect("localhost:143").unwrap();
52//!
53//! let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
54//! let mut buf = [0u8; 4096];
55//!
56//! // The server's advertised capabilities select the path: QRESYNC
57//! // here, the whole-mailbox fallback when it is absent.
58//! let capability = [Capability::QResync];
59//! let mailbox = "INBOX".try_into().unwrap();
60//! let shutdown = Arc::new(AtomicBool::new(false));
61//! let opts = Default::default();
62//! let mut coroutine = ImapMailboxWatch::new(&capability, mailbox, shutdown.clone(), opts);
63//! let mut arg = None;
64//!
65//! loop {
66//!     match coroutine.resume(&mut fragmentizer, arg.take()) {
67//!         ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWrite(bytes)) => {
68//!             stream.write_all(&bytes).unwrap();
69//!         }
70//!         ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsRead) => {
71//!             let n = stream.read(&mut buf).unwrap();
72//!             arg = Some(&buf[..n]);
73//!         }
74//!         ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWait) => {
75//!             // Only a polling watch asks; sleep as long as you poll for.
76//!         }
77//!         ImapCoroutineState::Yielded(ImapMailboxWatchYield::Event(event)) => {
78//!             println!("{event:?}");
79//!         }
80//!         ImapCoroutineState::Complete(Ok(())) => break,
81//!         ImapCoroutineState::Complete(Err(err)) => panic!("{err}"),
82//!     }
83//! }
84//! ```
85
86use core::{
87    mem,
88    num::{NonZeroU32, NonZeroU64},
89    sync::atomic::{AtomicBool, Ordering},
90    time::Duration,
91};
92
93use alloc::{
94    collections::{BTreeMap, VecDeque},
95    string::String,
96    sync::Arc,
97    vec,
98    vec::Vec,
99};
100
101use imap_codec::{
102    fragmentizer::Fragmentizer,
103    imap_types::{
104        command::SelectParameter,
105        core::{Atom, Vec1},
106        extensions::enable::CapabilityEnable,
107        fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
108        flag::{Flag, FlagFetch},
109        mailbox::Mailbox,
110        response::Capability,
111        sequence::SequenceSet,
112    },
113};
114use log::{debug, trace};
115use thiserror::Error;
116
117use crate::{
118    coroutine::*,
119    rfc2177::idle::{ImapIdle, ImapIdleError, ImapIdleOptions, ImapIdleYield},
120    rfc3501::{
121        examine::{ImapMailboxExamine, ImapMailboxExamineError, ImapMailboxExamineOptions},
122        fetch::{ImapMessageFetch, ImapMessageFetchError, ImapMessageFetchOptions},
123        select::ImapMailboxSelectData,
124    },
125    rfc5161::enable::{ImapExtensionEnable, ImapExtensionEnableError},
126};
127
128/// UID-keyed mailbox change emitted by the watcher.
129///
130/// `FlagsAdded`/`FlagsRemoved` are pre-diffed against the internal
131/// shadow; each `flags` vector lists only the changed flags.
132#[derive(Clone, Debug)]
133pub enum ImapMailboxWatchEvent {
134    /// A message appeared in the mailbox.
135    EnvelopeAdded {
136        /// The UID of the new message.
137        uid: NonZeroU32,
138        /// The FETCH items announcing the message.
139        items: Vec<MessageDataItem<'static>>,
140    },
141    /// Flags were set on an existing message.
142    FlagsAdded {
143        /// The UID of the changed message.
144        uid: NonZeroU32,
145        /// The flags that were added.
146        flags: Vec<Flag<'static>>,
147    },
148    /// Flags were cleared on an existing message.
149    FlagsRemoved {
150        /// The UID of the changed message.
151        uid: NonZeroU32,
152        /// The flags that were removed.
153        flags: Vec<Flag<'static>>,
154    },
155    /// A message left the mailbox (expunged or moved away).
156    EnvelopeRemoved {
157        /// The UID of the removed message.
158        uid: NonZeroU32,
159    },
160}
161
162/// Failure causes during the mailbox watch flow.
163#[derive(Debug, Error)]
164pub enum ImapMailboxWatchError {
165    /// The EXAMINE response carried no UIDVALIDITY, so deltas cannot be
166    /// keyed safely.
167    #[error("IMAP server did not return UIDVALIDITY in EXAMINE response")]
168    MissingUidValidity,
169    /// The EXAMINE response carried no HIGHESTMODSEQ, so there is no
170    /// QRESYNC resync point.
171    #[error("IMAP server did not return HIGHESTMODSEQ in EXAMINE response")]
172    MissingHighestModSeq,
173    /// The mailbox was recreated under the same name, so every known UID
174    /// now means something else and the watch cannot continue.
175    #[error("IMAP mailbox UIDVALIDITY changed from {known} to {seen}")]
176    UidValidityChanged {
177        /// The UIDVALIDITY the shadow was keyed on.
178        known: NonZeroU32,
179        /// The UIDVALIDITY the server reports now.
180        seen: NonZeroU32,
181    },
182    /// The baseline `1:*` sequence set failed to parse.
183    #[error("Invalid `1:*` sequence set: {0}")]
184    InvalidSequenceSet(String),
185    /// The initial or QRESYNC EXAMINE failed.
186    #[error("IMAP EXAMINE error")]
187    Examine(#[from] ImapMailboxExamineError),
188    /// The baseline FETCH failed.
189    #[error("IMAP FETCH error")]
190    Fetch(#[from] ImapMessageFetchError),
191    /// The IDLE wake-loop failed.
192    #[error("IMAP IDLE error")]
193    Idle(#[from] ImapIdleError),
194    /// The ENABLE QRESYNC round failed.
195    #[error("IMAP ENABLE error")]
196    Enable(#[from] ImapExtensionEnableError),
197}
198
199/// Options of [`ImapMailboxWatch`].
200#[derive(Clone, Copy, Debug, Default)]
201pub struct ImapMailboxWatchOptions {
202    /// How long an IDLE is held before it is re-issued.
203    ///
204    /// `None` takes io-imap's own default, which re-issues often
205    /// enough to survive a NAT middle-box that drops a quiet
206    /// connection. A server known to hold one open is asked less
207    /// often, up to the 29 minutes RFC 2177 §3 allows. Ignored by a
208    /// polling watch, which holds no IDLE.
209    pub idle_timeout: Option<Duration>,
210    /// Wait for the caller between two re-reads instead of holding
211    /// IDLE.
212    ///
213    /// A polling watch yields [`ImapMailboxWatchYield::WantsWait`] and
214    /// re-reads on the resume that follows, so how long it waits, and
215    /// therefore how quickly it notices a change, is its driver's to
216    /// decide. Off by default: a server that offers IDLE is better
217    /// asked to speak first.
218    pub poll: bool,
219}
220
221/// Yield variants from the mailbox watcher.
222#[derive(Debug)]
223pub enum ImapMailboxWatchYield {
224    /// The caller reads from its stream and resumes with the bytes.
225    WantsRead,
226    /// The caller waits as long as it means to poll for, then resumes
227    /// with no input. Only a polling watch yields this.
228    WantsWait,
229    /// The caller writes the given bytes to its stream and resumes.
230    WantsWrite(Vec<u8>),
231    /// A mailbox change to consume; the watcher keeps running.
232    Event(ImapMailboxWatchEvent),
233}
234
235enum State {
236    EnableQresync(ImapExtensionEnable),
237    ExamineInitial(ImapMailboxExamine),
238    FetchBaseline(ImapMessageFetch),
239    BeginIdle,
240    Idle(ImapIdle),
241    Waiting,
242    ExamineQresync(ImapMailboxExamine),
243    ExamineResync(ImapMailboxExamine),
244    FetchResync(ImapMessageFetch),
245    EmitDeltas,
246    Terminal,
247}
248
249/// I/O-free IDLE mailbox watcher, QRESYNC-driven or whole-mailbox.
250pub struct ImapMailboxWatch {
251    state: State,
252    opts: ImapMailboxWatchOptions,
253    qresync: bool,
254    shutdown: Arc<AtomicBool>,
255    idle_done: Arc<AtomicBool>,
256    idle_saw_data: bool,
257    mailbox: Mailbox<'static>,
258    uid_validity: Option<NonZeroU32>,
259    highest_mod_seq: u64,
260    shadow: BTreeMap<NonZeroU32, Vec<Flag<'static>>>,
261    pending: VecDeque<ImapMailboxWatchEvent>,
262}
263
264impl ImapMailboxWatch {
265    /// Builds a watcher for `mailbox`, picking its path from
266    /// `capability`: QRESYNC when the server advertises it, the
267    /// whole-mailbox fallback otherwise.
268    pub fn new(
269        capability: &[Capability<'static>],
270        mailbox: Mailbox<'static>,
271        shutdown: Arc<AtomicBool>,
272        opts: ImapMailboxWatchOptions,
273    ) -> Self {
274        let qresync = capability.contains(&Capability::QResync);
275
276        let state = if qresync {
277            // NOTE: RFC 7162 §3.1: QRESYNC implies CONDSTORE, but pass
278            // both since some servers only echo CONDSTORE in ENABLED.
279            let condstore = CapabilityEnable::CondStore;
280            // NOTE: QRESYNC is not in the typed enum, route via Atom.
281            let qresync = CapabilityEnable::from(
282                Atom::try_from("QRESYNC").expect("`QRESYNC` is a syntactically valid IMAP atom"),
283            );
284            let capabilities =
285                Vec1::try_from(vec![condstore, qresync]).expect("two capabilities is non-empty");
286
287            State::EnableQresync(ImapExtensionEnable::new(capabilities))
288        } else {
289            debug!("qresync unsupported, watching the whole mailbox");
290
291            State::ExamineInitial(ImapMailboxExamine::new(
292                mailbox.clone(),
293                ImapMailboxExamineOptions::default(),
294            ))
295        };
296
297        Self {
298            state,
299            opts,
300            qresync,
301            shutdown,
302            idle_done: Arc::new(AtomicBool::new(false)),
303            idle_saw_data: false,
304            mailbox,
305            uid_validity: None,
306            highest_mod_seq: 0,
307            shadow: BTreeMap::new(),
308            pending: VecDeque::new(),
309        }
310    }
311
312    /// The re-read that follows a wake, whichever way the watch was
313    /// woken: QRESYNC when the server can name what changed, a plain
314    /// EXAMINE when the whole mailbox has to be read again.
315    fn resync(&self) -> State {
316        if !self.qresync {
317            let examine =
318                ImapMailboxExamine::new(self.mailbox.clone(), ImapMailboxExamineOptions::default());
319
320            return State::ExamineResync(examine);
321        }
322
323        // NOTE: uid_validity is set by ExamineInitial.
324        let uid_validity = self.uid_validity.unwrap();
325        let modseq = NonZeroU64::new(self.highest_mod_seq)
326            .unwrap_or_else(|| NonZeroU64::new(1).expect("1 is non-zero"));
327        let parameters = vec![SelectParameter::QResync {
328            uid_validity,
329            mod_sequence_value: modseq,
330            known_uids: None,
331            seq_match_data: None,
332        }];
333        let examine = ImapMailboxExamine::new(
334            self.mailbox.clone(),
335            ImapMailboxExamineOptions { parameters },
336        );
337
338        State::ExamineQresync(examine)
339    }
340
341    /// Refuses a resync keyed on UIDs the server no longer means.
342    fn check_uid_validity(
343        &self,
344        data: &ImapMailboxSelectData,
345    ) -> Result<(), ImapMailboxWatchError> {
346        let (Some(known), Some(seen)) = (self.uid_validity, data.uid_validity) else {
347            return Ok(());
348        };
349
350        if known != seen {
351            return Err(ImapMailboxWatchError::UidValidityChanged { known, seen });
352        }
353
354        Ok(())
355    }
356
357    /// Queues the flag events between two states of one message, and
358    /// nothing when they agree.
359    fn push_flag_deltas(
360        &mut self,
361        uid: NonZeroU32,
362        old_flags: &[Flag<'static>],
363        new_flags: &[Flag<'static>],
364    ) {
365        let added: Vec<Flag<'static>> = new_flags
366            .iter()
367            .filter(|f| !old_flags.contains(f))
368            .cloned()
369            .collect();
370        let removed: Vec<Flag<'static>> = old_flags
371            .iter()
372            .filter(|f| !new_flags.contains(f))
373            .cloned()
374            .collect();
375
376        if !added.is_empty() {
377            self.pending
378                .push_back(ImapMailboxWatchEvent::FlagsAdded { uid, flags: added });
379        }
380
381        if !removed.is_empty() {
382            self.pending.push_back(ImapMailboxWatchEvent::FlagsRemoved {
383                uid,
384                flags: removed,
385            });
386        }
387    }
388
389    /// Diffs a whole-mailbox snapshot against the shadow, the fallback
390    /// counterpart of [`Self::compute_deltas`]: absence is what reports
391    /// a vanished message, since no untagged VANISHED arrives.
392    fn compute_snapshot_deltas(
393        &mut self,
394        snapshot: BTreeMap<NonZeroU32, Vec<MessageDataItem<'static>>>,
395    ) {
396        let vanished: Vec<NonZeroU32> = self
397            .shadow
398            .keys()
399            .filter(|uid| !snapshot.contains_key(uid))
400            .copied()
401            .collect();
402
403        for uid in vanished {
404            self.shadow.remove(&uid);
405            self.pending
406                .push_back(ImapMailboxWatchEvent::EnvelopeRemoved { uid });
407        }
408
409        for (uid, items) in snapshot {
410            let (_uid, new_flags) = extract_uid_flags(&items);
411
412            match self.shadow.insert(uid, new_flags.clone()) {
413                None => {
414                    self.pending
415                        .push_back(ImapMailboxWatchEvent::EnvelopeAdded { uid, items });
416                }
417                Some(old_flags) => self.push_flag_deltas(uid, &old_flags, &new_flags),
418            }
419        }
420    }
421
422    fn compute_deltas(&mut self, data: &ImapMailboxSelectData) {
423        for uid in &data.vanished_earlier {
424            if self.shadow.remove(uid).is_some() {
425                self.pending
426                    .push_back(ImapMailboxWatchEvent::EnvelopeRemoved { uid: *uid });
427            }
428        }
429
430        for fetch in &data.changed {
431            let items_vec: Vec<MessageDataItem<'static>> =
432                fetch.items.clone().into_inner().into_iter().collect();
433            let (uid_opt, new_flags) = extract_uid_flags(&items_vec);
434            let Some(uid) = uid_opt else {
435                continue;
436            };
437
438            match self.shadow.insert(uid, new_flags.clone()) {
439                None => {
440                    self.pending
441                        .push_back(ImapMailboxWatchEvent::EnvelopeAdded {
442                            uid,
443                            items: items_vec,
444                        });
445                }
446                Some(old_flags) => self.push_flag_deltas(uid, &old_flags, &new_flags),
447            }
448        }
449    }
450}
451
452/// Builds the `FETCH 1:* (UID FLAGS)` that seeds the shadow and, on the
453/// fallback path, re-reads it on every wake.
454fn fetch_uid_flags() -> Result<ImapMessageFetch, ImapMailboxWatchError> {
455    let sequence_set: SequenceSet = "1:*"
456        .try_into()
457        .map_err(|_| ImapMailboxWatchError::InvalidSequenceSet("1:*".into()))?;
458    let item_names = MacroOrMessageDataItemNames::MessageDataItemNames(vec![
459        MessageDataItemName::Uid,
460        MessageDataItemName::Flags,
461    ]);
462
463    Ok(ImapMessageFetch::new(
464        sequence_set,
465        item_names,
466        ImapMessageFetchOptions::default(),
467    ))
468}
469
470impl ImapCoroutine for ImapMailboxWatch {
471    type Yield = ImapMailboxWatchYield;
472    type Return = Result<(), ImapMailboxWatchError>;
473
474    fn resume(
475        &mut self,
476        fragmentizer: &mut Fragmentizer,
477        mut arg: Option<&[u8]>,
478    ) -> ImapCoroutineState<Self::Yield, Self::Return> {
479        if self.shutdown.load(Ordering::SeqCst) {
480            self.idle_done.store(true, Ordering::SeqCst);
481        }
482
483        loop {
484            let state = mem::replace(&mut self.state, State::Terminal);
485
486            match state {
487                State::EnableQresync(mut enable) => match enable.resume(fragmentizer, arg.take()) {
488                    ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
489                        self.state = State::EnableQresync(enable);
490                        return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsRead);
491                    }
492                    ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
493                        self.state = State::EnableQresync(enable);
494                        return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWrite(
495                            bytes,
496                        ));
497                    }
498                    ImapCoroutineState::Complete(Ok(enabled)) => {
499                        debug!("enabled qresync");
500                        trace!("{enabled:?}");
501                        let parameters = vec![SelectParameter::CondStore];
502                        let examine = ImapMailboxExamine::new(
503                            self.mailbox.clone(),
504                            ImapMailboxExamineOptions { parameters },
505                        );
506                        self.state = State::ExamineInitial(examine);
507                    }
508                    ImapCoroutineState::Complete(Err(err)) => {
509                        return ImapCoroutineState::Complete(Err(err.into()));
510                    }
511                },
512
513                State::ExamineInitial(mut examine) => {
514                    match examine.resume(fragmentizer, arg.take()) {
515                        ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
516                            self.state = State::ExamineInitial(examine);
517                            return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsRead);
518                        }
519                        ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
520                            self.state = State::ExamineInitial(examine);
521                            return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWrite(
522                                bytes,
523                            ));
524                        }
525                        ImapCoroutineState::Complete(Ok(data)) => {
526                            let Some(uid_validity) = data.uid_validity else {
527                                return ImapCoroutineState::Complete(Err(
528                                    ImapMailboxWatchError::MissingUidValidity,
529                                ));
530                            };
531
532                            self.uid_validity = Some(uid_validity);
533                            trace!("uid_validity: {uid_validity}");
534
535                            if self.qresync {
536                                let Some(highest_mod_seq) = data.highest_mod_seq else {
537                                    return ImapCoroutineState::Complete(Err(
538                                        ImapMailboxWatchError::MissingHighestModSeq,
539                                    ));
540                                };
541
542                                self.highest_mod_seq = highest_mod_seq;
543                                debug!("examined mailbox with condstore");
544                                trace!("highest_mod_seq: {highest_mod_seq}");
545                            } else {
546                                debug!("examined mailbox");
547                            }
548
549                            let fetch = match fetch_uid_flags() {
550                                Ok(fetch) => fetch,
551                                Err(err) => return ImapCoroutineState::Complete(Err(err)),
552                            };
553                            self.state = State::FetchBaseline(fetch);
554                        }
555                        ImapCoroutineState::Complete(Err(err)) => {
556                            return ImapCoroutineState::Complete(Err(err.into()));
557                        }
558                    }
559                }
560
561                State::FetchBaseline(mut fetch) => match fetch.resume(fragmentizer, arg.take()) {
562                    ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
563                        self.state = State::FetchBaseline(fetch);
564                        return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsRead);
565                    }
566                    ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
567                        self.state = State::FetchBaseline(fetch);
568                        return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWrite(
569                            bytes,
570                        ));
571                    }
572                    ImapCoroutineState::Complete(Ok(data)) => {
573                        for (_seq, items) in data {
574                            let items_vec = items.into_inner();
575                            if let (Some(uid), flags) = extract_uid_flags(&items_vec) {
576                                self.shadow.insert(uid, flags);
577                            }
578                        }
579                        debug!("seeded baseline shadow");
580                        trace!("uids: {}", self.shadow.len());
581                        self.state = State::BeginIdle;
582                    }
583                    ImapCoroutineState::Complete(Err(err)) => {
584                        return ImapCoroutineState::Complete(Err(err.into()));
585                    }
586                },
587
588                State::BeginIdle => {
589                    if self.shutdown.load(Ordering::SeqCst) {
590                        return ImapCoroutineState::Complete(Ok(()));
591                    }
592
593                    // NOTE: waiting is an effect, so a polling watch
594                    // hands the wait back to its driver rather than
595                    // holding IDLE. How long to wait is the driver's,
596                    // which is why nothing here names a duration.
597                    if self.opts.poll {
598                        self.state = State::Waiting;
599                        return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWait);
600                    }
601
602                    self.idle_done.store(false, Ordering::SeqCst);
603                    self.idle_saw_data = false;
604                    let opts = ImapIdleOptions {
605                        timeout: self.opts.idle_timeout,
606                    };
607                    let idle = ImapIdle::new(self.idle_done.clone(), opts);
608                    self.state = State::Idle(idle);
609                }
610
611                State::Waiting => {
612                    if self.shutdown.load(Ordering::SeqCst) {
613                        return ImapCoroutineState::Complete(Ok(()));
614                    }
615
616                    self.state = self.resync();
617                }
618
619                State::Idle(mut idle) => match idle.resume(fragmentizer, arg.take()) {
620                    ImapCoroutineState::Yielded(ImapIdleYield::Event(_)) => {
621                        debug!("idle saw untagged data");
622                        self.idle_saw_data = true;
623                        self.idle_done.store(true, Ordering::SeqCst);
624                        self.state = State::Idle(idle);
625                    }
626                    ImapCoroutineState::Yielded(ImapIdleYield::WantsRead) => {
627                        self.state = State::Idle(idle);
628                        return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsRead);
629                    }
630                    ImapCoroutineState::Yielded(ImapIdleYield::WantsWrite(bytes)) => {
631                        self.state = State::Idle(idle);
632                        return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWrite(
633                            bytes,
634                        ));
635                    }
636                    ImapCoroutineState::Complete(Ok(())) => {
637                        if self.shutdown.load(Ordering::SeqCst) {
638                            return ImapCoroutineState::Complete(Ok(()));
639                        }
640
641                        if self.idle_saw_data {
642                            self.state = self.resync();
643                        } else {
644                            debug!("idle timed out with no data, restarting");
645                            self.state = State::BeginIdle;
646                        }
647                    }
648                    ImapCoroutineState::Complete(Err(err)) => {
649                        return ImapCoroutineState::Complete(Err(err.into()));
650                    }
651                },
652
653                State::ExamineQresync(mut examine) => {
654                    match examine.resume(fragmentizer, arg.take()) {
655                        ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
656                            self.state = State::ExamineQresync(examine);
657                            return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsRead);
658                        }
659                        ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
660                            self.state = State::ExamineQresync(examine);
661                            return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWrite(
662                                bytes,
663                            ));
664                        }
665                        ImapCoroutineState::Complete(Ok(data)) => {
666                            if let Err(err) = self.check_uid_validity(&data) {
667                                return ImapCoroutineState::Complete(Err(err));
668                            }
669
670                            self.compute_deltas(&data);
671                            if let Some(new_modseq) = data.highest_mod_seq {
672                                self.highest_mod_seq = new_modseq;
673                            }
674                            self.state = State::EmitDeltas;
675                        }
676                        ImapCoroutineState::Complete(Err(err)) => {
677                            return ImapCoroutineState::Complete(Err(err.into()));
678                        }
679                    }
680                }
681
682                State::ExamineResync(mut examine) => {
683                    match examine.resume(fragmentizer, arg.take()) {
684                        ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
685                            self.state = State::ExamineResync(examine);
686                            return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsRead);
687                        }
688                        ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
689                            self.state = State::ExamineResync(examine);
690                            return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWrite(
691                                bytes,
692                            ));
693                        }
694                        ImapCoroutineState::Complete(Ok(data)) => {
695                            if let Err(err) = self.check_uid_validity(&data) {
696                                return ImapCoroutineState::Complete(Err(err));
697                            }
698
699                            let fetch = match fetch_uid_flags() {
700                                Ok(fetch) => fetch,
701                                Err(err) => return ImapCoroutineState::Complete(Err(err)),
702                            };
703                            self.state = State::FetchResync(fetch);
704                        }
705                        ImapCoroutineState::Complete(Err(err)) => {
706                            return ImapCoroutineState::Complete(Err(err.into()));
707                        }
708                    }
709                }
710
711                State::FetchResync(mut fetch) => match fetch.resume(fragmentizer, arg.take()) {
712                    ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
713                        self.state = State::FetchResync(fetch);
714                        return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsRead);
715                    }
716                    ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
717                        self.state = State::FetchResync(fetch);
718                        return ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWrite(
719                            bytes,
720                        ));
721                    }
722                    ImapCoroutineState::Complete(Ok(data)) => {
723                        let mut snapshot = BTreeMap::new();
724
725                        for (_seq, items) in data {
726                            let items_vec = items.into_inner();
727                            if let (Some(uid), _flags) = extract_uid_flags(&items_vec) {
728                                snapshot.insert(uid, items_vec);
729                            }
730                        }
731
732                        debug!("re-read the whole mailbox");
733                        trace!("uids: {}", snapshot.len());
734                        self.compute_snapshot_deltas(snapshot);
735                        self.state = State::EmitDeltas;
736                    }
737                    ImapCoroutineState::Complete(Err(err)) => {
738                        return ImapCoroutineState::Complete(Err(err.into()));
739                    }
740                },
741
742                State::EmitDeltas => {
743                    if let Some(event) = self.pending.pop_front() {
744                        self.state = State::EmitDeltas;
745                        return ImapCoroutineState::Yielded(ImapMailboxWatchYield::Event(event));
746                    }
747                    self.state = State::BeginIdle;
748                }
749
750                State::Terminal => {
751                    self.state = State::Terminal;
752                    return ImapCoroutineState::Complete(Ok(()));
753                }
754            }
755        }
756    }
757}
758
759/// Extract the UID and flag list from a single FETCH; preserves wire
760/// order, drops non-`Flag` variants of [`FlagFetch`].
761fn extract_uid_flags(
762    items: &[MessageDataItem<'static>],
763) -> (Option<NonZeroU32>, Vec<Flag<'static>>) {
764    let mut uid = None;
765    let mut flags = Vec::new();
766    for item in items {
767        match item {
768            MessageDataItem::Uid(u) => uid = Some(*u),
769            MessageDataItem::Flags(fs) => {
770                flags = fs
771                    .iter()
772                    .filter_map(|f| match f {
773                        FlagFetch::Flag(flag) => Some(flag.clone()),
774                        _ => None,
775                    })
776                    .collect();
777            }
778            _ => {}
779        }
780    }
781    (uid, flags)
782}
783
784#[cfg(test)]
785mod tests {
786    use core::str;
787
788    use alloc::{borrow::ToOwned, format, string::ToString};
789
790    use crate::watch::*;
791
792    const UID_VALIDITY: u32 = 1700;
793
794    /// One scripted exchange: the fragment the next written command
795    /// must contain, then the replies to feed it, `{tag}` standing for
796    /// the tag the watcher chose.
797    type Step<'a> = (&'a str, &'a [&'a str]);
798
799    fn watcher(capability: &[Capability<'static>]) -> (ImapMailboxWatch, Fragmentizer) {
800        watcher_with(capability, ImapMailboxWatchOptions::default())
801    }
802
803    fn watcher_with(
804        capability: &[Capability<'static>],
805        opts: ImapMailboxWatchOptions,
806    ) -> (ImapMailboxWatch, Fragmentizer) {
807        let watch = ImapMailboxWatch::new(
808            capability,
809            "INBOX".try_into().expect("valid mailbox"),
810            Arc::new(AtomicBool::new(false)),
811            opts,
812        );
813
814        (watch, Fragmentizer::new(50 * 1024 * 1024))
815    }
816
817    fn first_word(line: &str) -> &str {
818        line.split_whitespace()
819            .next()
820            .expect("first whitespace-separated token")
821    }
822
823    /// Plays `steps` against the watcher and collects what it emitted,
824    /// stopping at the first command the script does not answer.
825    fn drive(
826        cor: &mut ImapMailboxWatch,
827        frag: &mut Fragmentizer,
828        steps: &[Step],
829    ) -> Result<Vec<ImapMailboxWatchEvent>, ImapMailboxWatchError> {
830        let mut events = Vec::new();
831        let mut replies: VecDeque<String> = VecDeque::new();
832        let mut tag = String::new();
833        let mut next = 0;
834        let mut arg: Option<Vec<u8>> = None;
835
836        loop {
837            match cor.resume(frag, arg.take().as_deref()) {
838                ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWrite(bytes)) => {
839                    let line = str::from_utf8(&bytes).expect("utf8 command").to_string();
840
841                    let Some((expected, scripted)) = steps.get(next) else {
842                        return Ok(events);
843                    };
844
845                    assert!(line.contains(expected), "expected {expected}, wrote {line}");
846                    next += 1;
847
848                    // NOTE: DONE closes the IDLE the server still owes a
849                    // tagged reply to, so it carries no tag of its own.
850                    if !line.starts_with("DONE") {
851                        tag = first_word(&line).to_owned();
852                    }
853
854                    replies.extend(scripted.iter().map(|reply| reply.replace("{tag}", &tag)));
855                }
856                ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsRead) => {
857                    let reply = replies
858                        .pop_front()
859                        .expect("the script owes a reply to every read");
860                    arg = Some(reply.into_bytes());
861                }
862                ImapCoroutineState::Yielded(ImapMailboxWatchYield::Event(event)) => {
863                    events.push(event);
864                }
865                // NOTE: the driver is what waits, and a test has
866                // nothing to wait for.
867                ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWait) => {}
868                ImapCoroutineState::Complete(Ok(())) => panic!("the watch stopped early"),
869                ImapCoroutineState::Complete(Err(err)) => return Err(err),
870            }
871        }
872    }
873
874    /// The very first command the watcher writes, which is what its
875    /// path shows.
876    fn first_command(cor: &mut ImapMailboxWatch, frag: &mut Fragmentizer) -> String {
877        match cor.resume(frag, None) {
878            ImapCoroutineState::Yielded(ImapMailboxWatchYield::WantsWrite(bytes)) => {
879                str::from_utf8(&bytes).expect("utf8 command").to_string()
880            }
881            state => panic!("expected WantsWrite, got {state:?}"),
882        }
883    }
884
885    /// The EXAMINE reply of a mailbox that has never been recreated.
886    fn examined(uid_validity: u32) -> String {
887        format!(
888            "* 2 EXISTS\r\n\
889             * OK [UIDVALIDITY {uid_validity}] uid validity\r\n\
890             {{tag}} OK [READ-ONLY] EXAMINE completed\r\n",
891        )
892    }
893
894    /// A `FETCH 1:* (UID FLAGS)` reply, one message per UID and flags
895    /// pair.
896    fn fetched(messages: &[(u32, &str)]) -> String {
897        let mut reply = String::new();
898
899        for (seq, (uid, flags)) in messages.iter().enumerate() {
900            reply.push_str(&format!(
901                "* {} FETCH (UID {uid} FLAGS ({flags}))\r\n",
902                seq + 1
903            ));
904        }
905
906        reply.push_str("{tag} OK FETCH completed\r\n");
907        reply
908    }
909
910    #[test]
911    fn qresync_capability_enables_it_first() {
912        let (mut watch, mut frag) = watcher(&[Capability::QResync]);
913        let line = first_command(&mut watch, &mut frag);
914
915        assert!(line.contains("ENABLE"), "wrote {line}");
916    }
917
918    #[test]
919    fn missing_qresync_examines_straight_away() {
920        let (mut watch, mut frag) = watcher(&[]);
921        let line = first_command(&mut watch, &mut frag);
922
923        assert!(line.contains("EXAMINE INBOX"), "wrote {line}");
924        assert!(!line.contains("ENABLE"), "wrote {line}");
925        assert!(!line.contains("CONDSTORE"), "wrote {line}");
926    }
927
928    #[test]
929    fn fallback_diffs_the_whole_mailbox_on_every_wake() {
930        let (mut watch, mut frag) = watcher(&[]);
931        let baseline = fetched(&[(1, ""), (2, "")]);
932        let resynced = fetched(&[(2, "\\Seen"), (3, "")]);
933        let steps: &[Step] = &[
934            ("EXAMINE INBOX", &[&examined(UID_VALIDITY)]),
935            ("FETCH 1:* (UID FLAGS)", &[&baseline]),
936            ("IDLE", &["+ idling\r\n", "* 3 EXISTS\r\n"]),
937            ("DONE", &["{tag} OK IDLE terminated\r\n"]),
938            ("EXAMINE INBOX", &[&examined(UID_VALIDITY)]),
939            ("FETCH 1:* (UID FLAGS)", &[&resynced]),
940        ];
941
942        let events = drive(&mut watch, &mut frag, steps).expect("watch running");
943        assert_eq!(3, events.len(), "got {events:?}");
944
945        // NOTE: no VANISHED arrives without QRESYNC, so a UID missing
946        // from the snapshot is what reports the removal.
947        let ImapMailboxWatchEvent::EnvelopeRemoved { uid } = &events[0] else {
948            panic!("expected EnvelopeRemoved, got {:?}", events[0]);
949        };
950        assert_eq!(1, uid.get());
951
952        let ImapMailboxWatchEvent::FlagsAdded { uid, flags } = &events[1] else {
953            panic!("expected FlagsAdded, got {:?}", events[1]);
954        };
955        assert_eq!(2, uid.get());
956        assert_eq!(&vec![Flag::Seen], flags);
957
958        let ImapMailboxWatchEvent::EnvelopeAdded { uid, .. } = &events[2] else {
959            panic!("expected EnvelopeAdded, got {:?}", events[2]);
960        };
961        assert_eq!(3, uid.get());
962    }
963
964    #[test]
965    fn fallback_reports_nothing_when_the_mailbox_is_unchanged() {
966        let (mut watch, mut frag) = watcher(&[]);
967        let snapshot = fetched(&[(1, "\\Seen")]);
968        let steps: &[Step] = &[
969            ("EXAMINE INBOX", &[&examined(UID_VALIDITY)]),
970            ("FETCH 1:* (UID FLAGS)", &[&snapshot]),
971            ("IDLE", &["+ idling\r\n", "* 1 EXISTS\r\n"]),
972            ("DONE", &["{tag} OK IDLE terminated\r\n"]),
973            ("EXAMINE INBOX", &[&examined(UID_VALIDITY)]),
974            ("FETCH 1:* (UID FLAGS)", &[&snapshot]),
975        ];
976
977        let events = drive(&mut watch, &mut frag, steps).expect("watch running");
978        assert!(events.is_empty(), "got {events:?}");
979    }
980
981    /// A polling watch never sends IDLE: it asks its driver to wait
982    /// and re-reads on the resume that follows, which is what lets a
983    /// caller watch a server whose IDLE cannot be trusted.
984    #[test]
985    fn a_polling_watch_re_reads_instead_of_idling() {
986        let opts = ImapMailboxWatchOptions {
987            poll: true,
988            ..Default::default()
989        };
990        let (mut watch, mut frag) = watcher_with(&[], opts);
991        let baseline = fetched(&[(1, "")]);
992        let resynced = fetched(&[(1, "\\Seen")]);
993        let steps: &[Step] = &[
994            ("EXAMINE INBOX", &[&examined(UID_VALIDITY)]),
995            ("FETCH 1:* (UID FLAGS)", &[&baseline]),
996            ("EXAMINE INBOX", &[&examined(UID_VALIDITY)]),
997            ("FETCH 1:* (UID FLAGS)", &[&resynced]),
998        ];
999
1000        let events = drive(&mut watch, &mut frag, steps).expect("watch running");
1001
1002        assert_eq!(1, events.len(), "got {events:?}");
1003        let ImapMailboxWatchEvent::FlagsAdded { uid, flags } = &events[0] else {
1004            panic!("expected FlagsAdded, got {:?}", events[0]);
1005        };
1006        assert_eq!(1, uid.get());
1007        assert_eq!(&vec![Flag::Seen], flags);
1008    }
1009
1010    #[test]
1011    fn a_recreated_mailbox_ends_the_watch() {
1012        let (mut watch, mut frag) = watcher(&[]);
1013        let baseline = fetched(&[(1, "")]);
1014        let steps: &[Step] = &[
1015            ("EXAMINE INBOX", &[&examined(UID_VALIDITY)]),
1016            ("FETCH 1:* (UID FLAGS)", &[&baseline]),
1017            ("IDLE", &["+ idling\r\n", "* 1 EXPUNGE\r\n"]),
1018            ("DONE", &["{tag} OK IDLE terminated\r\n"]),
1019            ("EXAMINE INBOX", &[&examined(UID_VALIDITY + 1)]),
1020        ];
1021
1022        let err = drive(&mut watch, &mut frag, steps).expect_err("uid validity changed");
1023        let ImapMailboxWatchError::UidValidityChanged { known, seen } = err else {
1024            panic!("expected UidValidityChanged, got {err:?}");
1025        };
1026        assert_eq!(UID_VALIDITY, known.get());
1027        assert_eq!(UID_VALIDITY + 1, seen.get());
1028    }
1029}