1use rusmes_storage::ModSeq;
7use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CondStoreState {
12 Disabled,
14 Enabled,
16 ImplicitlyEnabled,
18}
19
20impl CondStoreState {
21 pub fn is_enabled(&self) -> bool {
23 matches!(self, Self::Enabled | Self::ImplicitlyEnabled)
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct ChangedSince {
30 pub modseq: ModSeq,
32}
33
34impl ChangedSince {
35 pub fn new(modseq: ModSeq) -> Self {
37 Self { modseq }
38 }
39
40 pub fn parse(args: &str) -> Result<Self, CondStoreError> {
44 let modseq = args
45 .trim()
46 .parse::<u64>()
47 .map_err(|_| CondStoreError::InvalidModSeq(args.to_string()))?;
48
49 if modseq == 0 {
50 return Err(CondStoreError::ZeroModSeq);
51 }
52
53 Ok(Self::new(ModSeq::new(modseq)))
54 }
55
56 pub fn matches(&self, message_modseq: ModSeq) -> bool {
58 message_modseq > self.modseq
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct UnchangedSince {
65 pub modseq: ModSeq,
67}
68
69impl UnchangedSince {
70 pub fn new(modseq: ModSeq) -> Self {
72 Self { modseq }
73 }
74
75 pub fn parse(args: &str) -> Result<Self, CondStoreError> {
79 let args = args.trim().trim_matches(|c| c == '(' || c == ')');
81
82 let parts: Vec<&str> = args.split_whitespace().collect();
84 if parts.len() != 2 || !parts[0].eq_ignore_ascii_case("UNCHANGEDSINCE") {
85 return Err(CondStoreError::InvalidUnchangedSince(args.to_string()));
86 }
87
88 let modseq = parts[1]
89 .parse::<u64>()
90 .map_err(|_| CondStoreError::InvalidModSeq(parts[1].to_string()))?;
91
92 if modseq == 0 {
93 return Err(CondStoreError::ZeroModSeq);
94 }
95
96 Ok(Self::new(ModSeq::new(modseq)))
97 }
98
99 pub fn can_modify(&self, current_modseq: ModSeq) -> bool {
101 current_modseq <= self.modseq
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum CondStoreError {
108 InvalidModSeq(String),
110 ZeroModSeq,
112 InvalidUnchangedSince(String),
114 NotEnabled,
116 StoreFailedModified {
118 failed_uids: Vec<u32>,
120 },
121}
122
123impl fmt::Display for CondStoreError {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 match self {
126 CondStoreError::InvalidModSeq(s) => write!(f, "Invalid MODSEQ: {}", s),
127 CondStoreError::ZeroModSeq => write!(f, "MODSEQ cannot be zero"),
128 CondStoreError::InvalidUnchangedSince(s) => {
129 write!(f, "Invalid UNCHANGEDSINCE syntax: {}", s)
130 }
131 CondStoreError::NotEnabled => write!(f, "CONDSTORE not enabled"),
132 CondStoreError::StoreFailedModified { failed_uids } => {
133 write!(f, "STORE failed for UIDs (modified): {:?}", failed_uids)
134 }
135 }
136 }
137}
138
139impl std::error::Error for CondStoreError {}
140
141#[derive(Debug, Clone)]
143pub struct CondStoreResponse {
144 pub uid: u32,
146 pub modseq: ModSeq,
148 pub seq: u32,
150}
151
152impl CondStoreResponse {
153 pub fn new(uid: u32, modseq: ModSeq, seq: u32) -> Self {
155 Self { uid, modseq, seq }
156 }
157
158 pub fn to_fetch_response(&self) -> String {
162 format!(
163 "* {} FETCH (UID {} MODSEQ ({}))",
164 self.seq, self.uid, self.modseq
165 )
166 }
167}
168
169#[derive(Debug, Clone)]
171pub struct CondStoreStatus {
172 pub mailbox: String,
174 pub highestmodseq: ModSeq,
176 pub exists: u32,
178 pub recent: u32,
180 pub unseen: u32,
182 pub uidvalidity: u32,
184 pub uidnext: u32,
186}
187
188impl CondStoreStatus {
189 pub fn to_status_response(&self) -> String {
193 format!(
194 "* STATUS {} (MESSAGES {} RECENT {} UNSEEN {} UIDVALIDITY {} UIDNEXT {} HIGHESTMODSEQ {})",
195 self.mailbox,
196 self.exists,
197 self.recent,
198 self.unseen,
199 self.uidvalidity,
200 self.uidnext,
201 self.highestmodseq
202 )
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_condstore_state() {
212 let state = CondStoreState::Disabled;
213 assert!(!state.is_enabled());
214
215 let state = CondStoreState::Enabled;
216 assert!(state.is_enabled());
217
218 let state = CondStoreState::ImplicitlyEnabled;
219 assert!(state.is_enabled());
220 }
221
222 #[test]
223 fn test_condstore_state_equality() {
224 assert_eq!(CondStoreState::Disabled, CondStoreState::Disabled);
225 assert_eq!(CondStoreState::Enabled, CondStoreState::Enabled);
226 assert_ne!(CondStoreState::Disabled, CondStoreState::Enabled);
227 }
228
229 #[test]
230 fn test_changed_since_parse() {
231 let cs = ChangedSince::parse("12345").expect("valid CHANGEDSINCE value");
232 assert_eq!(cs.modseq.value(), 12345);
233
234 assert!(ChangedSince::parse("0").is_err());
235 assert!(ChangedSince::parse("abc").is_err());
236 }
237
238 #[test]
239 fn test_changed_since_parse_with_whitespace() {
240 let cs =
241 ChangedSince::parse(" 12345 ").expect("CHANGEDSINCE with whitespace should parse");
242 assert_eq!(cs.modseq.value(), 12345);
243 }
244
245 #[test]
246 fn test_changed_since_parse_large_value() {
247 let cs = ChangedSince::parse("18446744073709551615")
248 .expect("CHANGEDSINCE with u64::MAX should parse");
249 assert_eq!(cs.modseq.value(), u64::MAX);
250 }
251
252 #[test]
253 fn test_changed_since_parse_invalid_values() {
254 assert!(matches!(
255 ChangedSince::parse("0").unwrap_err(),
256 CondStoreError::ZeroModSeq
257 ));
258 assert!(matches!(
259 ChangedSince::parse("abc").unwrap_err(),
260 CondStoreError::InvalidModSeq(_)
261 ));
262 assert!(matches!(
263 ChangedSince::parse("-1").unwrap_err(),
264 CondStoreError::InvalidModSeq(_)
265 ));
266 }
267
268 #[test]
269 fn test_changed_since_matches() {
270 let cs = ChangedSince::new(ModSeq::new(100));
271
272 assert!(!cs.matches(ModSeq::new(50)));
273 assert!(!cs.matches(ModSeq::new(100)));
274 assert!(cs.matches(ModSeq::new(150)));
275 }
276
277 #[test]
278 fn test_changed_since_matches_edge_cases() {
279 let cs = ChangedSince::new(ModSeq::new(1));
280 assert!(!cs.matches(ModSeq::new(1)));
281 assert!(cs.matches(ModSeq::new(2)));
282
283 let cs = ChangedSince::new(ModSeq::new(u64::MAX - 1));
284 assert!(cs.matches(ModSeq::new(u64::MAX)));
285 }
286
287 #[test]
288 fn test_changed_since_clone() {
289 let cs1 = ChangedSince::new(ModSeq::new(100));
290 let cs2 = cs1;
291 assert_eq!(cs1.modseq, cs2.modseq);
292 }
293
294 #[test]
295 fn test_unchanged_since_parse() {
296 let us = UnchangedSince::parse("(UNCHANGEDSINCE 12345)")
297 .expect("UNCHANGEDSINCE in parens should parse");
298 assert_eq!(us.modseq.value(), 12345);
299
300 let us = UnchangedSince::parse("UNCHANGEDSINCE 12345")
301 .expect("UNCHANGEDSINCE without parens should parse");
302 assert_eq!(us.modseq.value(), 12345);
303
304 assert!(UnchangedSince::parse("INVALID 12345").is_err());
305 assert!(UnchangedSince::parse("UNCHANGEDSINCE 0").is_err());
306 }
307
308 #[test]
309 fn test_unchanged_since_parse_case_insensitive() {
310 let us = UnchangedSince::parse("unchangedsince 12345")
311 .expect("lowercase unchangedsince should parse");
312 assert_eq!(us.modseq.value(), 12345);
313
314 let us = UnchangedSince::parse("UnChAnGeDsInCe 12345")
315 .expect("mixed-case UnChAnGeDsInCe should parse");
316 assert_eq!(us.modseq.value(), 12345);
317 }
318
319 #[test]
320 fn test_unchanged_since_parse_with_multiple_spaces() {
321 let us = UnchangedSince::parse(" UNCHANGEDSINCE 12345 ")
322 .expect("UNCHANGEDSINCE with extra spaces should parse");
323 assert_eq!(us.modseq.value(), 12345);
324 }
325
326 #[test]
327 fn test_unchanged_since_parse_invalid() {
328 assert!(UnchangedSince::parse("CHANGEDSINCE 12345").is_err());
329 assert!(UnchangedSince::parse("12345").is_err());
330 assert!(UnchangedSince::parse("UNCHANGEDSINCE").is_err());
331 assert!(UnchangedSince::parse("UNCHANGEDSINCE abc").is_err());
332 }
333
334 #[test]
335 fn test_unchanged_since_can_modify() {
336 let us = UnchangedSince::new(ModSeq::new(100));
337
338 assert!(us.can_modify(ModSeq::new(50)));
339 assert!(us.can_modify(ModSeq::new(100)));
340 assert!(!us.can_modify(ModSeq::new(150)));
341 }
342
343 #[test]
344 fn test_unchanged_since_can_modify_edge_cases() {
345 let us = UnchangedSince::new(ModSeq::new(1));
346 assert!(us.can_modify(ModSeq::new(1)));
347 assert!(!us.can_modify(ModSeq::new(2)));
348
349 let us = UnchangedSince::new(ModSeq::new(u64::MAX));
350 assert!(us.can_modify(ModSeq::new(u64::MAX)));
351 }
352
353 #[test]
354 fn test_condstore_error_display() {
355 let err = CondStoreError::InvalidModSeq("abc".to_string());
356 assert_eq!(err.to_string(), "Invalid MODSEQ: abc");
357
358 let err = CondStoreError::ZeroModSeq;
359 assert_eq!(err.to_string(), "MODSEQ cannot be zero");
360
361 let err = CondStoreError::NotEnabled;
362 assert_eq!(err.to_string(), "CONDSTORE not enabled");
363 }
364
365 #[test]
366 fn test_condstore_error_store_failed() {
367 let err = CondStoreError::StoreFailedModified {
368 failed_uids: vec![1, 2, 3],
369 };
370 assert!(err.to_string().contains("STORE failed"));
371 assert!(err.to_string().contains("[1, 2, 3]"));
372 }
373
374 #[test]
375 fn test_condstore_response() {
376 let resp = CondStoreResponse::new(42, ModSeq::new(12345), 1);
377 assert_eq!(
378 resp.to_fetch_response(),
379 "* 1 FETCH (UID 42 MODSEQ (12345))"
380 );
381 }
382
383 #[test]
384 fn test_condstore_response_multiple() {
385 let resp1 = CondStoreResponse::new(1, ModSeq::new(100), 1);
386 let resp2 = CondStoreResponse::new(2, ModSeq::new(200), 2);
387 let resp3 = CondStoreResponse::new(3, ModSeq::new(300), 3);
388
389 assert_eq!(resp1.to_fetch_response(), "* 1 FETCH (UID 1 MODSEQ (100))");
390 assert_eq!(resp2.to_fetch_response(), "* 2 FETCH (UID 2 MODSEQ (200))");
391 assert_eq!(resp3.to_fetch_response(), "* 3 FETCH (UID 3 MODSEQ (300))");
392 }
393
394 #[test]
395 fn test_condstore_response_clone() {
396 let resp1 = CondStoreResponse::new(42, ModSeq::new(12345), 1);
397 let resp2 = resp1.clone();
398 assert_eq!(resp1.uid, resp2.uid);
399 assert_eq!(resp1.modseq, resp2.modseq);
400 assert_eq!(resp1.seq, resp2.seq);
401 }
402
403 #[test]
404 fn test_condstore_status() {
405 let status = CondStoreStatus {
406 mailbox: "INBOX".to_string(),
407 highestmodseq: ModSeq::new(12345),
408 exists: 5,
409 recent: 2,
410 unseen: 3,
411 uidvalidity: 1,
412 uidnext: 6,
413 };
414
415 let response = status.to_status_response();
416 assert!(response.contains("INBOX"));
417 assert!(response.contains("HIGHESTMODSEQ 12345"));
418 assert!(response.contains("MESSAGES 5"));
419 }
420
421 #[test]
422 fn test_condstore_status_format() {
423 let status = CondStoreStatus {
424 mailbox: "Sent".to_string(),
425 highestmodseq: ModSeq::new(99999),
426 exists: 100,
427 recent: 5,
428 unseen: 10,
429 uidvalidity: 42,
430 uidnext: 101,
431 };
432
433 let response = status.to_status_response();
434 assert!(response.starts_with("* STATUS Sent"));
435 assert!(response.contains("MESSAGES 100"));
436 assert!(response.contains("RECENT 5"));
437 assert!(response.contains("UNSEEN 10"));
438 assert!(response.contains("UIDVALIDITY 42"));
439 assert!(response.contains("UIDNEXT 101"));
440 assert!(response.contains("HIGHESTMODSEQ 99999"));
441 }
442
443 #[test]
444 fn test_condstore_status_clone() {
445 let status1 = CondStoreStatus {
446 mailbox: "INBOX".to_string(),
447 highestmodseq: ModSeq::new(12345),
448 exists: 5,
449 recent: 2,
450 unseen: 3,
451 uidvalidity: 1,
452 uidnext: 6,
453 };
454 let status2 = status1.clone();
455 assert_eq!(status1.mailbox, status2.mailbox);
456 assert_eq!(status1.highestmodseq, status2.highestmodseq);
457 }
458
459 #[test]
460 fn test_condstore_status_zero_messages() {
461 let status = CondStoreStatus {
462 mailbox: "Empty".to_string(),
463 highestmodseq: ModSeq::new(1),
464 exists: 0,
465 recent: 0,
466 unseen: 0,
467 uidvalidity: 1,
468 uidnext: 1,
469 };
470
471 let response = status.to_status_response();
472 assert!(response.contains("MESSAGES 0"));
473 assert!(response.contains("RECENT 0"));
474 assert!(response.contains("UNSEEN 0"));
475 }
476}