Skip to main content

mailsis_utils/
imap.rs

1//! IMAP UID sequence-set parsing.
2//!
3//! IMAP `FETCH` and `SEARCH` commands accept sequence-set strings such as
4//! `"1:*"` or `"5:10"`. This module converts those strings into Rust
5//! ranges following [RFC 9051 ยง9](https://www.rfc-editor.org/rfc/rfc9051#section-9).
6
7use std::ops::RangeInclusive;
8
9/// Converts a string range from an IMAP UID FETCH command into a range of
10/// u32 values.
11///
12/// The input string should be in the format `start:end`, where `start` and
13/// `end` are u32 values.
14///
15/// If only one value is provided, the range will be from 1 to the provided
16/// value.
17///
18/// # Examples
19///
20/// ```rust
21/// let range = mailsis_utils::uid_fetch_range_str("1:10", 100);
22/// assert_eq!(range, Some(1..=10));
23/// ```
24///
25/// ```rust
26/// let range = mailsis_utils::uid_fetch_range_str("10", 100);
27/// assert_eq!(range, Some(10..=10));
28/// ```
29///
30/// ```rust
31/// let range = mailsis_utils::uid_fetch_range_str("1:*", 100);
32/// assert_eq!(range, Some(1..=100));
33/// ```
34///
35/// ```rust
36/// let range = mailsis_utils::uid_fetch_range_str("*", 100);
37/// assert_eq!(range, Some(100..=100));
38/// ```
39pub fn uid_fetch_range_str(input: &str, max_uid: u32) -> Option<RangeInclusive<u32>> {
40    let (start_str, end_str_opt) = if let Some((start, end)) = input.split_once(':') {
41        (start, Some(end))
42    } else {
43        (input, None)
44    };
45
46    uid_fetch_range(start_str, end_str_opt, max_uid)
47}
48
49pub fn uid_fetch_range(
50    start: &str,
51    end: Option<&str>,
52    max_uid: u32,
53) -> Option<RangeInclusive<u32>> {
54    let start = if start == "*" {
55        max_uid
56    } else {
57        start.parse::<u32>().ok()?
58    };
59
60    let end = match end {
61        Some("*") => max_uid,
62        Some(s) => s.parse::<u32>().ok()?,
63        None => start,
64    };
65
66    Some(start.min(end)..=start.max(end))
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn test_uid_fetch_range_str_simple_range() {
75        assert_eq!(uid_fetch_range_str("1:10", 100), Some(1..=10));
76    }
77
78    #[test]
79    fn test_uid_fetch_range_str_single_uid() {
80        assert_eq!(uid_fetch_range_str("10", 100), Some(10..=10));
81    }
82
83    #[test]
84    fn test_uid_fetch_range_str_wildcard_end() {
85        assert_eq!(uid_fetch_range_str("1:*", 100), Some(1..=100));
86    }
87
88    #[test]
89    fn test_uid_fetch_range_str_wildcard_only() {
90        assert_eq!(uid_fetch_range_str("*", 100), Some(100..=100));
91    }
92
93    #[test]
94    fn test_uid_fetch_range_str_reversed_range() {
95        assert_eq!(uid_fetch_range_str("10:1", 100), Some(1..=10));
96    }
97
98    #[test]
99    fn test_uid_fetch_range_str_invalid_input() {
100        assert_eq!(uid_fetch_range_str("abc", 100), None);
101    }
102
103    #[test]
104    fn test_uid_fetch_range_str_invalid_end() {
105        assert_eq!(uid_fetch_range_str("1:abc", 100), None);
106    }
107
108    #[test]
109    fn test_uid_fetch_range_str_same_start_end() {
110        assert_eq!(uid_fetch_range_str("5:5", 100), Some(5..=5));
111    }
112
113    #[test]
114    fn test_uid_fetch_range_with_end() {
115        assert_eq!(uid_fetch_range("1", Some("10"), 100), Some(1..=10));
116    }
117
118    #[test]
119    fn test_uid_fetch_range_without_end() {
120        assert_eq!(uid_fetch_range("5", None, 100), Some(5..=5));
121    }
122
123    #[test]
124    fn test_uid_fetch_range_wildcard_start() {
125        assert_eq!(uid_fetch_range("*", None, 50), Some(50..=50));
126    }
127
128    #[test]
129    fn test_uid_fetch_range_wildcard_end() {
130        assert_eq!(uid_fetch_range("1", Some("*"), 50), Some(1..=50));
131    }
132
133    #[test]
134    fn test_uid_fetch_range_both_wildcards() {
135        assert_eq!(uid_fetch_range("*", Some("*"), 50), Some(50..=50));
136    }
137
138    #[test]
139    fn test_uid_fetch_range_invalid_start() {
140        assert_eq!(uid_fetch_range("bad", None, 100), None);
141    }
142
143    #[test]
144    fn test_uid_fetch_range_invalid_end() {
145        assert_eq!(uid_fetch_range("1", Some("bad"), 100), None);
146    }
147
148    #[test]
149    fn test_uid_fetch_range_max_uid_zero() {
150        assert_eq!(uid_fetch_range_str("*", 0), Some(0..=0));
151    }
152}