Skip to main content

imap_core/
parser.rs

1//! Recursive-descent parser for IMAP4rev1 / IMAP4rev2 server responses.
2//!
3//! The parser operates on a byte slice and returns borrowed AST nodes
4//! pointing back into that slice (zero-copy). It is deliberately tolerant
5//! of trailing garbage on a line: anything after a recognized response that
6//! still ends in CRLF is consumed and the response is returned. This keeps
7//! the framer robust against minor server quirks.
8//!
9//! ## DoS guard
10//!
11//! Literals (`{n}\r\n`) are capped at [`MAX_LITERAL_SIZE`] to prevent a
12//! hostile server from forcing the client to allocate an arbitrarily large
13//! buffer. The cap applies to a single literal; pipelined responses are
14//! independent.
15//!
16//! ## Limitations
17//!
18//! Quoted-string escape sequences (`\\`, `\"`) are not unescaped — the raw
19//! bytes are returned. ENVELOPE / BODY / BODYSTRUCTURE are returned as raw
20//! parenthesized byte slices for callers to parse on demand.
21
22use crate::ast::{
23    ContinueReq, DataResponse, FetchAttribute, Response, ResponseCode, Status, StatusItem,
24    StatusResponse,
25};
26use crate::error::ParseError;
27
28/// Hard cap on a single literal's declared length, in octets.
29/// A server attempting to send a larger literal causes the parser to fail
30/// with [`ParseError::LiteralTooLarge`] before any buffer growth occurs.
31pub const MAX_LITERAL_SIZE: usize = 64 * 1024 * 1024;
32
33/// Result returned by the parser API.
34pub type IResult<'a, T> = Result<(&'a [u8], T), ParseError>;
35
36/// Parse a single IMAP server response from `input`.
37///
38/// Returns the unparsed remainder and the parsed [`Response`]. If `input`
39/// does not yet contain a complete response, returns
40/// [`ParseError::Incomplete`].
41pub fn parse_response(input: &[u8]) -> IResult<'_, Response<'_>> {
42    let mut parser = Parser::new(input);
43    let response = parser.parse_response()?;
44    Ok((parser.remaining(), response))
45}
46
47// ---------------------------------------------------------------------------
48// Internal parser
49// ---------------------------------------------------------------------------
50
51struct Parser<'a> {
52    input: &'a [u8],
53    pos: usize,
54}
55
56impl<'a> Parser<'a> {
57    fn new(input: &'a [u8]) -> Self {
58        Self { input, pos: 0 }
59    }
60
61    fn remaining(&self) -> &'a [u8] {
62        &self.input[self.pos..]
63    }
64
65    fn is_eof(&self) -> bool {
66        self.pos >= self.input.len()
67    }
68
69    fn peek(&self) -> Result<u8, ParseError> {
70        self.input
71            .get(self.pos)
72            .copied()
73            .ok_or(ParseError::Incomplete)
74    }
75
76    fn peek_at(&self, offset: usize) -> Result<u8, ParseError> {
77        let idx = self.pos.checked_add(offset).ok_or(ParseError::Incomplete)?;
78        self.input.get(idx).copied().ok_or(ParseError::Incomplete)
79    }
80
81    fn consume_byte(&mut self, b: u8) -> Result<(), ParseError> {
82        if self.peek()? == b {
83            self.pos += 1;
84            Ok(())
85        } else {
86            Err(ParseError::InvalidChar(self.pos))
87        }
88    }
89
90    fn try_consume_byte(&mut self, b: u8) -> bool {
91        match self.peek() {
92            Ok(c) if c == b => {
93                self.pos += 1;
94                true
95            }
96            _ => false,
97        }
98    }
99
100    /// Match `expected` case-insensitively at the current position. Does
101    /// not advance.
102    fn matches_ci(&self, expected: &[u8]) -> bool {
103        let end = match self.pos.checked_add(expected.len()) {
104            Some(e) if e <= self.input.len() => e,
105            _ => return false,
106        };
107        self.input[self.pos..end]
108            .iter()
109            .zip(expected.iter())
110            .all(|(a, b)| a.eq_ignore_ascii_case(b))
111    }
112
113    /// Consume `expected` case-insensitively iff the byte after the match
114    /// is a token boundary (SP, CR, LF, `]`, `)`, or EOF). This prevents
115    /// `OK` from spuriously matching the prefix of `OKAY`.
116    fn try_consume_keyword_ci(&mut self, expected: &[u8]) -> bool {
117        if !self.matches_ci(expected) {
118            return false;
119        }
120        let next = self.input.get(self.pos + expected.len()).copied();
121        let is_boundary = match next {
122            None => true,
123            Some(b) => matches!(b, b' ' | b'\r' | b'\n' | b']' | b')'),
124        };
125        if !is_boundary {
126            return false;
127        }
128        self.pos += expected.len();
129        true
130    }
131
132    fn consume_crlf(&mut self) -> Result<(), ParseError> {
133        if self.peek()? != b'\r' {
134            return Err(ParseError::Malformed("expected CR"));
135        }
136        match self.peek_at(1) {
137            Ok(b'\n') => {
138                self.pos += 2;
139                Ok(())
140            }
141            Ok(_) => Err(ParseError::Malformed("expected LF after CR")),
142            Err(_) => Err(ParseError::Incomplete),
143        }
144    }
145
146    fn consume_sp(&mut self) -> Result<(), ParseError> {
147        self.consume_byte(b' ')
148    }
149
150    /// Decimal number — `1*DIGIT`, returned as `u32`.
151    fn parse_number(&mut self) -> Result<u32, ParseError> {
152        let start = self.pos;
153        while let Ok(c) = self.peek() {
154            if c.is_ascii_digit() {
155                self.pos += 1;
156            } else {
157                break;
158            }
159        }
160        if start == self.pos {
161            // No digits consumed — could be EOF or a non-digit.
162            if self.is_eof() {
163                return Err(ParseError::Incomplete);
164            }
165            return Err(ParseError::InvalidNumber);
166        }
167        let bytes = &self.input[start..self.pos];
168        // Bytes are ASCII digits — UTF-8 conversion is infallible.
169        let s = match core::str::from_utf8(bytes) {
170            Ok(s) => s,
171            Err(_) => return Err(ParseError::InvalidNumber),
172        };
173        s.parse::<u32>().map_err(|_| ParseError::InvalidNumber)
174    }
175
176    /// Atom — one or more ATOM-CHARs (RFC 9051): any CHAR except atom-specials
177    /// (`(`, `)`, `{`, ` `, CTL, list-wildcards `%` `*`, quoted-specials `"` `\`,
178    /// resp-specials `]`).
179    fn parse_atom(&mut self) -> Result<&'a str, ParseError> {
180        let start = self.pos;
181        while let Ok(c) = self.peek() {
182            if is_atom_char(c) {
183                self.pos += 1;
184            } else {
185                break;
186            }
187        }
188        if start == self.pos {
189            if self.is_eof() {
190                return Err(ParseError::Incomplete);
191            }
192            return Err(ParseError::Malformed("expected atom"));
193        }
194        let bytes = &self.input[start..self.pos];
195        core::str::from_utf8(bytes).map_err(|_| ParseError::Other("Invalid UTF-8"))
196    }
197
198    /// Tag — like atom but `+` is also allowed-after-first-position; in
199    /// practice servers echo whatever client sent. We accept any non-SP,
200    /// non-CR, non-LF, non-`+` first char and the same body.
201    fn parse_tag(&mut self) -> Result<&'a str, ParseError> {
202        let start = self.pos;
203        while let Ok(c) = self.peek() {
204            if c == b' ' || c == b'\r' || c == b'\n' {
205                break;
206            }
207            self.pos += 1;
208        }
209        if start == self.pos {
210            if self.is_eof() {
211                return Err(ParseError::Incomplete);
212            }
213            return Err(ParseError::Malformed("expected tag"));
214        }
215        core::str::from_utf8(&self.input[start..self.pos])
216            .map_err(|_| ParseError::Other("Invalid UTF-8"))
217    }
218
219    /// Quoted string: `"..."`. Returns the byte content between the quotes.
220    /// Escape sequences (`\\`, `\"`) are NOT processed.
221    fn parse_quoted(&mut self) -> Result<&'a str, ParseError> {
222        self.consume_byte(b'"')?;
223        let start = self.pos;
224        loop {
225            let c = self.peek()?;
226            match c {
227                b'"' => {
228                    let s = core::str::from_utf8(&self.input[start..self.pos])
229                        .map_err(|_| ParseError::Other("Invalid UTF-8"))?;
230                    self.pos += 1;
231                    return Ok(s);
232                }
233                b'\\' => {
234                    // Skip the escape and the next char (must be `"` or `\`).
235                    self.pos += 1;
236                    let _escaped = self.peek()?;
237                    self.pos += 1;
238                }
239                b'\r' | b'\n' => {
240                    return Err(ParseError::Malformed("CR/LF inside quoted string"));
241                }
242                _ => self.pos += 1,
243            }
244        }
245    }
246
247    /// Literal: `{n}\r\n<n octets>`. Returns the n octets as a byte slice.
248    fn parse_literal(&mut self) -> Result<&'a [u8], ParseError> {
249        self.consume_byte(b'{')?;
250        let n = self.parse_number()? as usize;
251        self.consume_byte(b'}')?;
252        self.consume_crlf()?;
253        if n > MAX_LITERAL_SIZE {
254            return Err(ParseError::LiteralTooLarge {
255                got: n,
256                max: MAX_LITERAL_SIZE,
257            });
258        }
259        let end = self.pos.checked_add(n).ok_or(ParseError::Incomplete)?;
260        if end > self.input.len() {
261            return Err(ParseError::Incomplete);
262        }
263        let data = &self.input[self.pos..end];
264        self.pos = end;
265        Ok(data)
266    }
267
268    /// `astring` = atom / string. `string` = quoted / literal. We coerce
269    /// literals to `&str` via UTF-8 validation; an invalid-UTF-8 literal in
270    /// an astring context returns `Invalid UTF-8`.
271    fn parse_astring(&mut self) -> Result<&'a str, ParseError> {
272        match self.peek()? {
273            b'"' => self.parse_quoted(),
274            b'{' => {
275                let bytes = self.parse_literal()?;
276                core::str::from_utf8(bytes).map_err(|_| ParseError::Other("Invalid UTF-8"))
277            }
278            _ => self.parse_atom(),
279        }
280    }
281
282    /// `nstring` = string / NIL. Returns `None` for NIL.
283    fn parse_nstring(&mut self) -> Result<Option<&'a str>, ParseError> {
284        match self.peek()? {
285            b'"' => Ok(Some(self.parse_quoted()?)),
286            b'{' => {
287                let bytes = self.parse_literal()?;
288                let s =
289                    core::str::from_utf8(bytes).map_err(|_| ParseError::Other("Invalid UTF-8"))?;
290                Ok(Some(s))
291            }
292            _ => {
293                if self.try_consume_keyword_ci(b"NIL") {
294                    Ok(None)
295                } else {
296                    Err(ParseError::Malformed("expected nstring"))
297                }
298            }
299        }
300    }
301
302    /// Parse a parenthesized atom list, e.g. `(\Seen \Draft)`.
303    fn parse_paren_atom_list(&mut self) -> Result<Vec<&'a str>, ParseError> {
304        self.consume_byte(b'(')?;
305        let mut items = Vec::new();
306        if self.try_consume_byte(b')') {
307            return Ok(items);
308        }
309        loop {
310            items.push(self.parse_flag_or_atom()?);
311            match self.peek()? {
312                b' ' => {
313                    self.pos += 1;
314                }
315                b')' => {
316                    self.pos += 1;
317                    return Ok(items);
318                }
319                _ => return Err(ParseError::Malformed("expected SP or ) in list")),
320            }
321        }
322    }
323
324    /// A flag is `\<atom>` or just `<atom>` (keyword). For our purposes we
325    /// accept either and return the byte string verbatim.
326    fn parse_flag_or_atom(&mut self) -> Result<&'a str, ParseError> {
327        let start = self.pos;
328        if self.peek()? == b'\\' {
329            self.pos += 1;
330            // `\*` is the special "all keywords" marker permitted in PERMANENTFLAGS.
331            if self.try_consume_byte(b'*') {
332                return core::str::from_utf8(&self.input[start..self.pos])
333                    .map_err(|_| ParseError::Other("Invalid UTF-8"));
334            }
335        }
336        while let Ok(c) = self.peek() {
337            if is_atom_char(c) {
338                self.pos += 1;
339            } else {
340                break;
341            }
342        }
343        if start == self.pos {
344            return Err(ParseError::Malformed("expected flag/atom"));
345        }
346        core::str::from_utf8(&self.input[start..self.pos])
347            .map_err(|_| ParseError::Other("Invalid UTF-8"))
348    }
349
350    /// Parse a balanced parenthesized expression starting at the current
351    /// `(`. Returns the slice including the outer parens. Recognizes literals
352    /// inside so they aren't accidentally split. Used to capture ENVELOPE /
353    /// BODY / BODYSTRUCTURE for downstream parsing.
354    fn parse_balanced_parens(&mut self) -> Result<&'a [u8], ParseError> {
355        let start = self.pos;
356        self.consume_byte(b'(')?;
357        let mut depth: u32 = 1;
358        while depth > 0 {
359            match self.peek()? {
360                b'(' => {
361                    self.pos += 1;
362                    depth = depth
363                        .checked_add(1)
364                        .ok_or(ParseError::Other("paren depth overflow"))?;
365                }
366                b')' => {
367                    self.pos += 1;
368                    depth -= 1;
369                }
370                b'"' => {
371                    self.parse_quoted()?;
372                }
373                b'{' => {
374                    self.parse_literal()?;
375                }
376                _ => self.pos += 1,
377            }
378        }
379        Ok(&self.input[start..self.pos])
380    }
381
382    /// Read the rest of the current line into a `&str`, stopping at CR. Does
383    /// not consume the CRLF.
384    fn parse_text(&mut self) -> Result<&'a str, ParseError> {
385        let start = self.pos;
386        while let Ok(c) = self.peek() {
387            if c == b'\r' || c == b'\n' {
388                break;
389            }
390            self.pos += 1;
391        }
392        core::str::from_utf8(&self.input[start..self.pos])
393            .map_err(|_| ParseError::Other("Invalid UTF-8"))
394    }
395
396    // -----------------------------------------------------------------
397    // Top-level dispatch
398    // -----------------------------------------------------------------
399
400    fn parse_response(&mut self) -> Result<Response<'a>, ParseError> {
401        if self.is_eof() {
402            return Err(ParseError::Incomplete);
403        }
404        match self.peek()? {
405            b'+' => self.parse_continue_req(),
406            b'*' => self.parse_untagged(),
407            _ => self.parse_tagged(),
408        }
409    }
410
411    fn parse_continue_req(&mut self) -> Result<Response<'a>, ParseError> {
412        self.consume_byte(b'+')?;
413        self.consume_sp()?;
414        let (code, text) = self.parse_resp_text()?;
415        self.consume_crlf()?;
416        Ok(Response::Continue(ContinueReq { code, text }))
417    }
418
419    fn parse_untagged(&mut self) -> Result<Response<'a>, ParseError> {
420        self.consume_byte(b'*')?;
421        self.consume_sp()?;
422
423        // First, try a status keyword (OK / NO / BAD / PREAUTH / BYE).
424        if let Some(status) = self.try_parse_status_keyword() {
425            let (code, text) = self.parse_resp_text_after_keyword()?;
426            self.consume_crlf()?;
427            return Ok(Response::Status(StatusResponse {
428                tag: None,
429                status,
430                code,
431                text,
432            }));
433        }
434
435        // Otherwise: a data response. The first token is either a number
436        // (FETCH / EXISTS / RECENT / EXPUNGE) or a name.
437        if self.peek()?.is_ascii_digit() {
438            return self.parse_numeric_data_response();
439        }
440
441        self.parse_named_data_response()
442    }
443
444    fn parse_tagged(&mut self) -> Result<Response<'a>, ParseError> {
445        let tag = self.parse_tag()?;
446        self.consume_sp()?;
447        let status = match self.try_parse_status_keyword() {
448            Some(s) => s,
449            None => {
450                // The byte after the tag's SP is required to be a status
451                // keyword. If the buffer is too short for any keyword, or
452                // its prefix matches one (`A0001 O…`), we're incomplete.
453                let rem = &self.input[self.pos..];
454                if could_be_status_keyword_prefix(rem) {
455                    return Err(ParseError::Incomplete);
456                }
457                return Err(ParseError::Malformed("expected tagged status keyword"));
458            }
459        };
460        let (code, text) = self.parse_resp_text_after_keyword()?;
461        self.consume_crlf()?;
462        Ok(Response::Status(StatusResponse {
463            tag: Some(tag),
464            status,
465            code,
466            text,
467        }))
468    }
469
470    /// resp-text following a status keyword. RFC requires `SP resp-text`,
471    /// but we tolerate the SP being absent if the line ends immediately
472    /// (so an incomplete-buffer `A1 OK\r` is reported as Incomplete via
473    /// `consume_crlf`, not as InvalidChar).
474    fn parse_resp_text_after_keyword(
475        &mut self,
476    ) -> Result<(Option<ResponseCode<'a>>, &'a str), ParseError> {
477        if self.try_consume_byte(b' ') {
478            self.parse_resp_text()
479        } else {
480            // No SP — treat resp-text as empty. `consume_crlf` will catch
481            // truncated buffers as Incomplete.
482            Ok((None, ""))
483        }
484    }
485
486    fn try_parse_status_keyword(&mut self) -> Option<Status> {
487        // PREAUTH must be checked before generic OK/NO/BAD/BYE because none
488        // of those is a prefix of PREAUTH; the boundary-aware keyword match
489        // also prevents `OKAY`-style false positives.
490        if self.try_consume_keyword_ci(b"PREAUTH") {
491            Some(Status::PreAuth)
492        } else if self.try_consume_keyword_ci(b"OK") {
493            Some(Status::Ok)
494        } else if self.try_consume_keyword_ci(b"NO") {
495            Some(Status::No)
496        } else if self.try_consume_keyword_ci(b"BAD") {
497            Some(Status::Bad)
498        } else if self.try_consume_keyword_ci(b"BYE") {
499            Some(Status::Bye)
500        } else {
501            None
502        }
503    }
504
505    /// resp-text = ["[" resp-text-code "]" SP] text
506    fn parse_resp_text(&mut self) -> Result<(Option<ResponseCode<'a>>, &'a str), ParseError> {
507        let code = if self.try_consume_byte(b'[') {
508            let c = self.parse_resp_text_code()?;
509            self.consume_byte(b']')?;
510            // RFC: SP follows. Some servers omit it when text is empty.
511            let _ = self.try_consume_byte(b' ');
512            Some(c)
513        } else {
514            None
515        };
516        let text = self.parse_text()?;
517        Ok((code, text))
518    }
519
520    fn parse_resp_text_code(&mut self) -> Result<ResponseCode<'a>, ParseError> {
521        // Read the code atom up to `]` or SP.
522        let start = self.pos;
523        while let Ok(c) = self.peek() {
524            if c == b']' || c == b' ' {
525                break;
526            }
527            self.pos += 1;
528        }
529        if start == self.pos {
530            return Err(ParseError::Malformed("empty response code"));
531        }
532        let atom_bytes = &self.input[start..self.pos];
533        let atom =
534            core::str::from_utf8(atom_bytes).map_err(|_| ParseError::Other("Invalid UTF-8"))?;
535
536        let code = match () {
537            _ if atom.eq_ignore_ascii_case("ALERT") => ResponseCode::Alert,
538            _ if atom.eq_ignore_ascii_case("PARSE") => ResponseCode::Parse,
539            _ if atom.eq_ignore_ascii_case("READ-ONLY") => ResponseCode::ReadOnly,
540            _ if atom.eq_ignore_ascii_case("READ-WRITE") => ResponseCode::ReadWrite,
541            _ if atom.eq_ignore_ascii_case("TRYCREATE") => ResponseCode::TryCreate,
542            _ if atom.eq_ignore_ascii_case("UIDNEXT") => {
543                self.consume_sp()?;
544                ResponseCode::UidNext(self.parse_number()?)
545            }
546            _ if atom.eq_ignore_ascii_case("UIDVALIDITY") => {
547                self.consume_sp()?;
548                ResponseCode::UidValidity(self.parse_number()?)
549            }
550            _ if atom.eq_ignore_ascii_case("UNSEEN") => {
551                self.consume_sp()?;
552                ResponseCode::Unseen(self.parse_number()?)
553            }
554            _ if atom.eq_ignore_ascii_case("CAPABILITY") => {
555                let mut caps = Vec::new();
556                while self.try_consume_byte(b' ') {
557                    caps.push(self.parse_atom()?);
558                }
559                ResponseCode::Capability(caps)
560            }
561            _ if atom.eq_ignore_ascii_case("PERMANENTFLAGS") => {
562                self.consume_sp()?;
563                ResponseCode::PermanentFlags(self.parse_paren_atom_list()?)
564            }
565            _ if atom.eq_ignore_ascii_case("BADCHARSET") => {
566                let charsets = if self.try_consume_byte(b' ') {
567                    self.parse_paren_atom_list()?
568                } else {
569                    Vec::new()
570                };
571                ResponseCode::BadCharset(charsets)
572            }
573            _ => {
574                let extra = if self.try_consume_byte(b' ') {
575                    let extra_start = self.pos;
576                    while let Ok(c) = self.peek() {
577                        if c == b']' {
578                            break;
579                        }
580                        self.pos += 1;
581                    }
582                    let bytes = &self.input[extra_start..self.pos];
583                    Some(
584                        core::str::from_utf8(bytes)
585                            .map_err(|_| ParseError::Other("Invalid UTF-8"))?,
586                    )
587                } else {
588                    None
589                };
590                ResponseCode::Other(atom, extra)
591            }
592        };
593        Ok(code)
594    }
595
596    // -----------------------------------------------------------------
597    // Data responses
598    // -----------------------------------------------------------------
599
600    fn parse_numeric_data_response(&mut self) -> Result<Response<'a>, ParseError> {
601        let n = self.parse_number()?;
602        self.consume_sp()?;
603        let kind = self.parse_atom()?;
604        let resp = if kind.eq_ignore_ascii_case("EXISTS") {
605            DataResponse::Exists(n)
606        } else if kind.eq_ignore_ascii_case("RECENT") {
607            DataResponse::Recent(n)
608        } else if kind.eq_ignore_ascii_case("EXPUNGE") {
609            DataResponse::Expunge(n)
610        } else if kind.eq_ignore_ascii_case("FETCH") {
611            self.consume_sp()?;
612            let attributes = self.parse_fetch_msg_att()?;
613            DataResponse::Fetch { seq: n, attributes }
614        } else {
615            // Unknown numeric data response — fall back to Other (raw line).
616            return self.finish_other_data_with_prefix(n, kind);
617        };
618        self.consume_crlf()?;
619        Ok(Response::Data(resp))
620    }
621
622    /// Salvage path for unrecognized numeric data responses: capture the
623    /// rest of the line as raw bytes.
624    fn finish_other_data_with_prefix(
625        &mut self,
626        _n: u32,
627        _kind: &'a str,
628    ) -> Result<Response<'a>, ParseError> {
629        // Roll back to the start of the line — we want to capture the whole line.
630        // The prefix is already consumed; just continue scanning to CR.
631        let line_start = self.find_current_line_start();
632        while let Ok(c) = self.peek() {
633            if c == b'\r' {
634                break;
635            }
636            self.pos += 1;
637        }
638        let line = &self.input[line_start..self.pos];
639        self.consume_crlf()?;
640        Ok(Response::Data(DataResponse::Other(line)))
641    }
642
643    fn find_current_line_start(&self) -> usize {
644        // Walk backward until we hit a CRLF or input start.
645        let mut i = self.pos;
646        while i > 0 && !(i >= 2 && &self.input[i - 2..i] == b"\r\n") {
647            i -= 1;
648        }
649        i
650    }
651
652    fn parse_named_data_response(&mut self) -> Result<Response<'a>, ParseError> {
653        let line_start = self.pos;
654        let name = self.parse_atom()?;
655
656        let resp = if name.eq_ignore_ascii_case("CAPABILITY") {
657            let mut caps = Vec::new();
658            while self.try_consume_byte(b' ') {
659                caps.push(self.parse_atom()?);
660            }
661            DataResponse::Capability(caps)
662        } else if name.eq_ignore_ascii_case("LIST") {
663            self.consume_sp()?;
664            let (flags, delimiter, name) = self.parse_list_lsub_body()?;
665            DataResponse::List {
666                flags,
667                delimiter,
668                name,
669            }
670        } else if name.eq_ignore_ascii_case("LSUB") {
671            self.consume_sp()?;
672            let (flags, delimiter, name) = self.parse_list_lsub_body()?;
673            DataResponse::Lsub {
674                flags,
675                delimiter,
676                name,
677            }
678        } else if name.eq_ignore_ascii_case("STATUS") {
679            self.consume_sp()?;
680            let mailbox = self.parse_astring()?;
681            self.consume_sp()?;
682            let items = self.parse_status_items()?;
683            DataResponse::Status { mailbox, items }
684        } else if name.eq_ignore_ascii_case("SEARCH") {
685            let mut ids = Vec::new();
686            while self.try_consume_byte(b' ') {
687                ids.push(self.parse_number()?);
688            }
689            DataResponse::Search(ids)
690        } else if name.eq_ignore_ascii_case("FLAGS") {
691            self.consume_sp()?;
692            DataResponse::Flags(self.parse_paren_atom_list()?)
693        } else {
694            // Unknown — capture the raw line.
695            while let Ok(c) = self.peek() {
696                if c == b'\r' {
697                    break;
698                }
699                self.pos += 1;
700            }
701            let line = &self.input[line_start..self.pos];
702            self.consume_crlf()?;
703            return Ok(Response::Data(DataResponse::Other(line)));
704        };
705
706        self.consume_crlf()?;
707        Ok(Response::Data(resp))
708    }
709
710    fn parse_list_lsub_body(
711        &mut self,
712    ) -> Result<(Vec<&'a str>, Option<&'a str>, &'a str), ParseError> {
713        let flags = self.parse_paren_atom_list()?;
714        self.consume_sp()?;
715        let delimiter = self.parse_nstring()?;
716        self.consume_sp()?;
717        let name = self.parse_astring()?;
718        Ok((flags, delimiter, name))
719    }
720
721    fn parse_status_items(&mut self) -> Result<Vec<StatusItem>, ParseError> {
722        self.consume_byte(b'(')?;
723        let mut items = Vec::new();
724        if self.try_consume_byte(b')') {
725            return Ok(items);
726        }
727        loop {
728            let key = self.parse_atom()?;
729            self.consume_sp()?;
730            let value = self.parse_number()?;
731            items.push(if key.eq_ignore_ascii_case("MESSAGES") {
732                StatusItem::Messages(value)
733            } else if key.eq_ignore_ascii_case("RECENT") {
734                StatusItem::Recent(value)
735            } else if key.eq_ignore_ascii_case("UIDNEXT") {
736                StatusItem::UidNext(value)
737            } else if key.eq_ignore_ascii_case("UIDVALIDITY") {
738                StatusItem::UidValidity(value)
739            } else if key.eq_ignore_ascii_case("UNSEEN") {
740                StatusItem::Unseen(value)
741            } else {
742                StatusItem::Other(value)
743            });
744            match self.peek()? {
745                b' ' => {
746                    self.pos += 1;
747                }
748                b')' => {
749                    self.pos += 1;
750                    return Ok(items);
751                }
752                _ => return Err(ParseError::Malformed("expected SP or ) in STATUS items")),
753            }
754        }
755    }
756
757    // -----------------------------------------------------------------
758    // FETCH attributes
759    // -----------------------------------------------------------------
760
761    fn parse_fetch_msg_att(&mut self) -> Result<Vec<FetchAttribute<'a>>, ParseError> {
762        self.consume_byte(b'(')?;
763        let mut atts = Vec::new();
764        if self.try_consume_byte(b')') {
765            return Ok(atts);
766        }
767        loop {
768            atts.push(self.parse_one_fetch_att()?);
769            match self.peek()? {
770                b' ' => {
771                    self.pos += 1;
772                }
773                b')' => {
774                    self.pos += 1;
775                    return Ok(atts);
776                }
777                _ => return Err(ParseError::Malformed("expected SP or ) in FETCH atts")),
778            }
779        }
780    }
781
782    fn parse_one_fetch_att(&mut self) -> Result<FetchAttribute<'a>, ParseError> {
783        let name = self.parse_fetch_att_name()?;
784
785        if name.eq_ignore_ascii_case("UID") {
786            self.consume_sp()?;
787            return Ok(FetchAttribute::Uid(self.parse_number()?));
788        }
789        if name.eq_ignore_ascii_case("FLAGS") {
790            self.consume_sp()?;
791            return Ok(FetchAttribute::Flags(self.parse_paren_atom_list()?));
792        }
793        if name.eq_ignore_ascii_case("INTERNALDATE") {
794            self.consume_sp()?;
795            let s = self.parse_quoted()?;
796            return Ok(FetchAttribute::InternalDate(s));
797        }
798        if name.eq_ignore_ascii_case("RFC822.SIZE") {
799            self.consume_sp()?;
800            return Ok(FetchAttribute::Rfc822Size(self.parse_number()?));
801        }
802        if name.eq_ignore_ascii_case("RFC822") {
803            self.consume_sp()?;
804            return Ok(FetchAttribute::Rfc822(self.parse_nstring_bytes()?));
805        }
806        if name.eq_ignore_ascii_case("RFC822.HEADER") {
807            self.consume_sp()?;
808            return Ok(FetchAttribute::Rfc822Header(self.parse_nstring_bytes()?));
809        }
810        if name.eq_ignore_ascii_case("RFC822.TEXT") {
811            self.consume_sp()?;
812            return Ok(FetchAttribute::Rfc822Text(self.parse_nstring_bytes()?));
813        }
814        if name.eq_ignore_ascii_case("ENVELOPE") {
815            self.consume_sp()?;
816            return Ok(FetchAttribute::Envelope(self.parse_balanced_parens()?));
817        }
818        if name.eq_ignore_ascii_case("BODYSTRUCTURE") {
819            self.consume_sp()?;
820            return Ok(FetchAttribute::BodyStructure(self.parse_balanced_parens()?));
821        }
822        if name.eq_ignore_ascii_case("BODY") {
823            // Two cases: `BODY <body>` (= non-extensible body structure) or
824            // `BODY[<section>]<<origin>> <nstring>`.
825            if self.try_consume_byte(b'[') {
826                let section = self.read_until_byte(b']')?;
827                self.consume_byte(b']')?;
828                let origin = if self.try_consume_byte(b'<') {
829                    let n = self.parse_number()?;
830                    self.consume_byte(b'>')?;
831                    Some(n)
832                } else {
833                    None
834                };
835                self.consume_sp()?;
836                let data = self.parse_nstring_bytes_optional()?;
837                return Ok(FetchAttribute::BodySection {
838                    section: if section.is_empty() {
839                        None
840                    } else {
841                        Some(section)
842                    },
843                    origin,
844                    data,
845                });
846            }
847            self.consume_sp()?;
848            return Ok(FetchAttribute::Body(self.parse_balanced_parens()?));
849        }
850
851        // Unknown attribute — try to skip its value gracefully.
852        Err(ParseError::Malformed("unknown FETCH attribute"))
853    }
854
855    /// FETCH attribute names can include `.` and `[...]`; we read up to a SP.
856    fn parse_fetch_att_name(&mut self) -> Result<&'a str, ParseError> {
857        let start = self.pos;
858        while let Ok(c) = self.peek() {
859            if c == b' ' || c == b'[' || c == b')' {
860                break;
861            }
862            self.pos += 1;
863        }
864        if start == self.pos {
865            return Err(ParseError::Malformed("expected FETCH att name"));
866        }
867        core::str::from_utf8(&self.input[start..self.pos])
868            .map_err(|_| ParseError::Other("Invalid UTF-8"))
869    }
870
871    fn read_until_byte(&mut self, terminator: u8) -> Result<&'a str, ParseError> {
872        let start = self.pos;
873        while let Ok(c) = self.peek() {
874            if c == terminator {
875                break;
876            }
877            if c == b'\r' || c == b'\n' {
878                return Err(ParseError::Malformed("CR/LF before terminator"));
879            }
880            self.pos += 1;
881        }
882        core::str::from_utf8(&self.input[start..self.pos])
883            .map_err(|_| ParseError::Other("Invalid UTF-8"))
884    }
885
886    /// nstring → byte slice (literal or quoted). NIL becomes an empty slice.
887    fn parse_nstring_bytes(&mut self) -> Result<&'a [u8], ParseError> {
888        match self.peek()? {
889            b'"' => {
890                let s = self.parse_quoted()?;
891                Ok(s.as_bytes())
892            }
893            b'{' => self.parse_literal(),
894            _ => {
895                if self.try_consume_keyword_ci(b"NIL") {
896                    Ok(&self.input[self.pos..self.pos])
897                } else {
898                    Err(ParseError::Malformed("expected nstring"))
899                }
900            }
901        }
902    }
903
904    /// Like `parse_nstring_bytes` but returns `None` for NIL.
905    fn parse_nstring_bytes_optional(&mut self) -> Result<Option<&'a [u8]>, ParseError> {
906        match self.peek()? {
907            b'"' => {
908                let s = self.parse_quoted()?;
909                Ok(Some(s.as_bytes()))
910            }
911            b'{' => Ok(Some(self.parse_literal()?)),
912            _ => {
913                if self.try_consume_keyword_ci(b"NIL") {
914                    Ok(None)
915                } else {
916                    Err(ParseError::Malformed("expected nstring"))
917                }
918            }
919        }
920    }
921}
922
923/// True iff `bytes` is a (possibly empty) case-insensitive prefix of one
924/// of the IMAP status keywords. Used to disambiguate Incomplete from
925/// Malformed when the parser is sitting at a known keyword position.
926fn could_be_status_keyword_prefix(bytes: &[u8]) -> bool {
927    const KEYWORDS: &[&[u8]] = &[b"OK", b"NO", b"BAD", b"BYE", b"PREAUTH"];
928    for kw in KEYWORDS {
929        if bytes.len() < kw.len()
930            && kw[..bytes.len()]
931                .iter()
932                .zip(bytes.iter())
933                .all(|(a, b)| a.eq_ignore_ascii_case(b))
934        {
935            return true;
936        }
937    }
938    false
939}
940
941/// True if `c` is a valid ATOM-CHAR per RFC 9051 grammar.
942fn is_atom_char(c: u8) -> bool {
943    !matches!(
944        c,
945        // CTL: 0x00-0x1F and 0x7F
946        0x00..=0x1F | 0x7F | b'(' | b')' | b'{' | b' ' | b'%' | b'*' | b'"' | b'\\' | b']'
947    )
948}
949
950// ---------------------------------------------------------------------------
951// Tests
952// ---------------------------------------------------------------------------
953
954#[cfg(test)]
955mod tests {
956    use super::*;
957    use crate::ast::*;
958
959    fn ok<'a>(input: &'a [u8]) -> Response<'a> {
960        let (rem, r) = parse_response(input).unwrap();
961        assert_eq!(rem.len(), 0, "non-empty remainder for input {:?}", input);
962        r
963    }
964
965    // --- Continue ----------------------------------------------------
966
967    #[test]
968    fn test_parse_continue() {
969        let r = ok(b"+ ready for data\r\n");
970        assert_eq!(
971            r,
972            Response::Continue(ContinueReq {
973                code: None,
974                text: "ready for data",
975            })
976        );
977    }
978
979    #[test]
980    fn test_parse_continue_with_code() {
981        let r = ok(b"+ [ALERT] keep going\r\n");
982        if let Response::Continue(c) = r {
983            assert_eq!(c.code, Some(ResponseCode::Alert));
984            assert_eq!(c.text, "keep going");
985        } else {
986            panic!("expected continue");
987        }
988    }
989
990    #[test]
991    fn test_parse_continue_invalid_utf8() {
992        let res = parse_response(b"+ \xFF\r\n");
993        assert!(matches!(res, Err(ParseError::Other("Invalid UTF-8"))));
994    }
995
996    #[test]
997    fn test_parse_continue_expected_crlf() {
998        let res = parse_response(b"+ text\rX");
999        assert!(matches!(res, Err(ParseError::Malformed(_))));
1000    }
1001
1002    // --- Untagged Status --------------------------------------------
1003
1004    #[test]
1005    fn test_parse_untagged_ok() {
1006        let r = ok(b"* OK IMAP4rev1 Service Ready\r\n");
1007        assert_eq!(
1008            r,
1009            Response::Status(StatusResponse {
1010                tag: None,
1011                status: Status::Ok,
1012                code: None,
1013                text: "IMAP4rev1 Service Ready",
1014            })
1015        );
1016    }
1017
1018    #[test]
1019    fn test_parse_untagged_preauth() {
1020        let r = ok(b"* PREAUTH already authenticated\r\n");
1021        if let Response::Status(s) = r {
1022            assert_eq!(s.status, Status::PreAuth);
1023            assert_eq!(s.text, "already authenticated");
1024        } else {
1025            panic!("expected status");
1026        }
1027    }
1028
1029    #[test]
1030    fn test_parse_bye() {
1031        let r = ok(b"* BYE Logging out\r\n");
1032        if let Response::Status(s) = r {
1033            assert_eq!(s.status, Status::Bye);
1034        } else {
1035            panic!("expected status");
1036        }
1037    }
1038
1039    #[test]
1040    fn test_parse_untagged_invalid_utf8() {
1041        let res = parse_response(b"* OK \xFF\r\n");
1042        assert!(matches!(res, Err(ParseError::Other("Invalid UTF-8"))));
1043    }
1044
1045    // --- Response codes ---------------------------------------------
1046
1047    #[test]
1048    fn test_parse_resp_code_uidvalidity() {
1049        let r = ok(b"* OK [UIDVALIDITY 12345] mailbox open\r\n");
1050        if let Response::Status(s) = r {
1051            assert_eq!(s.code, Some(ResponseCode::UidValidity(12345)));
1052            assert_eq!(s.text, "mailbox open");
1053        } else {
1054            panic!();
1055        }
1056    }
1057
1058    #[test]
1059    fn test_parse_resp_code_uidnext_unseen() {
1060        let r = ok(b"A1 OK [UIDNEXT 7] done\r\n");
1061        if let Response::Status(s) = r {
1062            assert_eq!(s.code, Some(ResponseCode::UidNext(7)));
1063            assert_eq!(s.tag, Some("A1"));
1064            assert_eq!(s.status, Status::Ok);
1065        } else {
1066            panic!();
1067        }
1068        let r = ok(b"A1 OK [UNSEEN 4] done\r\n");
1069        if let Response::Status(s) = r {
1070            assert_eq!(s.code, Some(ResponseCode::Unseen(4)));
1071        } else {
1072            panic!();
1073        }
1074    }
1075
1076    #[test]
1077    fn test_parse_resp_code_alert() {
1078        let r = ok(b"* OK [ALERT] System down at midnight\r\n");
1079        if let Response::Status(s) = r {
1080            assert_eq!(s.code, Some(ResponseCode::Alert));
1081            assert_eq!(s.text, "System down at midnight");
1082        } else {
1083            panic!();
1084        }
1085    }
1086
1087    #[test]
1088    fn test_parse_resp_code_capability() {
1089        let r = ok(b"* OK [CAPABILITY IMAP4rev2 STARTTLS LOGIN] hello\r\n");
1090        if let Response::Status(s) = r {
1091            assert_eq!(
1092                s.code,
1093                Some(ResponseCode::Capability(vec![
1094                    "IMAP4rev2",
1095                    "STARTTLS",
1096                    "LOGIN"
1097                ]))
1098            );
1099        } else {
1100            panic!();
1101        }
1102    }
1103
1104    #[test]
1105    fn test_parse_resp_code_permanentflags() {
1106        let r = ok(b"* OK [PERMANENTFLAGS (\\Seen \\Draft \\*)] limited\r\n");
1107        if let Response::Status(s) = r {
1108            if let Some(ResponseCode::PermanentFlags(flags)) = s.code {
1109                assert_eq!(flags, vec!["\\Seen", "\\Draft", "\\*"]);
1110            } else {
1111                panic!("wrong code");
1112            }
1113        } else {
1114            panic!();
1115        }
1116    }
1117
1118    #[test]
1119    fn test_parse_resp_code_read_states() {
1120        let r = ok(b"A2 OK [READ-WRITE] selected\r\n");
1121        if let Response::Status(s) = r {
1122            assert_eq!(s.code, Some(ResponseCode::ReadWrite));
1123        } else {
1124            panic!();
1125        }
1126        let r = ok(b"A3 OK [READ-ONLY] selected\r\n");
1127        if let Response::Status(s) = r {
1128            assert_eq!(s.code, Some(ResponseCode::ReadOnly));
1129        } else {
1130            panic!();
1131        }
1132    }
1133
1134    #[test]
1135    fn test_parse_resp_code_other() {
1136        let r = ok(b"* OK [APPENDUID 12345 6] appended\r\n");
1137        if let Response::Status(s) = r {
1138            if let Some(ResponseCode::Other(name, extra)) = s.code {
1139                assert_eq!(name, "APPENDUID");
1140                assert_eq!(extra, Some("12345 6"));
1141            } else {
1142                panic!();
1143            }
1144        } else {
1145            panic!();
1146        }
1147    }
1148
1149    // --- Tagged status ----------------------------------------------
1150
1151    #[test]
1152    fn test_parse_tagged_ok() {
1153        let r = ok(b"A1 OK LOGIN completed\r\n");
1154        assert_eq!(
1155            r,
1156            Response::Status(StatusResponse {
1157                tag: Some("A1"),
1158                status: Status::Ok,
1159                code: None,
1160                text: "LOGIN completed",
1161            })
1162        );
1163    }
1164
1165    #[test]
1166    fn test_parse_tagged_no() {
1167        let r = ok(b"A1 NO LOGIN failed\r\n");
1168        if let Response::Status(s) = r {
1169            assert_eq!(s.status, Status::No);
1170            assert_eq!(s.text, "LOGIN failed");
1171        } else {
1172            panic!();
1173        }
1174    }
1175
1176    #[test]
1177    fn test_parse_tagged_bad() {
1178        let r = ok(b"A1 BAD bad command\r\n");
1179        if let Response::Status(s) = r {
1180            assert_eq!(s.status, Status::Bad);
1181        } else {
1182            panic!();
1183        }
1184    }
1185
1186    // --- Data responses ---------------------------------------------
1187
1188    #[test]
1189    fn test_parse_capability_data() {
1190        let r = ok(b"* CAPABILITY IMAP4rev2 STARTTLS AUTH=PLAIN\r\n");
1191        if let Response::Data(DataResponse::Capability(caps)) = r {
1192            assert_eq!(caps, vec!["IMAP4rev2", "STARTTLS", "AUTH=PLAIN"]);
1193        } else {
1194            panic!();
1195        }
1196    }
1197
1198    #[test]
1199    fn test_parse_list() {
1200        let r = ok(b"* LIST (\\HasNoChildren) \".\" \"INBOX\"\r\n");
1201        if let Response::Data(DataResponse::List {
1202            flags,
1203            delimiter,
1204            name,
1205        }) = r
1206        {
1207            assert_eq!(flags, vec!["\\HasNoChildren"]);
1208            assert_eq!(delimiter, Some("."));
1209            assert_eq!(name, "INBOX");
1210        } else {
1211            panic!();
1212        }
1213    }
1214
1215    #[test]
1216    fn test_parse_list_nil_delimiter() {
1217        let r = ok(b"* LIST () NIL \"INBOX\"\r\n");
1218        if let Response::Data(DataResponse::List {
1219            flags,
1220            delimiter,
1221            name,
1222        }) = r
1223        {
1224            assert!(flags.is_empty());
1225            assert_eq!(delimiter, None);
1226            assert_eq!(name, "INBOX");
1227        } else {
1228            panic!();
1229        }
1230    }
1231
1232    #[test]
1233    fn test_parse_lsub() {
1234        let r = ok(b"* LSUB (\\Noselect) \"/\" \"foo\"\r\n");
1235        if let Response::Data(DataResponse::Lsub { flags, .. }) = r {
1236            assert_eq!(flags, vec!["\\Noselect"]);
1237        } else {
1238            panic!();
1239        }
1240    }
1241
1242    #[test]
1243    fn test_parse_status_response() {
1244        let r = ok(b"* STATUS \"INBOX\" (MESSAGES 10 RECENT 1 UNSEEN 3)\r\n");
1245        if let Response::Data(DataResponse::Status { mailbox, items }) = r {
1246            assert_eq!(mailbox, "INBOX");
1247            assert_eq!(
1248                items,
1249                vec![
1250                    StatusItem::Messages(10),
1251                    StatusItem::Recent(1),
1252                    StatusItem::Unseen(3)
1253                ]
1254            );
1255        } else {
1256            panic!();
1257        }
1258    }
1259
1260    #[test]
1261    fn test_parse_search_empty() {
1262        let r = ok(b"* SEARCH\r\n");
1263        if let Response::Data(DataResponse::Search(ids)) = r {
1264            assert!(ids.is_empty());
1265        } else {
1266            panic!();
1267        }
1268    }
1269
1270    #[test]
1271    fn test_parse_search_results() {
1272        let r = ok(b"* SEARCH 1 2 3 42\r\n");
1273        if let Response::Data(DataResponse::Search(ids)) = r {
1274            assert_eq!(ids, vec![1, 2, 3, 42]);
1275        } else {
1276            panic!();
1277        }
1278    }
1279
1280    #[test]
1281    fn test_parse_flags() {
1282        let r = ok(b"* FLAGS (\\Seen \\Draft)\r\n");
1283        if let Response::Data(DataResponse::Flags(flags)) = r {
1284            assert_eq!(flags, vec!["\\Seen", "\\Draft"]);
1285        } else {
1286            panic!();
1287        }
1288    }
1289
1290    #[test]
1291    fn test_parse_exists_recent_expunge() {
1292        if let Response::Data(DataResponse::Exists(n)) = ok(b"* 5 EXISTS\r\n") {
1293            assert_eq!(n, 5);
1294        } else {
1295            panic!();
1296        }
1297        if let Response::Data(DataResponse::Recent(n)) = ok(b"* 1 RECENT\r\n") {
1298            assert_eq!(n, 1);
1299        } else {
1300            panic!();
1301        }
1302        if let Response::Data(DataResponse::Expunge(n)) = ok(b"* 10 EXPUNGE\r\n") {
1303            assert_eq!(n, 10);
1304        } else {
1305            panic!();
1306        }
1307    }
1308
1309    // --- FETCH attributes -------------------------------------------
1310
1311    #[test]
1312    fn test_parse_fetch_flags() {
1313        let r = ok(b"* 1 FETCH (FLAGS (\\Seen))\r\n");
1314        if let Response::Data(DataResponse::Fetch { seq, attributes }) = r {
1315            assert_eq!(seq, 1);
1316            assert_eq!(attributes, vec![FetchAttribute::Flags(vec!["\\Seen"])]);
1317        } else {
1318            panic!();
1319        }
1320    }
1321
1322    #[test]
1323    fn test_parse_fetch_uid() {
1324        let r = ok(b"* 1 FETCH (UID 42)\r\n");
1325        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1326            assert_eq!(attributes, vec![FetchAttribute::Uid(42)]);
1327        } else {
1328            panic!();
1329        }
1330    }
1331
1332    #[test]
1333    fn test_parse_fetch_internaldate() {
1334        let r = ok(b"* 1 FETCH (INTERNALDATE \"17-Jul-1996 02:44:25 -0700\")\r\n");
1335        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1336            assert_eq!(
1337                attributes,
1338                vec![FetchAttribute::InternalDate("17-Jul-1996 02:44:25 -0700")]
1339            );
1340        } else {
1341            panic!();
1342        }
1343    }
1344
1345    #[test]
1346    fn test_parse_fetch_rfc822_size() {
1347        let r = ok(b"* 2 FETCH (RFC822.SIZE 4242)\r\n");
1348        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1349            assert_eq!(attributes, vec![FetchAttribute::Rfc822Size(4242)]);
1350        } else {
1351            panic!();
1352        }
1353    }
1354
1355    #[test]
1356    fn test_parse_fetch_body_section_with_literal() {
1357        let r = ok(b"* 1 FETCH (BODY[] {10}\r\n0123456789)\r\n");
1358        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1359            assert_eq!(
1360                attributes,
1361                vec![FetchAttribute::BodySection {
1362                    section: None,
1363                    origin: None,
1364                    data: Some(b"0123456789".as_ref()),
1365                }]
1366            );
1367        } else {
1368            panic!();
1369        }
1370    }
1371
1372    #[test]
1373    fn test_parse_fetch_body_section_with_section_and_origin() {
1374        let r = ok(b"* 1 FETCH (BODY[HEADER.FIELDS (FROM TO)]<0> {7}\r\nFrom: a)\r\n");
1375        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1376            assert_eq!(
1377                attributes,
1378                vec![FetchAttribute::BodySection {
1379                    section: Some("HEADER.FIELDS (FROM TO)"),
1380                    origin: Some(0),
1381                    data: Some(b"From: a".as_ref()),
1382                }]
1383            );
1384        } else {
1385            panic!();
1386        }
1387    }
1388
1389    #[test]
1390    fn test_parse_fetch_body_section_nil() {
1391        let r = ok(b"* 1 FETCH (BODY[] NIL)\r\n");
1392        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1393            assert_eq!(
1394                attributes,
1395                vec![FetchAttribute::BodySection {
1396                    section: None,
1397                    origin: None,
1398                    data: None,
1399                }]
1400            );
1401        } else {
1402            panic!();
1403        }
1404    }
1405
1406    #[test]
1407    fn test_parse_fetch_multi_attrs_with_literal() {
1408        let r = ok(b"* 1 FETCH (UID 5 BODY[] {3}\r\nabc FLAGS (\\Seen))\r\n");
1409        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1410            assert_eq!(
1411                attributes,
1412                vec![
1413                    FetchAttribute::Uid(5),
1414                    FetchAttribute::BodySection {
1415                        section: None,
1416                        origin: None,
1417                        data: Some(b"abc".as_ref()),
1418                    },
1419                    FetchAttribute::Flags(vec!["\\Seen"]),
1420                ]
1421            );
1422        } else {
1423            panic!();
1424        }
1425    }
1426
1427    #[test]
1428    fn test_parse_fetch_envelope_raw() {
1429        let r = ok(
1430            b"* 1 FETCH (ENVELOPE (\"Date\" \"Subject\" NIL NIL NIL NIL NIL NIL NIL \"<id>\"))\r\n",
1431        );
1432        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1433            if let FetchAttribute::Envelope(bytes) = &attributes[0] {
1434                assert!(bytes.starts_with(b"("));
1435                assert!(bytes.ends_with(b")"));
1436            } else {
1437                panic!("expected envelope");
1438            }
1439        } else {
1440            panic!();
1441        }
1442    }
1443
1444    #[test]
1445    fn test_parse_fetch_bodystructure_raw() {
1446        let r = ok(b"* 1 FETCH (BODYSTRUCTURE (\"text\" \"plain\" NIL NIL NIL \"7BIT\" 12))\r\n");
1447        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1448            assert!(matches!(attributes[0], FetchAttribute::BodyStructure(_)));
1449        } else {
1450            panic!();
1451        }
1452    }
1453
1454    // --- Incomplete & error paths -----------------------------------
1455
1456    #[test]
1457    fn test_parse_empty_input_incomplete() {
1458        let res = parse_response(b"");
1459        assert!(matches!(res, Err(ParseError::Incomplete)));
1460    }
1461
1462    #[test]
1463    fn test_parse_incomplete_no_crlf() {
1464        let res = parse_response(b"* OK ");
1465        assert!(matches!(res, Err(ParseError::Incomplete)));
1466    }
1467
1468    #[test]
1469    fn test_parse_incomplete_only_cr() {
1470        let res = parse_response(b"* OK text\r");
1471        assert!(matches!(res, Err(ParseError::Incomplete)));
1472    }
1473
1474    #[test]
1475    fn test_parse_literal_incomplete() {
1476        let res = parse_response(b"* 1 FETCH (BODY[] {10}\r\nabc");
1477        assert!(matches!(res, Err(ParseError::Incomplete)));
1478    }
1479
1480    #[test]
1481    fn test_parse_literal_too_large() {
1482        // Use a literal length one byte over the cap.
1483        let s = format!("* 1 FETCH (BODY[] {{{}}}\r\n", MAX_LITERAL_SIZE + 1);
1484        let res = parse_response(s.as_bytes());
1485        assert!(matches!(res, Err(ParseError::LiteralTooLarge { .. })));
1486    }
1487
1488    #[test]
1489    fn test_parse_incomplete_tagged_no_space() {
1490        let res = parse_response(b"A1");
1491        assert!(matches!(res, Err(ParseError::Incomplete)));
1492    }
1493
1494    #[test]
1495    fn test_parse_incomplete_tagged_no_crlf() {
1496        let res = parse_response(b"A1 OK\r");
1497        assert!(matches!(res, Err(ParseError::Incomplete)));
1498    }
1499
1500    #[test]
1501    fn test_parse_missing_lf_after_cr() {
1502        let res = parse_response(b"* OK text\rX");
1503        assert!(matches!(res, Err(ParseError::Malformed(_))));
1504    }
1505
1506    #[test]
1507    fn test_parse_unknown_data_falls_back_to_other() {
1508        let r = ok(b"* WIBBLE foo bar\r\n");
1509        if let Response::Data(DataResponse::Other(line)) = r {
1510            assert_eq!(line, b"WIBBLE foo bar");
1511        } else {
1512            panic!();
1513        }
1514    }
1515
1516    // --- Trailing remainder -----------------------------------------
1517
1518    #[test]
1519    fn test_parse_returns_remainder() {
1520        let input = b"* OK first\r\n* OK second\r\n";
1521        let (rem, _) = parse_response(input).unwrap();
1522        assert_eq!(rem, b"* OK second\r\n");
1523    }
1524
1525    // --- Keyword boundary handling ----------------------------------
1526
1527    #[test]
1528    fn test_parse_keyword_prefix_not_status() {
1529        // "OKAY" must NOT be treated as the "OK" status keyword; it falls
1530        // through to a named data response captured verbatim.
1531        let r = ok(b"* OKAY all good\r\n");
1532        if let Response::Data(DataResponse::Other(line)) = r {
1533            assert_eq!(line, b"OKAY all good");
1534        } else {
1535            panic!("expected Other data, got {r:?}");
1536        }
1537    }
1538
1539    // --- CRLF framing errors ----------------------------------------
1540
1541    #[test]
1542    fn test_parse_lf_without_cr_is_malformed() {
1543        // Bare LF (no preceding CR) trips the "expected CR" guard.
1544        let res = parse_response(b"* OK text\n");
1545        assert!(matches!(res, Err(ParseError::Malformed("expected CR"))));
1546    }
1547
1548    // --- Number parsing edge cases ----------------------------------
1549
1550    #[test]
1551    fn test_parse_number_incomplete_at_eof() {
1552        let res = parse_response(b"* OK [UIDNEXT ");
1553        assert!(matches!(res, Err(ParseError::Incomplete)));
1554    }
1555
1556    #[test]
1557    fn test_parse_number_invalid_non_digit() {
1558        let res = parse_response(b"* OK [UIDNEXT x]\r\n");
1559        assert!(matches!(res, Err(ParseError::InvalidNumber)));
1560    }
1561
1562    // --- Atom parsing edge cases ------------------------------------
1563
1564    #[test]
1565    fn test_parse_atom_incomplete_at_eof() {
1566        // Numeric data response truncated right after the SP.
1567        let res = parse_response(b"* 5 ");
1568        assert!(matches!(res, Err(ParseError::Incomplete)));
1569    }
1570
1571    #[test]
1572    fn test_parse_atom_malformed_non_atom_char() {
1573        let res = parse_response(b"* 5 (\r\n");
1574        assert!(matches!(res, Err(ParseError::Malformed("expected atom"))));
1575    }
1576
1577    // --- Tag parsing edge cases -------------------------------------
1578
1579    #[test]
1580    fn test_parse_tag_malformed_empty() {
1581        // First byte is CR: not '+'/'*' so dispatched as tagged, but the tag
1582        // is empty.
1583        let res = parse_response(b"\r\n");
1584        assert!(matches!(res, Err(ParseError::Malformed("expected tag"))));
1585    }
1586
1587    // --- Quoted string escapes & control chars ----------------------
1588
1589    #[test]
1590    fn test_parse_quoted_with_escape_sequence() {
1591        // Escaped quote inside the quoted string is passed through raw.
1592        let r = ok(b"* 1 FETCH (INTERNALDATE \"a\\\"b\")\r\n");
1593        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1594            assert_eq!(attributes, vec![FetchAttribute::InternalDate("a\\\"b")]);
1595        } else {
1596            panic!("expected fetch, got {r:?}");
1597        }
1598    }
1599
1600    #[test]
1601    fn test_parse_quoted_with_cr_is_malformed() {
1602        let res = parse_response(b"* 1 FETCH (INTERNALDATE \"a\rb\")\r\n");
1603        assert!(matches!(
1604            res,
1605            Err(ParseError::Malformed("CR/LF inside quoted string"))
1606        ));
1607    }
1608
1609    // --- astring / nstring forms ------------------------------------
1610
1611    #[test]
1612    fn test_parse_status_mailbox_as_atom() {
1613        // Unquoted (atom) mailbox name exercises the astring atom path.
1614        let r = ok(b"* STATUS INBOX (MESSAGES 1)\r\n");
1615        if let Response::Data(DataResponse::Status { mailbox, .. }) = r {
1616            assert_eq!(mailbox, "INBOX");
1617        } else {
1618            panic!("expected status, got {r:?}");
1619        }
1620    }
1621
1622    #[test]
1623    fn test_parse_status_mailbox_as_literal() {
1624        let r = ok(b"* STATUS {5}\r\nINBOX (MESSAGES 1)\r\n");
1625        if let Response::Data(DataResponse::Status { mailbox, .. }) = r {
1626            assert_eq!(mailbox, "INBOX");
1627        } else {
1628            panic!("expected status, got {r:?}");
1629        }
1630    }
1631
1632    #[test]
1633    fn test_parse_list_delimiter_as_literal() {
1634        let r = ok(b"* LIST () {1}\r\n. \"INBOX\"\r\n");
1635        if let Response::Data(DataResponse::List {
1636            delimiter, name, ..
1637        }) = r
1638        {
1639            assert_eq!(delimiter, Some("."));
1640            assert_eq!(name, "INBOX");
1641        } else {
1642            panic!("expected list, got {r:?}");
1643        }
1644    }
1645
1646    #[test]
1647    fn test_parse_list_delimiter_malformed() {
1648        let res = parse_response(b"* LIST () X \"INBOX\"\r\n");
1649        assert!(matches!(
1650            res,
1651            Err(ParseError::Malformed("expected nstring"))
1652        ));
1653    }
1654
1655    // --- Paren atom list edge cases ---------------------------------
1656
1657    #[test]
1658    fn test_parse_flag_list_malformed_no_separator() {
1659        // No SP between flags after the first one is consumed.
1660        let res = parse_response(b"* FLAGS (\\Seen\\Draft)\r\n");
1661        assert!(matches!(
1662            res,
1663            Err(ParseError::Malformed("expected SP or ) in list"))
1664        ));
1665    }
1666
1667    #[test]
1668    fn test_parse_flag_list_empty_item_malformed() {
1669        let res = parse_response(b"* FLAGS (()\r\n");
1670        assert!(matches!(
1671            res,
1672            Err(ParseError::Malformed("expected flag/atom"))
1673        ));
1674    }
1675
1676    // --- Balanced parens: nesting & literals ------------------------
1677
1678    #[test]
1679    fn test_parse_envelope_with_nested_parens_and_literal() {
1680        let r = ok(b"* 1 FETCH (ENVELOPE ((\"a\") {2}\r\nbc))\r\n");
1681        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1682            if let FetchAttribute::Envelope(bytes) = &attributes[0] {
1683                assert_eq!(*bytes, b"((\"a\") {2}\r\nbc)".as_ref());
1684            } else {
1685                panic!("expected envelope, got {attributes:?}");
1686            }
1687        } else {
1688            panic!("expected fetch, got {r:?}");
1689        }
1690    }
1691
1692    // --- Tagged status keyword disambiguation -----------------------
1693
1694    #[test]
1695    fn test_parse_tagged_partial_keyword_incomplete() {
1696        let res = parse_response(b"A1 O");
1697        assert!(matches!(res, Err(ParseError::Incomplete)));
1698    }
1699
1700    #[test]
1701    fn test_parse_tagged_unknown_keyword_malformed() {
1702        let res = parse_response(b"A1 ZZ done\r\n");
1703        assert!(matches!(
1704            res,
1705            Err(ParseError::Malformed("expected tagged status keyword"))
1706        ));
1707    }
1708
1709    // --- Response code edge cases -----------------------------------
1710
1711    #[test]
1712    fn test_parse_resp_code_empty_malformed() {
1713        let res = parse_response(b"* OK [] text\r\n");
1714        assert!(matches!(
1715            res,
1716            Err(ParseError::Malformed("empty response code"))
1717        ));
1718    }
1719
1720    #[test]
1721    fn test_parse_resp_code_badcharset_with_list() {
1722        let r = ok(b"* NO [BADCHARSET (UTF-8 KOI8-R)] bad charset\r\n");
1723        if let Response::Status(s) = r {
1724            assert_eq!(
1725                s.code,
1726                Some(ResponseCode::BadCharset(vec!["UTF-8", "KOI8-R"]))
1727            );
1728        } else {
1729            panic!("expected status, got {r:?}");
1730        }
1731    }
1732
1733    #[test]
1734    fn test_parse_resp_code_badcharset_without_list() {
1735        let r = ok(b"* NO [BADCHARSET] bad charset\r\n");
1736        if let Response::Status(s) = r {
1737            assert_eq!(s.code, Some(ResponseCode::BadCharset(vec![])));
1738        } else {
1739            panic!("expected status, got {r:?}");
1740        }
1741    }
1742
1743    #[test]
1744    fn test_parse_resp_code_other_without_extra() {
1745        let r = ok(b"* NO [NONEXISTENT] gone\r\n");
1746        if let Response::Status(s) = r {
1747            assert_eq!(s.code, Some(ResponseCode::Other("NONEXISTENT", None)));
1748        } else {
1749            panic!("expected status, got {r:?}");
1750        }
1751    }
1752
1753    // --- Unknown numeric data salvage -------------------------------
1754
1755    #[test]
1756    fn test_parse_unknown_numeric_data_falls_back_to_other() {
1757        let r = ok(b"* 5 WIBBLE foo bar\r\n");
1758        if let Response::Data(DataResponse::Other(line)) = r {
1759            assert_eq!(line, b"* 5 WIBBLE foo bar");
1760        } else {
1761            panic!("expected Other data, got {r:?}");
1762        }
1763    }
1764
1765    // --- STATUS items -----------------------------------------------
1766
1767    #[test]
1768    fn test_parse_status_empty_items() {
1769        let r = ok(b"* STATUS \"INBOX\" ()\r\n");
1770        if let Response::Data(DataResponse::Status { items, .. }) = r {
1771            assert!(items.is_empty());
1772        } else {
1773            panic!("expected status, got {r:?}");
1774        }
1775    }
1776
1777    #[test]
1778    fn test_parse_status_items_uid_and_other() {
1779        let r = ok(b"* STATUS \"X\" (UIDNEXT 5 UIDVALIDITY 9 HIGHESTMODSEQ 100)\r\n");
1780        if let Response::Data(DataResponse::Status { items, .. }) = r {
1781            assert_eq!(
1782                items,
1783                vec![
1784                    StatusItem::UidNext(5),
1785                    StatusItem::UidValidity(9),
1786                    StatusItem::Other(100),
1787                ]
1788            );
1789        } else {
1790            panic!("expected status, got {r:?}");
1791        }
1792    }
1793
1794    #[test]
1795    fn test_parse_status_items_malformed_separator() {
1796        let res = parse_response(b"* STATUS \"X\" (MESSAGES 1Z)\r\n");
1797        assert!(matches!(
1798            res,
1799            Err(ParseError::Malformed("expected SP or ) in STATUS items"))
1800        ));
1801    }
1802
1803    #[test]
1804    fn test_parse_status_items_missing_open_paren() {
1805        // consume_byte('(') fails with InvalidChar.
1806        let res = parse_response(b"* STATUS \"X\" MESSAGES 1\r\n");
1807        assert!(matches!(res, Err(ParseError::InvalidChar(_))));
1808    }
1809
1810    // --- FETCH attribute list edge cases ----------------------------
1811
1812    #[test]
1813    fn test_parse_fetch_empty_attrs() {
1814        let r = ok(b"* 1 FETCH ()\r\n");
1815        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1816            assert!(attributes.is_empty());
1817        } else {
1818            panic!("expected fetch, got {r:?}");
1819        }
1820    }
1821
1822    #[test]
1823    fn test_parse_fetch_attrs_malformed_separator() {
1824        let res = parse_response(b"* 1 FETCH (UID 5Z)\r\n");
1825        assert!(matches!(
1826            res,
1827            Err(ParseError::Malformed("expected SP or ) in FETCH atts"))
1828        ));
1829    }
1830
1831    #[test]
1832    fn test_parse_fetch_unknown_attribute_malformed() {
1833        let res = parse_response(b"* 1 FETCH (FOOBAR 1)\r\n");
1834        assert!(matches!(
1835            res,
1836            Err(ParseError::Malformed("unknown FETCH attribute"))
1837        ));
1838    }
1839
1840    #[test]
1841    fn test_parse_fetch_empty_att_name_malformed() {
1842        let res = parse_response(b"* 1 FETCH ( )\r\n");
1843        assert!(matches!(
1844            res,
1845            Err(ParseError::Malformed("expected FETCH att name"))
1846        ));
1847    }
1848
1849    // --- RFC822 family ----------------------------------------------
1850
1851    #[test]
1852    fn test_parse_fetch_rfc822_literal() {
1853        let r = ok(b"* 1 FETCH (RFC822 {3}\r\nabc)\r\n");
1854        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1855            assert_eq!(attributes, vec![FetchAttribute::Rfc822(b"abc")]);
1856        } else {
1857            panic!("expected fetch, got {r:?}");
1858        }
1859    }
1860
1861    #[test]
1862    fn test_parse_fetch_rfc822_header_and_text() {
1863        let r = ok(b"* 1 FETCH (RFC822.HEADER {2}\r\nhi RFC822.TEXT {2}\r\nyo)\r\n");
1864        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1865            assert_eq!(
1866                attributes,
1867                vec![
1868                    FetchAttribute::Rfc822Header(b"hi"),
1869                    FetchAttribute::Rfc822Text(b"yo"),
1870                ]
1871            );
1872        } else {
1873            panic!("expected fetch, got {r:?}");
1874        }
1875    }
1876
1877    #[test]
1878    fn test_parse_fetch_rfc822_quoted() {
1879        let r = ok(b"* 1 FETCH (RFC822 \"hi\")\r\n");
1880        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1881            assert_eq!(attributes, vec![FetchAttribute::Rfc822(b"hi")]);
1882        } else {
1883            panic!("expected fetch, got {r:?}");
1884        }
1885    }
1886
1887    #[test]
1888    fn test_parse_fetch_rfc822_nil() {
1889        let r = ok(b"* 1 FETCH (RFC822 NIL)\r\n");
1890        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1891            assert_eq!(attributes, vec![FetchAttribute::Rfc822(b"")]);
1892        } else {
1893            panic!("expected fetch, got {r:?}");
1894        }
1895    }
1896
1897    #[test]
1898    fn test_parse_fetch_rfc822_malformed() {
1899        let res = parse_response(b"* 1 FETCH (RFC822 Z)\r\n");
1900        assert!(matches!(
1901            res,
1902            Err(ParseError::Malformed("expected nstring"))
1903        ));
1904    }
1905
1906    // --- BODY structure (no section) --------------------------------
1907
1908    #[test]
1909    fn test_parse_fetch_body_no_section() {
1910        let r = ok(b"* 1 FETCH (BODY (\"text\" \"plain\" NIL))\r\n");
1911        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1912            assert!(matches!(attributes[0], FetchAttribute::Body(_)));
1913        } else {
1914            panic!("expected fetch, got {r:?}");
1915        }
1916    }
1917
1918    #[test]
1919    fn test_parse_fetch_body_section_quoted_data() {
1920        let r = ok(b"* 1 FETCH (BODY[] \"hi\")\r\n");
1921        if let Response::Data(DataResponse::Fetch { attributes, .. }) = r {
1922            assert_eq!(
1923                attributes,
1924                vec![FetchAttribute::BodySection {
1925                    section: None,
1926                    origin: None,
1927                    data: Some(b"hi".as_ref()),
1928                }]
1929            );
1930        } else {
1931            panic!("expected fetch, got {r:?}");
1932        }
1933    }
1934
1935    #[test]
1936    fn test_parse_fetch_body_section_malformed_data() {
1937        let res = parse_response(b"* 1 FETCH (BODY[] Z)\r\n");
1938        assert!(matches!(
1939            res,
1940            Err(ParseError::Malformed("expected nstring"))
1941        ));
1942    }
1943
1944    #[test]
1945    fn test_parse_fetch_body_section_crlf_in_section() {
1946        let res = parse_response(b"* 1 FETCH (BODY[HE\r\n");
1947        assert!(matches!(
1948            res,
1949            Err(ParseError::Malformed("CR/LF before terminator"))
1950        ));
1951    }
1952}