diff --git a/imap-codec/Cargo.toml b/imap-codec/Cargo.toml index 3cc2b6e5..1cb62d2e 100644 --- a/imap-codec/Cargo.toml +++ b/imap-codec/Cargo.toml @@ -33,6 +33,7 @@ quirk = [ "quirk_trailing_space_id", "quirk_trailing_space_search", "quirk_spaces_between_addresses", + "quirk_spaces_between_body_parts", "quirk_empty_continue_req", "quirk_body_fld_enc_nil_to_empty", "quirk_always_normalize_sequence_sets", @@ -49,6 +50,8 @@ quirk_rectify_numbers = [] quirk_excessive_space_quota_resource = [] # Accept spaces between envelope addresses in `FETCH` data response. quirk_spaces_between_addresses = [] +# Accept spaces between body parts in multipart BODYSTRUCTURE responses. +quirk_spaces_between_body_parts = [] # Accept a trailing space in `STATUS` data response. quirk_trailing_space_status = [] # Accept a trailing space in `CAPABILITY` data response. diff --git a/imap-codec/src/body.rs b/imap-codec/src/body.rs index b78f51cc..eebdbe1e 100644 --- a/imap-codec/src/body.rs +++ b/imap-codec/src/body.rs @@ -454,8 +454,13 @@ fn body_type_mpart_limited( })); } + let body_parser = body(remaining_recursion); + + #[cfg(feature = "quirk_spaces_between_body_parts")] + let body_parser = preceded(many0(sp), body_parser); + let mut parser = tuple(( - many1(body(remaining_recursion)), + many1(body_parser), sp, media_subtype, opt(preceded(sp, body_ext_mpart)), @@ -790,4 +795,30 @@ mod tests { assert_eq!(body_fld_octets(b"-999999)").unwrap().1, 0); } } + + #[test] + fn test_spaces_between_body_parts_quirk() { + use super::body_type_mpart_limited; + + // Trailing ")" simulates the enclosing body delimiter needed by the streaming parser. + + // Adjacent bodies (no space between parts) should always parse. + let adjacent = b"(\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 0 0)(\"TEXT\" \"HTML\" NIL NIL NIL \"7BIT\" 0 0) \"alternative\")"; + assert!(body_type_mpart_limited(adjacent, 8).is_ok()); + + // Bodies with spaces between parts (common server quirk). + let spaced = b"(\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 0 0) (\"TEXT\" \"HTML\" NIL NIL NIL \"7BIT\" 0 0) \"alternative\")"; + + #[cfg(not(feature = "quirk_spaces_between_body_parts"))] + { + // Without quirk, the parser stops after the first body and fails + // because it can't parse the space-separated second body. + assert!(body_type_mpart_limited(spaced, 8).is_err()); + } + + #[cfg(feature = "quirk_spaces_between_body_parts")] + { + assert!(body_type_mpart_limited(spaced, 8).is_ok()); + } + } } diff --git a/justfile b/justfile index f2a507e4..f848d342 100644 --- a/justfile +++ b/justfile @@ -71,6 +71,7 @@ cargo_hack mode: install_cargo_hack quirk_trailing_space_search,\ quirk_trailing_space_status,\ quirk_spaces_between_addresses,\ + quirk_spaces_between_body_parts,\ quirk_empty_continue_req,\ quirk_body_fld_enc_nil_to_empty\ {{ mode }}