Skip to main content

rusmes_imap/
parser.rs

1//! IMAP command parser
2//!
3//! This module provides parsing functionality for IMAP commands, including support
4//! for RFC 7888 LITERAL+ extension for non-synchronizing literals.
5//!
6//! # LITERAL+ Extension (RFC 7888)
7//!
8//! The LITERAL+ extension allows clients to send literal data without waiting for
9//! server continuation. This significantly improves APPEND performance by reducing
10//! round-trips.
11//!
12//! ## Traditional Synchronizing Literals
13//!
14//! ```text
15//! C: A001 APPEND INBOX {310}
16//! S: + Ready for literal data
17//! C: <310 bytes of message data>
18//! S: A001 OK APPEND completed
19//! ```
20//!
21//! ## Non-Synchronizing Literals (LITERAL+)
22//!
23//! ```text
24//! C: A001 APPEND INBOX {310+}
25//! C: <310 bytes of message data>
26//! S: A001 OK APPEND completed
27//! ```
28//!
29//! The parser automatically detects both literal types:
30//! - `{size}` - Synchronizing literal (requires server continuation)
31//! - `{size+}` - Non-synchronizing literal (no continuation needed)
32
33use 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/// Type of literal in IMAP command
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum LiteralType {
44    /// Synchronizing literal {size} - requires server continuation
45    Synchronizing,
46    /// Non-synchronizing literal {size+} (RFC 7888) - no server continuation needed
47    NonSynchronizing,
48}
49
50/// Parse an IMAP command
51pub fn parse_command(input: &str) -> Result<(String, ImapCommand), String> {
52    // IMAP commands are: tag COMMAND args
53    // Example: A001 LOGIN user password
54
55    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
77/// Parse APPEND command with literal data
78/// This is separate because it needs access to the full input including literal data
79pub fn parse_append_command(
80    input: &str,
81    literal_data: Vec<u8>,
82) -> Result<(String, ImapCommand), String> {
83    // Format: tag APPEND mailbox [flags] [date-time] {size}
84    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    // Parse the rest after APPEND
92    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
106/// Parse APPEND arguments (mailbox, optional flags, optional date-time)
107fn 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                // Start of literal, stop here
135                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    // First part is always the mailbox name
153    let mailbox = parts[0].trim_matches('"').to_string();
154    let mut flags = Vec::new();
155    let mut date_time = None;
156
157    // Parse remaining optional parts
158    let mut i = 1;
159    while i < parts.len() {
160        let part = &parts[i];
161        if part.starts_with('(') && part.ends_with(')') {
162            // Flags list
163            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 string
170            date_time = Some(part.trim_matches('"').to_string());
171        }
172        i += 1;
173    }
174
175    Ok((mailbox, flags, date_time))
176}
177
178/// Check if command line contains a literal and return its size and type
179///
180/// Returns `Some((size, LiteralType))` if a literal is found:
181/// - {size} -> Synchronizing literal (traditional, requires server continuation)
182/// - {size+} -> Non-synchronizing literal (RFC 7888 LITERAL+, no continuation needed)
183///
184/// # Examples
185/// ```
186/// use rusmes_imap::parser::{has_literal, LiteralType};
187///
188/// let sync = has_literal("A001 APPEND INBOX {100}");
189/// assert_eq!(sync, Some((100, LiteralType::Synchronizing)));
190///
191/// let non_sync = has_literal("A001 APPEND INBOX {100+}");
192/// assert_eq!(non_sync, Some((100, LiteralType::NonSynchronizing)));
193/// ```
194pub fn has_literal(input: &str) -> Option<(usize, LiteralType)> {
195    // Look for {size} or {size+} pattern at the end of the line
196    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            // Reject if empty or starts with invalid characters
201            if size_str.is_empty() || size_str.starts_with('+') || size_str.starts_with('-') {
202                return None;
203            }
204
205            // Check for non-synchronizing literal (ends with +)
206            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                // Traditional synchronizing literal
212                if let Ok(size) = size_str.parse::<usize>() {
213                    return Some((size, LiteralType::Synchronizing));
214                }
215            }
216        }
217    }
218    None
219}
220
221/// Legacy function for backward compatibility
222/// Returns just the size of the literal, ignoring the type
223#[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    // Split into two groups to work around nom's 21-alternative limit
230    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    // Check for optional initial response (SASL-IR, RFC 4959)
302    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    // Parse items (simplified)
353    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    // Determine store mode
461    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        // Default to replace
469        StoreMode::Replace
470    };
471
472    // Parse flags - rest of the line
473    let (input, _) = space1(input)?;
474    let (input, flags_str) = nom::combinator::rest(input)?;
475
476    // Parse flags (simplified - handle parenthesized list or single flag)
477    let flags_str = flags_str.trim();
478    let flags: Vec<String> = if flags_str.starts_with('(') && flags_str.ends_with(')') {
479        // Parenthesized list
480        flags_str[1..flags_str.len() - 1]
481            .split_whitespace()
482            .map(|s| s.to_string())
483            .collect()
484    } else {
485        // Single flag
486        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    // Parse search criteria (simplified - just split by whitespace for now)
505    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    // Parse the subcommand
607    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    // Parse items (simplified)
633    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    // Determine store mode
657    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    // Parse flags - rest of the line
668    let (input, _) = space1(input)?;
669    let (input, flags_str) = nom::combinator::rest(input)?;
670
671    // Parse flags (simplified - handle parenthesized list or single flag)
672    let flags_str = flags_str.trim();
673    let flags: Vec<String> = if flags_str.starts_with('(') && flags_str.ends_with(')') {
674        // Parenthesized list
675        flags_str[1..flags_str.len() - 1]
676            .split_whitespace()
677            .map(|s| s.to_string())
678            .collect()
679    } else {
680        // Single flag
681        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    // Parse search criteria (simplified - just split by whitespace for now)
702    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    // LITERAL+ (RFC 7888) Tests
800    #[test]
801    fn test_has_literal_synchronizing() {
802        // Traditional synchronizing literal {size}
803        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        // Non-synchronizing literal {size+} - RFC 7888
810        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        // No literal in command
854        let result = has_literal("A001 SELECT INBOX");
855        assert_eq!(result, None);
856    }
857
858    #[test]
859    fn test_has_literal_invalid_format() {
860        // Invalid literal format
861        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        // Invalid literal format with plus
868        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        // Edge case: zero-size literal
875        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        // Edge case: zero-size non-synchronizing literal
882        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        // Large literal size
889        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        // Large non-synchronizing literal size
896        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        // Unclosed brace
903        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        // If multiple literal patterns exist, rfind ensures we get the last one
910        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        // Empty braces
917        let result = has_literal("A001 APPEND INBOX {}");
918        assert_eq!(result, None);
919    }
920
921    #[test]
922    fn test_has_literal_plus_only() {
923        // Just a plus sign
924        let result = has_literal("A001 APPEND INBOX {+}");
925        assert_eq!(result, None);
926    }
927
928    #[test]
929    fn test_get_literal_size_synchronizing() {
930        // Legacy function test - synchronizing
931        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        // Legacy function test - non-synchronizing
938        let result = get_literal_size("A001 APPEND INBOX {100+}");
939        assert_eq!(result, Some(100));
940    }
941
942    // Additional comprehensive LITERAL+ tests
943    #[test]
944    fn test_has_literal_mixed_case_append() {
945        // Test case insensitivity
946        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        // Mailbox with special characters
953        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        // Multiple flags with synchronizing literal
960        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        // Multiple flags with non-synchronizing literal
967        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        // Extra whitespace before literal
974        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        // Extra whitespace before literal with LITERAL+
981        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        // Just braces with size
988        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        // Just braces with size and plus
995        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        // RFC 2822 style date-time with synchronizing literal
1002        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        // RFC 2822 style date-time with non-synchronizing literal
1009        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        // Custom flags with synchronizing literal
1016        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        // Custom flags with non-synchronizing literal
1025        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        // Single digit size - synchronizing
1034        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        // Single digit size - non-synchronizing
1041        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        // Invalid: double plus
1048        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        // Plus at start is invalid - explicitly rejected by the parser
1055        // IMAP spec requires literal size to be a non-negative decimal number
1056        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        // Invalid: negative size
1063        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        // Invalid: spaces inside braces
1070        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        // Invalid: scientific notation
1077        let result = has_literal("A001 APPEND INBOX {1e5}");
1078        assert_eq!(result, None);
1079    }
1080
1081    #[test]
1082    fn test_has_literal_hexadecimal() {
1083        // Invalid: hexadecimal
1084        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        // Basic APPEND with just mailbox
1091        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        // APPEND with flags
1101        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        // APPEND with date-time
1111        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        // APPEND with flags and date-time
1122        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        // APPEND with quoted mailbox name
1133        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        // Basic APPEND command parsing
1143        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        // APPEND command with flags
1166        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        // Complete APPEND command with all parameters
1192        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        // Test LiteralType enum equality
1218        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        // Test LiteralType clone
1226        let sync = LiteralType::Synchronizing;
1227        let sync_clone = sync;
1228        assert_eq!(sync, sync_clone);
1229    }
1230}