Skip to main content

rusmes_imap/
qresync.rs

1//! IMAP QRESYNC Extension - RFC 7162
2//!
3//! This module implements Quick Resynchronization (QRESYNC) for efficient
4//! mailbox synchronization. QRESYNC requires CONDSTORE and ENABLE support.
5//!
6//! Key features:
7//! - SELECT/EXAMINE with QRESYNC parameters
8//! - VANISHED responses for efficiently reporting expunged messages
9//! - UID mapping for sequence number changes
10//! - Known UIDs optimization
11
12use rusmes_storage::ModSeq;
13use std::fmt;
14
15/// QRESYNC enablement state
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum QResyncState {
18    /// QRESYNC not enabled
19    Disabled,
20    /// QRESYNC enabled via ENABLE command
21    Enabled,
22}
23
24impl QResyncState {
25    /// Check if QRESYNC is enabled
26    pub fn is_enabled(&self) -> bool {
27        matches!(self, Self::Enabled)
28    }
29}
30
31/// QRESYNC parameters for SELECT/EXAMINE commands
32///
33/// Format: QRESYNC (uidvalidity modseq [known-uids [seq-match-data]])
34///
35/// Example: SELECT INBOX (QRESYNC (67890007 20050715194045000 41,43:211,214:541))
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct QResyncParams {
38    /// Last known UIDVALIDITY value
39    pub uidvalidity: u32,
40    /// Last known modification sequence
41    pub modseq: ModSeq,
42    /// Optional set of known UIDs (for optimization)
43    pub known_uids: Option<UidSet>,
44    /// Optional sequence-to-UID mapping data
45    pub seq_match_data: Option<SeqMatchData>,
46}
47
48impl QResyncParams {
49    /// Create new QRESYNC parameters
50    pub fn new(uidvalidity: u32, modseq: ModSeq) -> Self {
51        Self {
52            uidvalidity,
53            modseq,
54            known_uids: None,
55            seq_match_data: None,
56        }
57    }
58
59    /// Create QRESYNC parameters with known UIDs
60    pub fn with_known_uids(uidvalidity: u32, modseq: ModSeq, known_uids: UidSet) -> Self {
61        Self {
62            uidvalidity,
63            modseq,
64            known_uids: Some(known_uids),
65            seq_match_data: None,
66        }
67    }
68
69    /// Create QRESYNC parameters with sequence match data
70    pub fn with_seq_match_data(
71        uidvalidity: u32,
72        modseq: ModSeq,
73        known_uids: UidSet,
74        seq_match_data: SeqMatchData,
75    ) -> Self {
76        Self {
77            uidvalidity,
78            modseq,
79            known_uids: Some(known_uids),
80            seq_match_data: Some(seq_match_data),
81        }
82    }
83
84    /// Parse QRESYNC parameters from command arguments
85    ///
86    /// Format: (uidvalidity modseq [known-uids [seq-match-data]])
87    pub fn parse(args: &str) -> Result<Self, QResyncError> {
88        let args = args.trim().trim_matches(|c| c == '(' || c == ')');
89        let parts: Vec<&str> = args.split_whitespace().collect();
90
91        if parts.len() < 2 {
92            return Err(QResyncError::InvalidSyntax(args.to_string()));
93        }
94
95        // Parse uidvalidity
96        let uidvalidity = parts[0]
97            .parse::<u32>()
98            .map_err(|_| QResyncError::InvalidUidValidity(parts[0].to_string()))?;
99
100        // Parse modseq
101        let modseq_value = parts[1]
102            .parse::<u64>()
103            .map_err(|_| QResyncError::InvalidModSeq(parts[1].to_string()))?;
104
105        if modseq_value == 0 {
106            return Err(QResyncError::ZeroModSeq);
107        }
108
109        let modseq = ModSeq::new(modseq_value);
110
111        // Parse optional known-uids
112        let known_uids = if parts.len() > 2 {
113            Some(UidSet::parse(parts[2])?)
114        } else {
115            None
116        };
117
118        // Parse optional seq-match-data
119        let seq_match_data = if parts.len() > 3 {
120            Some(SeqMatchData::parse(parts[3])?)
121        } else {
122            None
123        };
124
125        Ok(Self {
126            uidvalidity,
127            modseq,
128            known_uids,
129            seq_match_data,
130        })
131    }
132
133    /// Format as IMAP QRESYNC parameter
134    pub fn to_imap_string(&self) -> String {
135        let mut result = format!("({} {}", self.uidvalidity, self.modseq);
136
137        if let Some(ref known_uids) = self.known_uids {
138            result.push(' ');
139            result.push_str(&known_uids.to_string());
140
141            if let Some(ref seq_match_data) = self.seq_match_data {
142                result.push(' ');
143                result.push_str(&seq_match_data.to_string());
144            }
145        }
146
147        result.push(')');
148        result
149    }
150}
151
152/// UID set representation
153///
154/// Examples: "1:5", "1,3,5", "1:100,200:*"
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct UidSet {
157    /// Raw UID set string
158    ranges: Vec<UidRange>,
159}
160
161impl UidSet {
162    /// Create new UID set
163    pub fn new(ranges: Vec<UidRange>) -> Self {
164        Self { ranges }
165    }
166
167    /// Parse UID set from string
168    ///
169    /// Examples: "1:5", "1,3,5", "1:100,200:*"
170    pub fn parse(s: &str) -> Result<Self, QResyncError> {
171        let parts: Vec<&str> = s.split(',').collect();
172        let mut ranges = Vec::new();
173
174        for part in parts {
175            ranges.push(UidRange::parse(part)?);
176        }
177
178        if ranges.is_empty() {
179            return Err(QResyncError::InvalidUidSet(s.to_string()));
180        }
181
182        Ok(Self { ranges })
183    }
184
185    /// Check if a UID is in this set
186    pub fn contains(&self, uid: u32) -> bool {
187        self.ranges.iter().any(|range| range.contains(uid))
188    }
189
190    /// Get all ranges
191    pub fn ranges(&self) -> &[UidRange] {
192        &self.ranges
193    }
194}
195
196impl fmt::Display for UidSet {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        let ranges_str: Vec<String> = self.ranges.iter().map(|r| r.to_string()).collect();
199        write!(f, "{}", ranges_str.join(","))
200    }
201}
202
203/// UID range
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub enum UidRange {
206    /// Single UID
207    Single(u32),
208    /// Range start:end
209    Range { start: u32, end: u32 },
210    /// Range start:* (to highest)
211    RangeToMax { start: u32 },
212}
213
214impl UidRange {
215    /// Parse UID range from string
216    pub fn parse(s: &str) -> Result<Self, QResyncError> {
217        if s.contains(':') {
218            let parts: Vec<&str> = s.split(':').collect();
219            if parts.len() != 2 {
220                return Err(QResyncError::InvalidUidRange(s.to_string()));
221            }
222
223            let start = parts[0]
224                .parse::<u32>()
225                .map_err(|_| QResyncError::InvalidUidRange(s.to_string()))?;
226
227            if parts[1] == "*" {
228                Ok(Self::RangeToMax { start })
229            } else {
230                let end = parts[1]
231                    .parse::<u32>()
232                    .map_err(|_| QResyncError::InvalidUidRange(s.to_string()))?;
233
234                if start > end {
235                    return Err(QResyncError::InvalidUidRange(s.to_string()));
236                }
237
238                Ok(Self::Range { start, end })
239            }
240        } else {
241            let uid = s
242                .parse::<u32>()
243                .map_err(|_| QResyncError::InvalidUidRange(s.to_string()))?;
244            Ok(Self::Single(uid))
245        }
246    }
247
248    /// Check if this range contains the given UID
249    pub fn contains(&self, uid: u32) -> bool {
250        match self {
251            Self::Single(u) => *u == uid,
252            Self::Range { start, end } => uid >= *start && uid <= *end,
253            Self::RangeToMax { start } => uid >= *start,
254        }
255    }
256}
257
258impl fmt::Display for UidRange {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        match self {
261            Self::Single(uid) => write!(f, "{}", uid),
262            Self::Range { start, end } => write!(f, "{}:{}", start, end),
263            Self::RangeToMax { start } => write!(f, "{}:*", start),
264        }
265    }
266}
267
268/// Sequence match data (sequence:UID pairs)
269///
270/// Example: "(1 2 3 4 5 6)" maps to UIDs in known-uids
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct SeqMatchData {
273    /// Sequence numbers
274    sequences: Vec<u32>,
275}
276
277impl SeqMatchData {
278    /// Create new sequence match data
279    pub fn new(sequences: Vec<u32>) -> Self {
280        Self { sequences }
281    }
282
283    /// Parse sequence match data from string
284    ///
285    /// Format: (seq1 seq2 seq3 ...)
286    pub fn parse(s: &str) -> Result<Self, QResyncError> {
287        let s = s.trim().trim_matches(|c| c == '(' || c == ')');
288        let parts: Vec<&str> = s.split_whitespace().collect();
289
290        let mut sequences = Vec::new();
291        for part in parts {
292            let seq = part
293                .parse::<u32>()
294                .map_err(|_| QResyncError::InvalidSeqMatchData(s.to_string()))?;
295            sequences.push(seq);
296        }
297
298        Ok(Self { sequences })
299    }
300
301    /// Get sequences
302    pub fn sequences(&self) -> &[u32] {
303        &self.sequences
304    }
305}
306
307impl fmt::Display for SeqMatchData {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        let seqs: Vec<String> = self.sequences.iter().map(|s| s.to_string()).collect();
310        write!(f, "({})", seqs.join(" "))
311    }
312}
313
314/// VANISHED response for reporting expunged messages
315///
316/// VANISHED responses efficiently report UIDs that have been expunged
317/// since the last synchronization point.
318#[derive(Debug, Clone, PartialEq, Eq)]
319pub struct VanishedResponse {
320    /// Set of vanished (expunged) UIDs
321    pub uids: UidSet,
322    /// Whether this is an earlier response (used with EARLIER tag)
323    pub earlier: bool,
324}
325
326impl VanishedResponse {
327    /// Create new VANISHED response
328    pub fn new(uids: UidSet) -> Self {
329        Self {
330            uids,
331            earlier: false,
332        }
333    }
334
335    /// Create VANISHED (EARLIER) response
336    pub fn earlier(uids: UidSet) -> Self {
337        Self {
338            uids,
339            earlier: true,
340        }
341    }
342
343    /// Format as IMAP VANISHED response
344    ///
345    /// Examples:
346    /// - * VANISHED 1:5,7,9:12
347    /// - * VANISHED (EARLIER) 1:5,7,9:12
348    pub fn to_imap_response(&self) -> String {
349        if self.earlier {
350            format!("* VANISHED (EARLIER) {}", self.uids)
351        } else {
352            format!("* VANISHED {}", self.uids)
353        }
354    }
355
356    /// Parse VANISHED response
357    pub fn parse(line: &str) -> Result<Self, QResyncError> {
358        let line = line.trim();
359
360        if !line.starts_with("* VANISHED") && !line.starts_with("VANISHED") {
361            return Err(QResyncError::InvalidVanishedResponse(line.to_string()));
362        }
363
364        let line = line
365            .trim_start_matches("* ")
366            .trim_start_matches("VANISHED")
367            .trim();
368
369        let (earlier, uid_str) = if line.starts_with("(EARLIER)") {
370            (true, line.trim_start_matches("(EARLIER)").trim())
371        } else {
372            (false, line)
373        };
374
375        let uids = UidSet::parse(uid_str)?;
376
377        Ok(Self { uids, earlier })
378    }
379}
380
381/// QRESYNC-related errors
382#[derive(Debug, Clone, PartialEq, Eq)]
383pub enum QResyncError {
384    /// Invalid QRESYNC syntax
385    InvalidSyntax(String),
386    /// Invalid UIDVALIDITY value
387    InvalidUidValidity(String),
388    /// Invalid MODSEQ value
389    InvalidModSeq(String),
390    /// MODSEQ cannot be zero
391    ZeroModSeq,
392    /// Invalid UID set
393    InvalidUidSet(String),
394    /// Invalid UID range
395    InvalidUidRange(String),
396    /// Invalid sequence match data
397    InvalidSeqMatchData(String),
398    /// Invalid VANISHED response
399    InvalidVanishedResponse(String),
400    /// QRESYNC not enabled
401    NotEnabled,
402    /// CONDSTORE not enabled (required for QRESYNC)
403    CondStoreRequired,
404    /// UIDVALIDITY mismatch
405    UidValidityMismatch {
406        /// Expected UIDVALIDITY
407        expected: u32,
408        /// Actual UIDVALIDITY
409        actual: u32,
410    },
411}
412
413impl fmt::Display for QResyncError {
414    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415        match self {
416            QResyncError::InvalidSyntax(s) => write!(f, "Invalid QRESYNC syntax: {}", s),
417            QResyncError::InvalidUidValidity(s) => write!(f, "Invalid UIDVALIDITY: {}", s),
418            QResyncError::InvalidModSeq(s) => write!(f, "Invalid MODSEQ: {}", s),
419            QResyncError::ZeroModSeq => write!(f, "MODSEQ cannot be zero"),
420            QResyncError::InvalidUidSet(s) => write!(f, "Invalid UID set: {}", s),
421            QResyncError::InvalidUidRange(s) => write!(f, "Invalid UID range: {}", s),
422            QResyncError::InvalidSeqMatchData(s) => write!(f, "Invalid sequence match data: {}", s),
423            QResyncError::InvalidVanishedResponse(s) => {
424                write!(f, "Invalid VANISHED response: {}", s)
425            }
426            QResyncError::NotEnabled => write!(f, "QRESYNC not enabled"),
427            QResyncError::CondStoreRequired => write!(f, "CONDSTORE required for QRESYNC"),
428            QResyncError::UidValidityMismatch { expected, actual } => {
429                write!(
430                    f,
431                    "UIDVALIDITY mismatch: expected {}, got {}",
432                    expected, actual
433                )
434            }
435        }
436    }
437}
438
439impl std::error::Error for QResyncError {}
440
441/// Quick resynchronization logic
442///
443/// This structure provides the logic for efficiently resynchronizing a mailbox
444/// using QRESYNC parameters.
445#[derive(Debug)]
446pub struct QResyncLogic {
447    /// Current UIDVALIDITY
448    pub uidvalidity: u32,
449    /// Highest MODSEQ in mailbox
450    pub highest_modseq: ModSeq,
451}
452
453impl QResyncLogic {
454    /// Create new QRESYNC logic handler
455    pub fn new(uidvalidity: u32, highest_modseq: ModSeq) -> Self {
456        Self {
457            uidvalidity,
458            highest_modseq,
459        }
460    }
461
462    /// Validate QRESYNC parameters against current mailbox state
463    ///
464    /// Returns an error if UIDVALIDITY doesn't match
465    pub fn validate_params(&self, params: &QResyncParams) -> Result<(), QResyncError> {
466        if params.uidvalidity != self.uidvalidity {
467            return Err(QResyncError::UidValidityMismatch {
468                expected: params.uidvalidity,
469                actual: self.uidvalidity,
470            });
471        }
472        Ok(())
473    }
474
475    /// Determine which UIDs have been expunged since the given MODSEQ
476    ///
477    /// This should be called with the list of all UIDs that existed at the
478    /// client's last known MODSEQ and the current list of UIDs in the mailbox.
479    pub fn find_vanished_uids(&self, known_uids: &UidSet, current_uids: &[u32]) -> Vec<u32> {
480        let mut vanished = Vec::new();
481
482        // Check each range in the known UIDs set
483        for range in known_uids.ranges() {
484            match range {
485                UidRange::Single(uid) => {
486                    if !current_uids.contains(uid) {
487                        vanished.push(*uid);
488                    }
489                }
490                UidRange::Range { start, end } => {
491                    for uid in *start..=*end {
492                        if !current_uids.contains(&uid) {
493                            vanished.push(uid);
494                        }
495                    }
496                }
497                UidRange::RangeToMax { start } => {
498                    // For open-ended ranges, check from start to max known UID
499                    let max_uid = current_uids.iter().max().copied().unwrap_or(*start);
500                    for uid in *start..=max_uid {
501                        if !current_uids.contains(&uid) {
502                            vanished.push(uid);
503                        }
504                    }
505                }
506            }
507        }
508
509        vanished.sort_unstable();
510        vanished
511    }
512
513    /// Create a VANISHED response from expunged UIDs
514    ///
515    /// Optimizes the list of UIDs into ranges for efficiency
516    pub fn create_vanished_response(
517        &self,
518        vanished_uids: Vec<u32>,
519        earlier: bool,
520    ) -> Option<VanishedResponse> {
521        if vanished_uids.is_empty() {
522            return None;
523        }
524
525        let ranges = Self::compress_to_ranges(vanished_uids);
526        let uid_set = UidSet::new(ranges);
527
528        Some(if earlier {
529            VanishedResponse::earlier(uid_set)
530        } else {
531            VanishedResponse::new(uid_set)
532        })
533    }
534
535    /// Compress a list of UIDs into ranges
536    ///
537    /// Example: [1,2,3,5,6,7,10] -> [1:3, 5:7, 10]
538    fn compress_to_ranges(mut uids: Vec<u32>) -> Vec<UidRange> {
539        if uids.is_empty() {
540            return Vec::new();
541        }
542
543        uids.sort_unstable();
544        uids.dedup();
545
546        let mut ranges = Vec::new();
547        let mut range_start = uids[0];
548        let mut range_end = uids[0];
549
550        for &uid in &uids[1..] {
551            if uid == range_end + 1 {
552                // Continue current range
553                range_end = uid;
554            } else {
555                // End current range and start new one
556                if range_start == range_end {
557                    ranges.push(UidRange::Single(range_start));
558                } else {
559                    ranges.push(UidRange::Range {
560                        start: range_start,
561                        end: range_end,
562                    });
563                }
564                range_start = uid;
565                range_end = uid;
566            }
567        }
568
569        // Add final range
570        if range_start == range_end {
571            ranges.push(UidRange::Single(range_start));
572        } else {
573            ranges.push(UidRange::Range {
574                start: range_start,
575                end: range_end,
576            });
577        }
578
579        ranges
580    }
581
582    /// Check if resynchronization is needed
583    ///
584    /// Returns true if the client's MODSEQ is outdated
585    pub fn needs_resync(&self, client_modseq: ModSeq) -> bool {
586        client_modseq < self.highest_modseq
587    }
588}
589
590#[cfg(test)]
591mod tests {
592    use super::*;
593
594    #[test]
595    fn test_qresync_state() {
596        let state = QResyncState::Disabled;
597        assert!(!state.is_enabled());
598
599        let state = QResyncState::Enabled;
600        assert!(state.is_enabled());
601    }
602
603    #[test]
604    fn test_qresync_params_basic() {
605        let params = QResyncParams::new(12345, ModSeq::new(67890));
606        assert_eq!(params.uidvalidity, 12345);
607        assert_eq!(params.modseq.value(), 67890);
608        assert!(params.known_uids.is_none());
609        assert!(params.seq_match_data.is_none());
610    }
611
612    #[test]
613    fn test_qresync_params_parse_basic() {
614        let params = QResyncParams::parse("(12345 67890)").expect("basic QRESYNC params parse");
615        assert_eq!(params.uidvalidity, 12345);
616        assert_eq!(params.modseq.value(), 67890);
617        assert!(params.known_uids.is_none());
618    }
619
620    #[test]
621    fn test_qresync_params_parse_with_known_uids() {
622        let params = QResyncParams::parse("(12345 67890 1:100)")
623            .expect("QRESYNC params with known UIDs parse");
624        assert_eq!(params.uidvalidity, 12345);
625        assert_eq!(params.modseq.value(), 67890);
626        assert!(params.known_uids.is_some());
627    }
628
629    #[test]
630    fn test_qresync_params_parse_full() {
631        let params = QResyncParams::parse("(12345 67890 1:100 (1 2 3 4 5))")
632            .expect("full QRESYNC params parse");
633        assert_eq!(params.uidvalidity, 12345);
634        assert_eq!(params.modseq.value(), 67890);
635        assert!(params.known_uids.is_some());
636        assert!(params.seq_match_data.is_some());
637    }
638
639    #[test]
640    fn test_qresync_params_parse_invalid() {
641        assert!(QResyncParams::parse("(12345)").is_err());
642        assert!(QResyncParams::parse("(abc 67890)").is_err());
643        assert!(QResyncParams::parse("(12345 0)").is_err());
644    }
645
646    #[test]
647    fn test_qresync_params_to_imap_string() {
648        let params = QResyncParams::new(12345, ModSeq::new(67890));
649        assert_eq!(params.to_imap_string(), "(12345 67890)");
650
651        let params = QResyncParams::with_known_uids(
652            12345,
653            ModSeq::new(67890),
654            UidSet::parse("1:100").expect("valid UID set parse"),
655        );
656        assert_eq!(params.to_imap_string(), "(12345 67890 1:100)");
657    }
658
659    #[test]
660    fn test_uid_range_parse_single() {
661        let range = UidRange::parse("42").expect("single UID range parse");
662        assert_eq!(range, UidRange::Single(42));
663        assert!(range.contains(42));
664        assert!(!range.contains(41));
665        assert_eq!(range.to_string(), "42");
666    }
667
668    #[test]
669    fn test_uid_range_parse_range() {
670        let range = UidRange::parse("10:20").expect("UID range 10:20 parse");
671        assert_eq!(range, UidRange::Range { start: 10, end: 20 });
672        assert!(range.contains(10));
673        assert!(range.contains(15));
674        assert!(range.contains(20));
675        assert!(!range.contains(9));
676        assert!(!range.contains(21));
677        assert_eq!(range.to_string(), "10:20");
678    }
679
680    #[test]
681    fn test_uid_range_parse_to_max() {
682        let range = UidRange::parse("100:*").expect("UID range 100:* parse");
683        assert_eq!(range, UidRange::RangeToMax { start: 100 });
684        assert!(range.contains(100));
685        assert!(range.contains(1000));
686        assert!(range.contains(u32::MAX));
687        assert!(!range.contains(99));
688        assert_eq!(range.to_string(), "100:*");
689    }
690
691    #[test]
692    fn test_uid_range_parse_invalid() {
693        assert!(UidRange::parse("abc").is_err());
694        assert!(UidRange::parse("10:5").is_err());
695        assert!(UidRange::parse("10:20:30").is_err());
696    }
697
698    #[test]
699    fn test_uid_set_parse_single() {
700        let set = UidSet::parse("42").expect("single UID set parse");
701        assert_eq!(set.ranges.len(), 1);
702        assert!(set.contains(42));
703        assert!(!set.contains(41));
704        assert_eq!(set.to_string(), "42");
705    }
706
707    #[test]
708    fn test_uid_set_parse_multiple() {
709        let set = UidSet::parse("1,3,5").expect("multiple single UIDs set parse");
710        assert_eq!(set.ranges.len(), 3);
711        assert!(set.contains(1));
712        assert!(!set.contains(2));
713        assert!(set.contains(3));
714        assert!(!set.contains(4));
715        assert!(set.contains(5));
716        assert_eq!(set.to_string(), "1,3,5");
717    }
718
719    #[test]
720    fn test_uid_set_parse_ranges() {
721        let set = UidSet::parse("1:5,10:20,100:*").expect("UID set with ranges parse");
722        assert_eq!(set.ranges.len(), 3);
723        assert!(set.contains(1));
724        assert!(set.contains(5));
725        assert!(!set.contains(6));
726        assert!(set.contains(10));
727        assert!(set.contains(20));
728        assert!(!set.contains(21));
729        assert!(set.contains(100));
730        assert!(set.contains(1000));
731        assert_eq!(set.to_string(), "1:5,10:20,100:*");
732    }
733
734    #[test]
735    fn test_uid_set_parse_mixed() {
736        let set = UidSet::parse("1,5:10,15,20:*").expect("mixed UID set parse");
737        assert_eq!(set.ranges.len(), 4);
738        assert!(set.contains(1));
739        assert!(set.contains(7));
740        assert!(set.contains(15));
741        assert!(set.contains(100));
742        assert_eq!(set.to_string(), "1,5:10,15,20:*");
743    }
744
745    #[test]
746    fn test_seq_match_data_parse() {
747        let data = SeqMatchData::parse("(1 2 3 4 5)").expect("SeqMatchData with parens parse");
748        assert_eq!(data.sequences.len(), 5);
749        assert_eq!(data.sequences[0], 1);
750        assert_eq!(data.sequences[4], 5);
751        assert_eq!(data.to_string(), "(1 2 3 4 5)");
752    }
753
754    #[test]
755    fn test_seq_match_data_parse_without_parens() {
756        let data = SeqMatchData::parse("1 2 3").expect("SeqMatchData without parens parse");
757        assert_eq!(data.sequences.len(), 3);
758        assert_eq!(data.sequences(), &[1, 2, 3]);
759    }
760
761    #[test]
762    fn test_vanished_response_new() {
763        let uids = UidSet::parse("1:5,7,9:12").expect("UID set parse for VANISHED test");
764        let response = VanishedResponse::new(uids);
765        assert!(!response.earlier);
766        assert_eq!(response.to_imap_response(), "* VANISHED 1:5,7,9:12");
767    }
768
769    #[test]
770    fn test_vanished_response_earlier() {
771        let uids = UidSet::parse("1:5,7,9:12").expect("UID set parse for VANISHED EARLIER test");
772        let response = VanishedResponse::earlier(uids);
773        assert!(response.earlier);
774        assert_eq!(
775            response.to_imap_response(),
776            "* VANISHED (EARLIER) 1:5,7,9:12"
777        );
778    }
779
780    #[test]
781    fn test_vanished_response_parse() {
782        let response =
783            VanishedResponse::parse("* VANISHED 1:5,7").expect("VANISHED response parse");
784        assert!(!response.earlier);
785        assert!(response.uids.contains(1));
786        assert!(response.uids.contains(5));
787        assert!(response.uids.contains(7));
788
789        let response = VanishedResponse::parse("* VANISHED (EARLIER) 1:5,7")
790            .expect("VANISHED (EARLIER) response parse");
791        assert!(response.earlier);
792        assert!(response.uids.contains(1));
793    }
794
795    #[test]
796    fn test_vanished_response_parse_without_star() {
797        let response =
798            VanishedResponse::parse("VANISHED 1:5").expect("VANISHED without * prefix parse");
799        assert!(!response.earlier);
800        assert!(response.uids.contains(1));
801    }
802
803    #[test]
804    fn test_qresync_error_display() {
805        let err = QResyncError::NotEnabled;
806        assert_eq!(err.to_string(), "QRESYNC not enabled");
807
808        let err = QResyncError::ZeroModSeq;
809        assert_eq!(err.to_string(), "MODSEQ cannot be zero");
810
811        let err = QResyncError::UidValidityMismatch {
812            expected: 100,
813            actual: 200,
814        };
815        assert_eq!(
816            err.to_string(),
817            "UIDVALIDITY mismatch: expected 100, got 200"
818        );
819    }
820
821    #[test]
822    fn test_uid_set_empty() {
823        assert!(UidSet::parse("").is_err());
824    }
825
826    #[test]
827    fn test_qresync_params_with_seq_match_data() {
828        let known_uids = UidSet::parse("1:100").expect("UID set 1:100 parse");
829        let seq_match = SeqMatchData::new(vec![1, 2, 3]);
830        let params =
831            QResyncParams::with_seq_match_data(12345, ModSeq::new(67890), known_uids, seq_match);
832        assert_eq!(params.uidvalidity, 12345);
833        assert!(params.seq_match_data.is_some());
834        assert_eq!(params.to_imap_string(), "(12345 67890 1:100 (1 2 3))");
835    }
836
837    #[test]
838    fn test_qresync_logic_new() {
839        let logic = QResyncLogic::new(12345, ModSeq::new(67890));
840        assert_eq!(logic.uidvalidity, 12345);
841        assert_eq!(logic.highest_modseq.value(), 67890);
842    }
843
844    #[test]
845    fn test_qresync_logic_validate_params_success() {
846        let logic = QResyncLogic::new(12345, ModSeq::new(100));
847        let params = QResyncParams::new(12345, ModSeq::new(50));
848        assert!(logic.validate_params(&params).is_ok());
849    }
850
851    #[test]
852    fn test_qresync_logic_validate_params_mismatch() {
853        let logic = QResyncLogic::new(12345, ModSeq::new(100));
854        let params = QResyncParams::new(99999, ModSeq::new(50));
855        let result = logic.validate_params(&params);
856        assert!(result.is_err());
857        match result.unwrap_err() {
858            QResyncError::UidValidityMismatch { expected, actual } => {
859                assert_eq!(expected, 99999);
860                assert_eq!(actual, 12345);
861            }
862            _ => panic!("Expected UidValidityMismatch error"),
863        }
864    }
865
866    #[test]
867    fn test_qresync_logic_find_vanished_uids_single() {
868        let logic = QResyncLogic::new(12345, ModSeq::new(100));
869        let known_uids = UidSet::parse("1,2,3,4,5").expect("UID set 1,2,3,4,5 parse");
870        let current_uids = vec![1, 3, 5];
871        let vanished = logic.find_vanished_uids(&known_uids, &current_uids);
872        assert_eq!(vanished, vec![2, 4]);
873    }
874
875    #[test]
876    fn test_qresync_logic_find_vanished_uids_range() {
877        let logic = QResyncLogic::new(12345, ModSeq::new(100));
878        let known_uids = UidSet::parse("1:10").expect("UID set 1:10 parse");
879        let current_uids = vec![1, 2, 5, 6, 7, 10];
880        let vanished = logic.find_vanished_uids(&known_uids, &current_uids);
881        assert_eq!(vanished, vec![3, 4, 8, 9]);
882    }
883
884    #[test]
885    fn test_qresync_logic_find_vanished_uids_none() {
886        let logic = QResyncLogic::new(12345, ModSeq::new(100));
887        let known_uids = UidSet::parse("1,2,3").expect("UID set 1,2,3 parse");
888        let current_uids = vec![1, 2, 3];
889        let vanished = logic.find_vanished_uids(&known_uids, &current_uids);
890        assert!(vanished.is_empty());
891    }
892
893    #[test]
894    fn test_qresync_logic_find_vanished_uids_mixed() {
895        let logic = QResyncLogic::new(12345, ModSeq::new(100));
896        let known_uids = UidSet::parse("1:5,10,15:20").expect("UID set 1:5,10,15:20 parse");
897        let current_uids = vec![1, 3, 5, 15, 17, 20];
898        let vanished = logic.find_vanished_uids(&known_uids, &current_uids);
899        assert_eq!(vanished, vec![2, 4, 10, 16, 18, 19]);
900    }
901
902    #[test]
903    fn test_qresync_logic_compress_to_ranges_single() {
904        let ranges = QResyncLogic::compress_to_ranges(vec![5]);
905        assert_eq!(ranges.len(), 1);
906        assert_eq!(ranges[0], UidRange::Single(5));
907    }
908
909    #[test]
910    fn test_qresync_logic_compress_to_ranges_consecutive() {
911        let ranges = QResyncLogic::compress_to_ranges(vec![1, 2, 3, 4, 5]);
912        assert_eq!(ranges.len(), 1);
913        assert_eq!(ranges[0], UidRange::Range { start: 1, end: 5 });
914    }
915
916    #[test]
917    fn test_qresync_logic_compress_to_ranges_gaps() {
918        let ranges = QResyncLogic::compress_to_ranges(vec![1, 3, 5, 7]);
919        assert_eq!(ranges.len(), 4);
920        assert_eq!(ranges[0], UidRange::Single(1));
921        assert_eq!(ranges[1], UidRange::Single(3));
922        assert_eq!(ranges[2], UidRange::Single(5));
923        assert_eq!(ranges[3], UidRange::Single(7));
924    }
925
926    #[test]
927    fn test_qresync_logic_compress_to_ranges_mixed() {
928        let ranges = QResyncLogic::compress_to_ranges(vec![1, 2, 3, 5, 7, 8, 9, 15]);
929        assert_eq!(ranges.len(), 4);
930        assert_eq!(ranges[0], UidRange::Range { start: 1, end: 3 });
931        assert_eq!(ranges[1], UidRange::Single(5));
932        assert_eq!(ranges[2], UidRange::Range { start: 7, end: 9 });
933        assert_eq!(ranges[3], UidRange::Single(15));
934    }
935
936    #[test]
937    fn test_qresync_logic_compress_to_ranges_unordered() {
938        let ranges = QResyncLogic::compress_to_ranges(vec![5, 1, 3, 2, 4]);
939        assert_eq!(ranges.len(), 1);
940        assert_eq!(ranges[0], UidRange::Range { start: 1, end: 5 });
941    }
942
943    #[test]
944    fn test_qresync_logic_compress_to_ranges_duplicates() {
945        let ranges = QResyncLogic::compress_to_ranges(vec![1, 2, 2, 3, 3, 4]);
946        assert_eq!(ranges.len(), 1);
947        assert_eq!(ranges[0], UidRange::Range { start: 1, end: 4 });
948    }
949
950    #[test]
951    fn test_qresync_logic_create_vanished_response_empty() {
952        let logic = QResyncLogic::new(12345, ModSeq::new(100));
953        let response = logic.create_vanished_response(vec![], false);
954        assert!(response.is_none());
955    }
956
957    #[test]
958    fn test_qresync_logic_create_vanished_response_single() {
959        let logic = QResyncLogic::new(12345, ModSeq::new(100));
960        let response = logic
961            .create_vanished_response(vec![5], false)
962            .expect("VANISHED response for single UID");
963        assert!(!response.earlier);
964        assert_eq!(response.to_imap_response(), "* VANISHED 5");
965    }
966
967    #[test]
968    fn test_qresync_logic_create_vanished_response_range() {
969        let logic = QResyncLogic::new(12345, ModSeq::new(100));
970        let response = logic
971            .create_vanished_response(vec![1, 2, 3, 4, 5], false)
972            .expect("VANISHED response for sequential UIDs");
973        assert_eq!(response.to_imap_response(), "* VANISHED 1:5");
974    }
975
976    #[test]
977    fn test_qresync_logic_create_vanished_response_earlier() {
978        let logic = QResyncLogic::new(12345, ModSeq::new(100));
979        let response = logic
980            .create_vanished_response(vec![1, 2, 3], true)
981            .expect("VANISHED (EARLIER) response");
982        assert!(response.earlier);
983        assert_eq!(response.to_imap_response(), "* VANISHED (EARLIER) 1:3");
984    }
985
986    #[test]
987    fn test_qresync_logic_create_vanished_response_mixed() {
988        let logic = QResyncLogic::new(12345, ModSeq::new(100));
989        let response = logic
990            .create_vanished_response(vec![1, 2, 3, 5, 7, 8, 9], false)
991            .expect("VANISHED response for mixed UIDs");
992        assert_eq!(response.to_imap_response(), "* VANISHED 1:3,5,7:9");
993    }
994
995    #[test]
996    fn test_qresync_logic_needs_resync() {
997        let logic = QResyncLogic::new(12345, ModSeq::new(100));
998        assert!(logic.needs_resync(ModSeq::new(50)));
999        assert!(logic.needs_resync(ModSeq::new(99)));
1000        assert!(!logic.needs_resync(ModSeq::new(100)));
1001        assert!(!logic.needs_resync(ModSeq::new(101)));
1002    }
1003
1004    #[test]
1005    fn test_qresync_integration_full_resync() {
1006        // Simulate a full QRESYNC operation
1007        let logic = QResyncLogic::new(12345, ModSeq::new(200));
1008
1009        // Client's last known state
1010        let params = QResyncParams::with_known_uids(
1011            12345,
1012            ModSeq::new(100),
1013            UidSet::parse("1:50").expect("UID set 1:50 parse"),
1014        );
1015
1016        // Validate
1017        assert!(logic.validate_params(&params).is_ok());
1018        assert!(logic.needs_resync(params.modseq));
1019
1020        // Current mailbox state (some messages deleted)
1021        let current_uids: Vec<u32> = (1..=50).filter(|&n| n % 3 != 0).collect();
1022
1023        // Find vanished UIDs
1024        let known = params
1025            .known_uids
1026            .as_ref()
1027            .expect("known_uids should be set");
1028        let vanished = logic.find_vanished_uids(known, &current_uids);
1029        assert!(!vanished.is_empty());
1030
1031        // Create VANISHED response
1032        let response = logic.create_vanished_response(vanished, true);
1033        assert!(response.is_some());
1034        assert!(response.expect("VANISHED response should be Some").earlier);
1035    }
1036
1037    #[test]
1038    fn test_uid_range_boundary_values() {
1039        // Test with boundary values
1040        let range = UidRange::Range {
1041            start: 1,
1042            end: u32::MAX,
1043        };
1044        assert!(range.contains(1));
1045        assert!(range.contains(u32::MAX));
1046        assert!(range.contains(u32::MAX / 2));
1047    }
1048
1049    #[test]
1050    fn test_uid_set_large_range() {
1051        let set = UidSet::parse("1:4294967295").expect("UID set spanning full u32 range parse");
1052        assert!(set.contains(1));
1053        assert!(set.contains(1000000));
1054        assert!(set.contains(u32::MAX));
1055    }
1056
1057    #[test]
1058    fn test_vanished_response_roundtrip() {
1059        let original =
1060            VanishedResponse::earlier(UidSet::parse("1:5,10,20:25").expect("UID set parse"));
1061        let imap_str = original.to_imap_response();
1062        let parsed = VanishedResponse::parse(&imap_str).expect("roundtrip VANISHED response parse");
1063        assert_eq!(parsed.earlier, original.earlier);
1064        assert_eq!(parsed.to_imap_response(), original.to_imap_response());
1065    }
1066
1067    #[test]
1068    fn test_qresync_params_roundtrip() {
1069        let original = QResyncParams::with_seq_match_data(
1070            12345,
1071            ModSeq::new(67890),
1072            UidSet::parse("1:100,200:300").expect("UID set 1:100,200:300 parse"),
1073            SeqMatchData::new(vec![1, 2, 3, 4, 5]),
1074        );
1075        let imap_str = original.to_imap_string();
1076        let parsed = QResyncParams::parse(&imap_str).expect("roundtrip QRESYNC params parse");
1077        assert_eq!(parsed.uidvalidity, original.uidvalidity);
1078        assert_eq!(parsed.modseq.value(), original.modseq.value());
1079    }
1080}