1use 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#[derive(Clone, Debug)]
133pub enum ImapMailboxWatchEvent {
134 EnvelopeAdded {
136 uid: NonZeroU32,
138 items: Vec<MessageDataItem<'static>>,
140 },
141 FlagsAdded {
143 uid: NonZeroU32,
145 flags: Vec<Flag<'static>>,
147 },
148 FlagsRemoved {
150 uid: NonZeroU32,
152 flags: Vec<Flag<'static>>,
154 },
155 EnvelopeRemoved {
157 uid: NonZeroU32,
159 },
160}
161
162#[derive(Debug, Error)]
164pub enum ImapMailboxWatchError {
165 #[error("IMAP server did not return UIDVALIDITY in EXAMINE response")]
168 MissingUidValidity,
169 #[error("IMAP server did not return HIGHESTMODSEQ in EXAMINE response")]
172 MissingHighestModSeq,
173 #[error("IMAP mailbox UIDVALIDITY changed from {known} to {seen}")]
176 UidValidityChanged {
177 known: NonZeroU32,
179 seen: NonZeroU32,
181 },
182 #[error("Invalid `1:*` sequence set: {0}")]
184 InvalidSequenceSet(String),
185 #[error("IMAP EXAMINE error")]
187 Examine(#[from] ImapMailboxExamineError),
188 #[error("IMAP FETCH error")]
190 Fetch(#[from] ImapMessageFetchError),
191 #[error("IMAP IDLE error")]
193 Idle(#[from] ImapIdleError),
194 #[error("IMAP ENABLE error")]
196 Enable(#[from] ImapExtensionEnableError),
197}
198
199#[derive(Clone, Copy, Debug, Default)]
201pub struct ImapMailboxWatchOptions {
202 pub idle_timeout: Option<Duration>,
210 pub poll: bool,
219}
220
221#[derive(Debug)]
223pub enum ImapMailboxWatchYield {
224 WantsRead,
226 WantsWait,
229 WantsWrite(Vec<u8>),
231 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
249pub 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 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 let condstore = CapabilityEnable::CondStore;
280 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 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 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 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 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 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
452fn 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 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
759fn 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 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 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 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 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 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 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 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 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 #[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}