1use crate::command::{ImapCommand, UidSubcommand};
34use nom::{
35 branch::alt,
36 bytes::complete::{tag_no_case, take_while1},
37 character::complete::space1,
38 IResult, Parser,
39};
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum LiteralType {
44 Synchronizing,
46 NonSynchronizing,
48}
49
50pub fn parse_command(input: &str) -> Result<(String, ImapCommand), String> {
52 let parts: Vec<&str> = input.splitn(3, ' ').collect();
56 if parts.is_empty() {
57 return Err("Empty command".to_string());
58 }
59
60 let tag = parts[0].to_string();
61
62 if parts.len() < 2 {
63 return Err("No command specified".to_string());
64 }
65
66 let cmd_line = if parts.len() == 3 {
67 format!("{} {}", parts[1], parts[2])
68 } else {
69 parts[1].to_string()
70 };
71
72 let (_rest, command) = parse_imap_command(&cmd_line).map_err(|e| e.to_string())?;
73
74 Ok((tag, command))
75}
76
77pub fn parse_append_command(
80 input: &str,
81 literal_data: Vec<u8>,
82) -> Result<(String, ImapCommand), String> {
83 let parts: Vec<&str> = input.splitn(3, ' ').collect();
85 if parts.len() < 3 {
86 return Err("Invalid APPEND command".to_string());
87 }
88
89 let tag = parts[0].to_string();
90
91 let args = parts[2];
93 let (mailbox, flags, date_time) = parse_append_args(args)?;
94
95 Ok((
96 tag,
97 ImapCommand::Append {
98 mailbox,
99 flags,
100 date_time,
101 message_literal: literal_data,
102 },
103 ))
104}
105
106fn parse_append_args(input: &str) -> Result<(String, Vec<String>, Option<String>), String> {
108 let mut parts: Vec<String> = Vec::new();
109 let mut in_quotes = false;
110 let mut in_parens = false;
111 let mut current = String::new();
112
113 for c in input.chars() {
114 match c {
115 '"' => {
116 in_quotes = !in_quotes;
117 current.push(c);
118 }
119 '(' if !in_quotes => {
120 in_parens = true;
121 current.push(c);
122 }
123 ')' if !in_quotes => {
124 in_parens = false;
125 current.push(c);
126 }
127 ' ' if !in_quotes && !in_parens => {
128 if !current.is_empty() {
129 parts.push(current.clone());
130 current.clear();
131 }
132 }
133 '{' if !in_quotes => {
134 if !current.is_empty() {
136 parts.push(current.clone());
137 }
138 break;
139 }
140 _ => current.push(c),
141 }
142 }
143
144 if !current.is_empty() && !current.starts_with('{') {
145 parts.push(current.clone());
146 }
147
148 if parts.is_empty() {
149 return Err("Missing mailbox name".to_string());
150 }
151
152 let mailbox = parts[0].trim_matches('"').to_string();
154 let mut flags = Vec::new();
155 let mut date_time = None;
156
157 let mut i = 1;
159 while i < parts.len() {
160 let part = &parts[i];
161 if part.starts_with('(') && part.ends_with(')') {
162 let flags_str = &part[1..part.len() - 1];
164 flags = flags_str
165 .split_whitespace()
166 .map(|s| s.to_string())
167 .collect();
168 } else if part.starts_with('"') {
169 date_time = Some(part.trim_matches('"').to_string());
171 }
172 i += 1;
173 }
174
175 Ok((mailbox, flags, date_time))
176}
177
178pub fn has_literal(input: &str) -> Option<(usize, LiteralType)> {
195 if let Some(start) = input.rfind('{') {
197 if let Some(end) = input[start..].find('}') {
198 let size_str = &input[start + 1..start + end];
199
200 if size_str.is_empty() || size_str.starts_with('+') || size_str.starts_with('-') {
202 return None;
203 }
204
205 if let Some(stripped) = size_str.strip_suffix('+') {
207 if let Ok(size) = stripped.parse::<usize>() {
208 return Some((size, LiteralType::NonSynchronizing));
209 }
210 } else {
211 if let Ok(size) = size_str.parse::<usize>() {
213 return Some((size, LiteralType::Synchronizing));
214 }
215 }
216 }
217 }
218 None
219}
220
221#[allow(dead_code)]
224pub fn get_literal_size(input: &str) -> Option<usize> {
225 has_literal(input).map(|(size, _)| size)
226}
227
228fn parse_imap_command(input: &str) -> IResult<&str, ImapCommand> {
229 alt((parse_imap_command_group1, parse_imap_command_group2)).parse(input)
231}
232
233fn parse_imap_command_group1(input: &str) -> IResult<&str, ImapCommand> {
234 alt((
235 parse_uid,
236 parse_login,
237 parse_authenticate,
238 parse_select,
239 parse_examine,
240 parse_fetch,
241 parse_store,
242 parse_search,
243 parse_list,
244 parse_lsub,
245 parse_subscribe,
246 parse_unsubscribe,
247 ))
248 .parse(input)
249}
250
251fn parse_imap_command_group2(input: &str) -> IResult<&str, ImapCommand> {
252 alt((
253 parse_create_special_use,
254 parse_create,
255 parse_delete,
256 parse_rename,
257 parse_copy,
258 parse_move,
259 parse_expunge,
260 parse_close,
261 parse_capability,
262 parse_logout,
263 parse_noop,
264 parse_idle,
265 parse_namespace,
266 ))
267 .parse(input)
268}
269
270fn parse_idle(input: &str) -> IResult<&str, ImapCommand> {
271 let (input, _) = tag_no_case("IDLE").parse(input)?;
272 Ok((input, ImapCommand::Idle))
273}
274
275fn parse_namespace(input: &str) -> IResult<&str, ImapCommand> {
276 let (input, _) = tag_no_case("NAMESPACE").parse(input)?;
277 Ok((input, ImapCommand::Namespace))
278}
279
280fn parse_login(input: &str) -> IResult<&str, ImapCommand> {
281 let (input, _) = tag_no_case("LOGIN").parse(input)?;
282 let (input, _) = space1(input)?;
283 let (input, user) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
284 let (input, _) = space1(input)?;
285 let (input, password) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
286
287 Ok((
288 input,
289 ImapCommand::Login {
290 user: user.to_string(),
291 password: password.to_string(),
292 },
293 ))
294}
295
296fn parse_authenticate(input: &str) -> IResult<&str, ImapCommand> {
297 let (input, _) = tag_no_case("AUTHENTICATE").parse(input)?;
298 let (input, _) = space1(input)?;
299 let (input, mechanism) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
300
301 let (input, initial_response) =
303 if let Ok((remaining, _)) = space1::<_, nom::error::Error<&str>>(input) {
304 let (remaining, response) = nom::combinator::rest(remaining)?;
305 (remaining, Some(response.trim().to_string()))
306 } else {
307 (input, None)
308 };
309
310 Ok((
311 input,
312 ImapCommand::Authenticate {
313 mechanism: mechanism.to_uppercase(),
314 initial_response,
315 },
316 ))
317}
318
319fn parse_select(input: &str) -> IResult<&str, ImapCommand> {
320 let (input, _) = tag_no_case("SELECT").parse(input)?;
321 let (input, _) = space1(input)?;
322 let (input, mailbox) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
323
324 Ok((
325 input,
326 ImapCommand::Select {
327 mailbox: mailbox.to_string(),
328 },
329 ))
330}
331
332fn parse_examine(input: &str) -> IResult<&str, ImapCommand> {
333 let (input, _) = tag_no_case("EXAMINE").parse(input)?;
334 let (input, _) = space1(input)?;
335 let (input, mailbox) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
336
337 Ok((
338 input,
339 ImapCommand::Examine {
340 mailbox: mailbox.to_string(),
341 },
342 ))
343}
344
345fn parse_fetch(input: &str) -> IResult<&str, ImapCommand> {
346 let (input, _) = tag_no_case("FETCH").parse(input)?;
347 let (input, _) = space1(input)?;
348 let (input, sequence) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
349 let (input, _) = space1(input)?;
350 let (input, items_str) = nom::combinator::rest(input)?;
351
352 let items: Vec<String> = items_str
354 .split_whitespace()
355 .map(|s| s.to_string())
356 .collect();
357
358 Ok((
359 input,
360 ImapCommand::Fetch {
361 sequence: sequence.to_string(),
362 items,
363 },
364 ))
365}
366
367fn parse_list(input: &str) -> IResult<&str, ImapCommand> {
368 let (input, _) = tag_no_case("LIST").parse(input)?;
369 let (input, _) = space1(input)?;
370 let (input, reference) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
371 let (input, _) = space1(input)?;
372 let (input, mailbox) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
373
374 Ok((
375 input,
376 ImapCommand::List {
377 reference: reference.to_string(),
378 mailbox: mailbox.to_string(),
379 },
380 ))
381}
382
383fn parse_create_special_use(input: &str) -> IResult<&str, ImapCommand> {
384 let (input, _) = tag_no_case("CREATE-SPECIAL-USE").parse(input)?;
385 let (input, _) = space1(input)?;
386 let (input, mailbox) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
387 let (input, _) = space1(input)?;
388 let (input, special_use) = nom::combinator::rest(input)?;
389
390 Ok((
391 input,
392 ImapCommand::CreateSpecialUse {
393 mailbox: mailbox.trim().to_string(),
394 special_use: special_use.trim().to_string(),
395 },
396 ))
397}
398
399fn parse_create(input: &str) -> IResult<&str, ImapCommand> {
400 let (input, _) = tag_no_case("CREATE").parse(input)?;
401 let (input, _) = space1(input)?;
402 let (input, mailbox) = nom::combinator::rest(input)?;
403
404 Ok((
405 input,
406 ImapCommand::Create {
407 mailbox: mailbox.trim().to_string(),
408 },
409 ))
410}
411
412fn parse_delete(input: &str) -> IResult<&str, ImapCommand> {
413 let (input, _) = tag_no_case("DELETE").parse(input)?;
414 let (input, _) = space1(input)?;
415 let (input, mailbox) = nom::combinator::rest(input)?;
416
417 Ok((
418 input,
419 ImapCommand::Delete {
420 mailbox: mailbox.trim().to_string(),
421 },
422 ))
423}
424
425fn parse_rename(input: &str) -> IResult<&str, ImapCommand> {
426 let (input, _) = tag_no_case("RENAME").parse(input)?;
427 let (input, _) = space1(input)?;
428 let (input, old) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
429 let (input, _) = space1(input)?;
430 let (input, new) = nom::combinator::rest(input)?;
431
432 Ok((
433 input,
434 ImapCommand::Rename {
435 old: old.trim_matches('"').to_string(),
436 new: new.trim().trim_matches('"').to_string(),
437 },
438 ))
439}
440
441fn parse_logout(input: &str) -> IResult<&str, ImapCommand> {
442 let (input, _) = tag_no_case("LOGOUT").parse(input)?;
443 Ok((input, ImapCommand::Logout))
444}
445
446fn parse_noop(input: &str) -> IResult<&str, ImapCommand> {
447 let (input, _) = tag_no_case("NOOP").parse(input)?;
448 Ok((input, ImapCommand::Noop))
449}
450
451fn parse_store(input: &str) -> IResult<&str, ImapCommand> {
452 use crate::command::StoreMode;
453
454 let (input, _) = tag_no_case("STORE").parse(input)?;
455 let (input, _) = space1(input)?;
456 let (input, sequence) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
457 let (input, _) = space1(input)?;
458 let (input, mode_str) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
459
460 let mode = if mode_str.eq_ignore_ascii_case("FLAGS") {
462 StoreMode::Replace
463 } else if mode_str.eq_ignore_ascii_case("+FLAGS") {
464 StoreMode::Add
465 } else if mode_str.eq_ignore_ascii_case("-FLAGS") {
466 StoreMode::Remove
467 } else {
468 StoreMode::Replace
470 };
471
472 let (input, _) = space1(input)?;
474 let (input, flags_str) = nom::combinator::rest(input)?;
475
476 let flags_str = flags_str.trim();
478 let flags: Vec<String> = if flags_str.starts_with('(') && flags_str.ends_with(')') {
479 flags_str[1..flags_str.len() - 1]
481 .split_whitespace()
482 .map(|s| s.to_string())
483 .collect()
484 } else {
485 vec![flags_str.to_string()]
487 };
488
489 Ok((
490 input,
491 ImapCommand::Store {
492 sequence: sequence.to_string(),
493 mode,
494 flags,
495 },
496 ))
497}
498
499fn parse_search(input: &str) -> IResult<&str, ImapCommand> {
500 let (input, _) = tag_no_case("SEARCH").parse(input)?;
501 let (input, _) = space1(input)?;
502 let (input, criteria_str) = nom::combinator::rest(input)?;
503
504 let criteria: Vec<String> = criteria_str
506 .split_whitespace()
507 .map(|s| s.to_string())
508 .collect();
509
510 Ok((input, ImapCommand::Search { criteria }))
511}
512
513fn parse_capability(input: &str) -> IResult<&str, ImapCommand> {
514 let (input, _) = tag_no_case("CAPABILITY").parse(input)?;
515 Ok((input, ImapCommand::Capability))
516}
517
518fn parse_copy(input: &str) -> IResult<&str, ImapCommand> {
519 let (input, _) = tag_no_case("COPY").parse(input)?;
520 let (input, _) = space1(input)?;
521 let (input, sequence) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
522 let (input, _) = space1(input)?;
523 let (input, mailbox) = nom::combinator::rest(input)?;
524
525 Ok((
526 input,
527 ImapCommand::Copy {
528 sequence: sequence.to_string(),
529 mailbox: mailbox.trim().trim_matches('"').to_string(),
530 },
531 ))
532}
533
534fn parse_move(input: &str) -> IResult<&str, ImapCommand> {
535 let (input, _) = tag_no_case("MOVE").parse(input)?;
536 let (input, _) = space1(input)?;
537 let (input, sequence) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
538 let (input, _) = space1(input)?;
539 let (input, mailbox) = nom::combinator::rest(input)?;
540
541 Ok((
542 input,
543 ImapCommand::Move {
544 sequence: sequence.to_string(),
545 mailbox: mailbox.trim().trim_matches('"').to_string(),
546 },
547 ))
548}
549
550fn parse_lsub(input: &str) -> IResult<&str, ImapCommand> {
551 let (input, _) = tag_no_case("LSUB").parse(input)?;
552 let (input, _) = space1(input)?;
553 let (input, reference) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
554 let (input, _) = space1(input)?;
555 let (input, mailbox) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
556
557 Ok((
558 input,
559 ImapCommand::Lsub {
560 reference: reference.to_string(),
561 mailbox: mailbox.to_string(),
562 },
563 ))
564}
565
566fn parse_subscribe(input: &str) -> IResult<&str, ImapCommand> {
567 let (input, _) = tag_no_case("SUBSCRIBE").parse(input)?;
568 let (input, _) = space1(input)?;
569 let (input, mailbox) = nom::combinator::rest(input)?;
570
571 Ok((
572 input,
573 ImapCommand::Subscribe {
574 mailbox: mailbox.trim().to_string(),
575 },
576 ))
577}
578
579fn parse_unsubscribe(input: &str) -> IResult<&str, ImapCommand> {
580 let (input, _) = tag_no_case("UNSUBSCRIBE").parse(input)?;
581 let (input, _) = space1(input)?;
582 let (input, mailbox) = nom::combinator::rest(input)?;
583
584 Ok((
585 input,
586 ImapCommand::Unsubscribe {
587 mailbox: mailbox.trim().to_string(),
588 },
589 ))
590}
591
592fn parse_expunge(input: &str) -> IResult<&str, ImapCommand> {
593 let (input, _) = tag_no_case("EXPUNGE").parse(input)?;
594 Ok((input, ImapCommand::Expunge))
595}
596
597fn parse_close(input: &str) -> IResult<&str, ImapCommand> {
598 let (input, _) = tag_no_case("CLOSE").parse(input)?;
599 Ok((input, ImapCommand::Close))
600}
601
602fn parse_uid(input: &str) -> IResult<&str, ImapCommand> {
603 let (input, _) = tag_no_case("UID").parse(input)?;
604 let (input, _) = space1(input)?;
605
606 let (input, subcommand) = alt((
608 parse_uid_fetch,
609 parse_uid_store,
610 parse_uid_search,
611 parse_uid_copy,
612 parse_uid_move,
613 parse_uid_expunge,
614 ))
615 .parse(input)?;
616
617 Ok((
618 input,
619 ImapCommand::Uid {
620 subcommand: Box::new(subcommand),
621 },
622 ))
623}
624
625fn parse_uid_fetch(input: &str) -> IResult<&str, crate::command::UidSubcommand> {
626 let (input, _) = tag_no_case("FETCH").parse(input)?;
627 let (input, _) = space1(input)?;
628 let (input, sequence) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
629 let (input, _) = space1(input)?;
630 let (input, items_str) = nom::combinator::rest(input)?;
631
632 let items: Vec<String> = items_str
634 .split_whitespace()
635 .map(|s| s.to_string())
636 .collect();
637
638 Ok((
639 input,
640 UidSubcommand::Fetch {
641 sequence: sequence.to_string(),
642 items,
643 },
644 ))
645}
646
647fn parse_uid_store(input: &str) -> IResult<&str, crate::command::UidSubcommand> {
648 use crate::command::{StoreMode, UidSubcommand};
649
650 let (input, _) = tag_no_case("STORE").parse(input)?;
651 let (input, _) = space1(input)?;
652 let (input, sequence) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
653 let (input, _) = space1(input)?;
654 let (input, mode_str) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
655
656 let mode = if mode_str.eq_ignore_ascii_case("FLAGS") {
658 StoreMode::Replace
659 } else if mode_str.eq_ignore_ascii_case("+FLAGS") {
660 StoreMode::Add
661 } else if mode_str.eq_ignore_ascii_case("-FLAGS") {
662 StoreMode::Remove
663 } else {
664 StoreMode::Replace
665 };
666
667 let (input, _) = space1(input)?;
669 let (input, flags_str) = nom::combinator::rest(input)?;
670
671 let flags_str = flags_str.trim();
673 let flags: Vec<String> = if flags_str.starts_with('(') && flags_str.ends_with(')') {
674 flags_str[1..flags_str.len() - 1]
676 .split_whitespace()
677 .map(|s| s.to_string())
678 .collect()
679 } else {
680 vec![flags_str.to_string()]
682 };
683
684 Ok((
685 input,
686 UidSubcommand::Store {
687 sequence: sequence.to_string(),
688 mode,
689 flags,
690 },
691 ))
692}
693
694fn parse_uid_search(input: &str) -> IResult<&str, crate::command::UidSubcommand> {
695 use crate::command::UidSubcommand;
696
697 let (input, _) = tag_no_case("SEARCH").parse(input)?;
698 let (input, _) = space1(input)?;
699 let (input, criteria_str) = nom::combinator::rest(input)?;
700
701 let criteria: Vec<String> = criteria_str
703 .split_whitespace()
704 .map(|s| s.to_string())
705 .collect();
706
707 Ok((input, UidSubcommand::Search { criteria }))
708}
709
710fn parse_uid_copy(input: &str) -> IResult<&str, crate::command::UidSubcommand> {
711 use crate::command::UidSubcommand;
712
713 let (input, _) = tag_no_case("COPY").parse(input)?;
714 let (input, _) = space1(input)?;
715 let (input, sequence) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
716 let (input, _) = space1(input)?;
717 let (input, mailbox) = nom::combinator::rest(input)?;
718
719 Ok((
720 input,
721 UidSubcommand::Copy {
722 sequence: sequence.to_string(),
723 mailbox: mailbox.trim().trim_matches('"').to_string(),
724 },
725 ))
726}
727
728fn parse_uid_move(input: &str) -> IResult<&str, crate::command::UidSubcommand> {
729 use crate::command::UidSubcommand;
730
731 let (input, _) = tag_no_case("MOVE").parse(input)?;
732 let (input, _) = space1(input)?;
733 let (input, sequence) = take_while1(|c: char| !c.is_whitespace()).parse(input)?;
734 let (input, _) = space1(input)?;
735 let (input, mailbox) = nom::combinator::rest(input)?;
736
737 Ok((
738 input,
739 UidSubcommand::Move {
740 sequence: sequence.to_string(),
741 mailbox: mailbox.trim().trim_matches('"').to_string(),
742 },
743 ))
744}
745
746fn parse_uid_expunge(input: &str) -> IResult<&str, crate::command::UidSubcommand> {
747 use crate::command::UidSubcommand;
748
749 let (input, _) = tag_no_case("EXPUNGE").parse(input)?;
750 let (input, _) = space1(input)?;
751 let (input, sequence) = nom::combinator::rest(input)?;
752
753 Ok((
754 input,
755 UidSubcommand::Expunge {
756 sequence: sequence.trim().to_string(),
757 },
758 ))
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764
765 #[test]
766 fn test_parse_login() {
767 let (tag, cmd) =
768 parse_command("A001 LOGIN user password").expect("LOGIN command parse should succeed");
769 assert_eq!(tag, "A001");
770 match cmd {
771 ImapCommand::Login { user, password } => {
772 assert_eq!(user, "user");
773 assert_eq!(password, "password");
774 }
775 _ => panic!("Expected Login command"),
776 }
777 }
778
779 #[test]
780 fn test_parse_select() {
781 let (tag, cmd) =
782 parse_command("A002 SELECT INBOX").expect("SELECT INBOX parse should succeed");
783 assert_eq!(tag, "A002");
784 match cmd {
785 ImapCommand::Select { mailbox } => {
786 assert_eq!(mailbox, "INBOX");
787 }
788 _ => panic!("Expected Select command"),
789 }
790 }
791
792 #[test]
793 fn test_parse_logout() {
794 let (tag, cmd) = parse_command("A003 LOGOUT").expect("LOGOUT parse should succeed");
795 assert_eq!(tag, "A003");
796 assert!(matches!(cmd, ImapCommand::Logout));
797 }
798
799 #[test]
801 fn test_has_literal_synchronizing() {
802 let result = has_literal("A001 APPEND INBOX {100}");
804 assert_eq!(result, Some((100, LiteralType::Synchronizing)));
805 }
806
807 #[test]
808 fn test_has_literal_non_synchronizing() {
809 let result = has_literal("A001 APPEND INBOX {100+}");
811 assert_eq!(result, Some((100, LiteralType::NonSynchronizing)));
812 }
813
814 #[test]
815 fn test_has_literal_with_flags_synchronizing() {
816 let result = has_literal("A001 APPEND INBOX (\\Seen \\Draft) {250}");
817 assert_eq!(result, Some((250, LiteralType::Synchronizing)));
818 }
819
820 #[test]
821 fn test_has_literal_with_flags_non_synchronizing() {
822 let result = has_literal("A001 APPEND INBOX (\\Seen \\Draft) {250+}");
823 assert_eq!(result, Some((250, LiteralType::NonSynchronizing)));
824 }
825
826 #[test]
827 fn test_has_literal_with_date_synchronizing() {
828 let result = has_literal("A001 APPEND INBOX \"7-Feb-1994 21:52:25 -0800\" {1024}");
829 assert_eq!(result, Some((1024, LiteralType::Synchronizing)));
830 }
831
832 #[test]
833 fn test_has_literal_with_date_non_synchronizing() {
834 let result = has_literal("A001 APPEND INBOX \"7-Feb-1994 21:52:25 -0800\" {1024+}");
835 assert_eq!(result, Some((1024, LiteralType::NonSynchronizing)));
836 }
837
838 #[test]
839 fn test_has_literal_complete_append_synchronizing() {
840 let result = has_literal("A001 APPEND INBOX (\\Seen) \"7-Feb-1994 21:52:25 -0800\" {5000}");
841 assert_eq!(result, Some((5000, LiteralType::Synchronizing)));
842 }
843
844 #[test]
845 fn test_has_literal_complete_append_non_synchronizing() {
846 let result =
847 has_literal("A001 APPEND INBOX (\\Seen) \"7-Feb-1994 21:52:25 -0800\" {5000+}");
848 assert_eq!(result, Some((5000, LiteralType::NonSynchronizing)));
849 }
850
851 #[test]
852 fn test_has_literal_no_literal() {
853 let result = has_literal("A001 SELECT INBOX");
855 assert_eq!(result, None);
856 }
857
858 #[test]
859 fn test_has_literal_invalid_format() {
860 let result = has_literal("A001 APPEND INBOX {abc}");
862 assert_eq!(result, None);
863 }
864
865 #[test]
866 fn test_has_literal_invalid_format_plus() {
867 let result = has_literal("A001 APPEND INBOX {abc+}");
869 assert_eq!(result, None);
870 }
871
872 #[test]
873 fn test_has_literal_zero_size_synchronizing() {
874 let result = has_literal("A001 APPEND INBOX {0}");
876 assert_eq!(result, Some((0, LiteralType::Synchronizing)));
877 }
878
879 #[test]
880 fn test_has_literal_zero_size_non_synchronizing() {
881 let result = has_literal("A001 APPEND INBOX {0+}");
883 assert_eq!(result, Some((0, LiteralType::NonSynchronizing)));
884 }
885
886 #[test]
887 fn test_has_literal_large_size_synchronizing() {
888 let result = has_literal("A001 APPEND INBOX {999999999}");
890 assert_eq!(result, Some((999999999, LiteralType::Synchronizing)));
891 }
892
893 #[test]
894 fn test_has_literal_large_size_non_synchronizing() {
895 let result = has_literal("A001 APPEND INBOX {999999999+}");
897 assert_eq!(result, Some((999999999, LiteralType::NonSynchronizing)));
898 }
899
900 #[test]
901 fn test_has_literal_unclosed_brace() {
902 let result = has_literal("A001 APPEND INBOX {100");
904 assert_eq!(result, None);
905 }
906
907 #[test]
908 fn test_has_literal_multiple_literals_takes_last() {
909 let result = has_literal("A001 {50} APPEND {100}");
911 assert_eq!(result, Some((100, LiteralType::Synchronizing)));
912 }
913
914 #[test]
915 fn test_has_literal_empty_braces() {
916 let result = has_literal("A001 APPEND INBOX {}");
918 assert_eq!(result, None);
919 }
920
921 #[test]
922 fn test_has_literal_plus_only() {
923 let result = has_literal("A001 APPEND INBOX {+}");
925 assert_eq!(result, None);
926 }
927
928 #[test]
929 fn test_get_literal_size_synchronizing() {
930 let result = get_literal_size("A001 APPEND INBOX {100}");
932 assert_eq!(result, Some(100));
933 }
934
935 #[test]
936 fn test_get_literal_size_non_synchronizing() {
937 let result = get_literal_size("A001 APPEND INBOX {100+}");
939 assert_eq!(result, Some(100));
940 }
941
942 #[test]
944 fn test_has_literal_mixed_case_append() {
945 let result = has_literal("a001 append inbox {50+}");
947 assert_eq!(result, Some((50, LiteralType::NonSynchronizing)));
948 }
949
950 #[test]
951 fn test_has_literal_with_special_chars_in_mailbox_name() {
952 let result = has_literal("A001 APPEND \"Sent Items\" {128+}");
954 assert_eq!(result, Some((128, LiteralType::NonSynchronizing)));
955 }
956
957 #[test]
958 fn test_has_literal_all_flags_sync() {
959 let result = has_literal("A001 APPEND INBOX (\\Seen \\Flagged \\Draft \\Answered) {1000}");
961 assert_eq!(result, Some((1000, LiteralType::Synchronizing)));
962 }
963
964 #[test]
965 fn test_has_literal_all_flags_non_sync() {
966 let result = has_literal("A001 APPEND INBOX (\\Seen \\Flagged \\Draft \\Answered) {1000+}");
968 assert_eq!(result, Some((1000, LiteralType::NonSynchronizing)));
969 }
970
971 #[test]
972 fn test_has_literal_whitespace_before_brace_sync() {
973 let result = has_literal("A001 APPEND INBOX {200}");
975 assert_eq!(result, Some((200, LiteralType::Synchronizing)));
976 }
977
978 #[test]
979 fn test_has_literal_whitespace_before_brace_non_sync() {
980 let result = has_literal("A001 APPEND INBOX {200+}");
982 assert_eq!(result, Some((200, LiteralType::NonSynchronizing)));
983 }
984
985 #[test]
986 fn test_has_literal_only_braces_no_command() {
987 let result = has_literal("{500}");
989 assert_eq!(result, Some((500, LiteralType::Synchronizing)));
990 }
991
992 #[test]
993 fn test_has_literal_only_braces_plus_no_command() {
994 let result = has_literal("{500+}");
996 assert_eq!(result, Some((500, LiteralType::NonSynchronizing)));
997 }
998
999 #[test]
1000 fn test_has_literal_date_time_rfc2822_sync() {
1001 let result = has_literal("A001 APPEND INBOX \"07-Feb-1994 21:52:25 -0800\" {2048}");
1003 assert_eq!(result, Some((2048, LiteralType::Synchronizing)));
1004 }
1005
1006 #[test]
1007 fn test_has_literal_date_time_rfc2822_non_sync() {
1008 let result = has_literal("A001 APPEND INBOX \"07-Feb-1994 21:52:25 -0800\" {2048+}");
1010 assert_eq!(result, Some((2048, LiteralType::NonSynchronizing)));
1011 }
1012
1013 #[test]
1014 fn test_has_literal_complete_with_custom_flags_sync() {
1015 let result = has_literal(
1017 "A001 APPEND INBOX (\\Seen $Important) \"01-Jan-2024 12:00:00 +0000\" {4096}",
1018 );
1019 assert_eq!(result, Some((4096, LiteralType::Synchronizing)));
1020 }
1021
1022 #[test]
1023 fn test_has_literal_complete_with_custom_flags_non_sync() {
1024 let result = has_literal(
1026 "A001 APPEND INBOX (\\Seen $Important) \"01-Jan-2024 12:00:00 +0000\" {4096+}",
1027 );
1028 assert_eq!(result, Some((4096, LiteralType::NonSynchronizing)));
1029 }
1030
1031 #[test]
1032 fn test_has_literal_single_digit_sync() {
1033 let result = has_literal("A001 APPEND INBOX {5}");
1035 assert_eq!(result, Some((5, LiteralType::Synchronizing)));
1036 }
1037
1038 #[test]
1039 fn test_has_literal_single_digit_non_sync() {
1040 let result = has_literal("A001 APPEND INBOX {5+}");
1042 assert_eq!(result, Some((5, LiteralType::NonSynchronizing)));
1043 }
1044
1045 #[test]
1046 fn test_has_literal_double_plus() {
1047 let result = has_literal("A001 APPEND INBOX {100++}");
1049 assert_eq!(result, None);
1050 }
1051
1052 #[test]
1053 fn test_has_literal_plus_at_start() {
1054 let result = has_literal("A001 APPEND INBOX {+100}");
1057 assert_eq!(result, None);
1058 }
1059
1060 #[test]
1061 fn test_has_literal_minus_sign() {
1062 let result = has_literal("A001 APPEND INBOX {-100}");
1064 assert_eq!(result, None);
1065 }
1066
1067 #[test]
1068 fn test_has_literal_with_spaces_inside() {
1069 let result = has_literal("A001 APPEND INBOX {100 }");
1071 assert_eq!(result, None);
1072 }
1073
1074 #[test]
1075 fn test_has_literal_scientific_notation() {
1076 let result = has_literal("A001 APPEND INBOX {1e5}");
1078 assert_eq!(result, None);
1079 }
1080
1081 #[test]
1082 fn test_has_literal_hexadecimal() {
1083 let result = has_literal("A001 APPEND INBOX {0x100}");
1085 assert_eq!(result, None);
1086 }
1087
1088 #[test]
1089 fn test_parse_append_args_basic() {
1090 let (mailbox, flags, date_time) =
1092 parse_append_args("INBOX {100}").expect("basic APPEND args parse should succeed");
1093 assert_eq!(mailbox, "INBOX");
1094 assert!(flags.is_empty());
1095 assert_eq!(date_time, None);
1096 }
1097
1098 #[test]
1099 fn test_parse_append_args_with_flags() {
1100 let (mailbox, flags, date_time) = parse_append_args("INBOX (\\Seen \\Draft) {100}")
1102 .expect("APPEND args with flags parse should succeed");
1103 assert_eq!(mailbox, "INBOX");
1104 assert_eq!(flags, vec!["\\Seen", "\\Draft"]);
1105 assert_eq!(date_time, None);
1106 }
1107
1108 #[test]
1109 fn test_parse_append_args_with_date() {
1110 let (mailbox, flags, date_time) =
1112 parse_append_args("INBOX \"7-Feb-1994 21:52:25 -0800\" {100}")
1113 .expect("APPEND args with date-time parse should succeed");
1114 assert_eq!(mailbox, "INBOX");
1115 assert!(flags.is_empty());
1116 assert_eq!(date_time, Some("7-Feb-1994 21:52:25 -0800".to_string()));
1117 }
1118
1119 #[test]
1120 fn test_parse_append_args_complete() {
1121 let (mailbox, flags, date_time) =
1123 parse_append_args("INBOX (\\Seen) \"7-Feb-1994 21:52:25 -0800\" {100}")
1124 .expect("complete APPEND args parse should succeed");
1125 assert_eq!(mailbox, "INBOX");
1126 assert_eq!(flags, vec!["\\Seen"]);
1127 assert_eq!(date_time, Some("7-Feb-1994 21:52:25 -0800".to_string()));
1128 }
1129
1130 #[test]
1131 fn test_parse_append_args_quoted_mailbox() {
1132 let (mailbox, flags, date_time) = parse_append_args("\"Sent Items\" {100}")
1134 .expect("APPEND args with quoted mailbox name parse should succeed");
1135 assert_eq!(mailbox, "Sent Items");
1136 assert!(flags.is_empty());
1137 assert_eq!(date_time, None);
1138 }
1139
1140 #[test]
1141 fn test_parse_append_command_basic() {
1142 let literal_data = b"Subject: Test\r\n\r\nHello World".to_vec();
1144 let (tag, cmd) = parse_append_command("A001 APPEND INBOX {30}", literal_data.clone())
1145 .expect("basic APPEND command parse should succeed");
1146 assert_eq!(tag, "A001");
1147 match cmd {
1148 ImapCommand::Append {
1149 mailbox,
1150 flags,
1151 date_time,
1152 message_literal,
1153 } => {
1154 assert_eq!(mailbox, "INBOX");
1155 assert!(flags.is_empty());
1156 assert_eq!(date_time, None);
1157 assert_eq!(message_literal, literal_data);
1158 }
1159 _ => panic!("Expected Append command"),
1160 }
1161 }
1162
1163 #[test]
1164 fn test_parse_append_command_with_flags() {
1165 let literal_data = b"Subject: Test\r\n\r\nHello".to_vec();
1167 let (tag, cmd) = parse_append_command(
1168 "A002 APPEND INBOX (\\Seen \\Flagged) {25}",
1169 literal_data.clone(),
1170 )
1171 .expect("APPEND command with flags parse should succeed");
1172 assert_eq!(tag, "A002");
1173 match cmd {
1174 ImapCommand::Append {
1175 mailbox,
1176 flags,
1177 date_time,
1178 message_literal,
1179 } => {
1180 assert_eq!(mailbox, "INBOX");
1181 assert_eq!(flags, vec!["\\Seen", "\\Flagged"]);
1182 assert_eq!(date_time, None);
1183 assert_eq!(message_literal, literal_data);
1184 }
1185 _ => panic!("Expected Append command"),
1186 }
1187 }
1188
1189 #[test]
1190 fn test_parse_append_command_complete() {
1191 let literal_data = b"Subject: Test\r\n\r\nTest message".to_vec();
1193 let (tag, cmd) = parse_append_command(
1194 "A003 APPEND INBOX (\\Seen) \"15-Feb-2026 10:30:00 +0000\" {32}",
1195 literal_data.clone(),
1196 )
1197 .expect("complete APPEND command parse should succeed");
1198 assert_eq!(tag, "A003");
1199 match cmd {
1200 ImapCommand::Append {
1201 mailbox,
1202 flags,
1203 date_time,
1204 message_literal,
1205 } => {
1206 assert_eq!(mailbox, "INBOX");
1207 assert_eq!(flags, vec!["\\Seen"]);
1208 assert_eq!(date_time, Some("15-Feb-2026 10:30:00 +0000".to_string()));
1209 assert_eq!(message_literal, literal_data);
1210 }
1211 _ => panic!("Expected Append command"),
1212 }
1213 }
1214
1215 #[test]
1216 fn test_literal_type_equality() {
1217 assert_eq!(LiteralType::Synchronizing, LiteralType::Synchronizing);
1219 assert_eq!(LiteralType::NonSynchronizing, LiteralType::NonSynchronizing);
1220 assert_ne!(LiteralType::Synchronizing, LiteralType::NonSynchronizing);
1221 }
1222
1223 #[test]
1224 fn test_literal_type_clone() {
1225 let sync = LiteralType::Synchronizing;
1227 let sync_clone = sync;
1228 assert_eq!(sync, sync_clone);
1229 }
1230}