#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::similar_names,
clippy::unreadable_literal,
clippy::wildcard_in_or_patterns,
clippy::vec_init_then_push,
clippy::match_wild_err_arm,
clippy::same_item_push
)]
use super::*;
use crate::types::validated::MailboxName;
#[test]
fn atom_valid() {
let (rest, val) = atom(b"INBOX rest").unwrap();
assert_eq!(val, b"INBOX");
assert_eq!(rest, b" rest");
}
#[test]
fn atom_rejects_specials() {
assert!(atom(b"(bad").is_err());
assert!(atom(b")bad").is_err());
assert!(atom(b"{bad").is_err());
assert!(atom(b"\"bad").is_err());
}
#[test]
fn atom_empty_fails() {
assert!(atom(b" oops").is_err());
}
#[test]
fn quoted_string_simple() {
let (rest, val) = quoted_string(b"\"hello\" rest").unwrap();
assert_eq!(val, b"hello");
assert_eq!(rest, b" rest");
}
#[test]
fn quoted_string_escapes() {
let (_, val) = quoted_string(b"\"a\\\"b\\\\c\"").unwrap();
assert_eq!(val, b"a\"b\\c");
}
#[test]
fn quoted_string_non_ascii() {
let (_, val) = quoted_string(b"\"caf\xc3\xa9\"").unwrap();
assert_eq!(val, b"caf\xc3\xa9");
}
#[test]
fn quoted_string_empty() {
let (_, val) = quoted_string(b"\"\"").unwrap();
assert!(val.is_empty());
}
#[test]
fn literal_simple() {
let (rest, val) = literal(b"{5}\r\nhello rest").unwrap();
assert_eq!(val, b"hello");
assert_eq!(rest, b" rest");
}
#[test]
fn literal_plus() {
let (_, val) = literal(b"{3+}\r\nabc").unwrap();
assert_eq!(val, b"abc");
}
#[test]
fn literal_zero_length() {
let (_, val) = literal(b"{0}\r\n").unwrap();
assert!(val.is_empty());
}
#[test]
fn nstring_nil() {
let (_, val) = nstring(b"NIL").unwrap();
assert!(val.is_none());
}
#[test]
fn nstring_nil_case_insensitive() {
let (_, val) = nstring(b"nil").unwrap();
assert!(val.is_none());
let (_, val) = nstring(b"Nil").unwrap();
assert!(val.is_none());
}
#[test]
fn nstring_string() {
let (_, val) = nstring(b"\"hello\"").unwrap();
assert_eq!(val.unwrap(), b"hello");
}
#[test]
fn nstring_nil_requires_token_boundary() {
assert!(
nstring(b"NILX").is_err(),
"nstring must not partially match 'NIL' from 'NILX' \
(RFC 3501 Section 9: atoms are delimited tokens)"
);
}
#[test]
fn nstring_nil_with_delimiter() {
let (rest, val) = nstring(b"NIL rest").unwrap();
assert!(val.is_none());
assert_eq!(rest, b" rest");
let (rest, val) = nstring(b"NIL)").unwrap();
assert!(val.is_none());
assert_eq!(rest, b")");
let (rest, val) = nstring(b"NIL\r\n").unwrap();
assert!(val.is_none());
assert_eq!(rest, b"\r\n");
}
#[test]
fn nil_token_open_paren_boundary() {
let (rest, val) = nstring(b"NIL(").unwrap();
assert!(val.is_none(), "NIL( should parse NIL as None");
assert_eq!(rest, b"(");
}
#[test]
fn nil_token_open_brace_boundary() {
let (rest, val) = nstring(b"NIL{5}\r\nhello").unwrap();
assert!(val.is_none(), "NIL{{ should parse NIL as None");
assert_eq!(rest, b"{5}\r\nhello");
}
#[test]
fn nil_token_tab_boundary() {
let (rest, val) = nstring(b"NIL\t").unwrap();
assert!(val.is_none(), "NIL followed by TAB should parse as None");
assert_eq!(rest, b"\t");
}
#[test]
fn nil_token_nul_boundary() {
let (rest, val) = nstring(b"NIL\x00").unwrap();
assert!(val.is_none(), "NIL followed by NUL should parse as None");
assert_eq!(rest, b"\x00");
}
#[test]
fn nil_token_del_boundary() {
let (rest, val) = nstring(b"NIL\x7F").unwrap();
assert!(val.is_none(), "NIL followed by DEL should parse as None");
assert_eq!(rest, b"\x7F");
}
#[test]
fn number_valid() {
let (_, val) = number(b"12345").unwrap();
assert_eq!(val, 12345);
}
#[test]
fn number_overflow() {
assert!(number(b"99999999999").is_err());
}
#[test]
fn number64_valid() {
let (_, val) = number64(b"9223372036854775807").unwrap();
assert_eq!(val, i64::MAX as u64);
}
#[test]
fn flag_system() {
let (_, f) = flag_or_perm(b"\\Seen rest").unwrap();
assert_eq!(f, Flag::Seen);
}
#[test]
fn flag_custom() {
let (_, f) = flag_or_perm(b"$Important rest").unwrap();
assert_eq!(f, Flag::Custom("$Important".into()));
}
#[test]
fn flag_or_perm_accepts_wildcard() {
let (_, f) = flag_or_perm(b"\\* rest").unwrap();
assert_eq!(f, Flag::Wildcard);
}
#[test]
fn flag_list_filters_wildcard() {
let (_, flags) = flag_list(b"(\\Seen \\*)").unwrap();
assert_eq!(flags.len(), 1, "\\* must be filtered from flag_list");
assert_eq!(flags[0], Flag::Seen);
}
#[test]
fn flag_perm_list_retains_wildcard() {
let (_, flags) = flag_perm_list(b"(\\Seen \\*)").unwrap();
assert_eq!(flags.len(), 2, "\\* must be retained in flag_perm_list");
assert!(flags.contains(&Flag::Wildcard));
}
#[test]
fn flag_list_basic() {
let (_, flags) = flag_list(b"(\\Seen \\Flagged $Important)").unwrap();
assert_eq!(flags.len(), 3);
assert_eq!(flags[0], Flag::Seen);
assert_eq!(flags[1], Flag::Flagged);
}
#[test]
fn flag_list_retains_recent() {
let (_, flags) = flag_list(b"(\\Seen \\Recent \\Flagged)").unwrap();
assert_eq!(flags.len(), 3, "\\Recent must be retained in flag_list");
assert!(
flags.contains(&Flag::Recent),
"\\Recent was filtered from flag_list, losing server flag info"
);
}
#[test]
fn flag_list_empty() {
let (_, flags) = flag_list(b"()").unwrap();
assert!(flags.is_empty());
}
#[test]
fn flag_list_in_fetch_context_filters_wildcard() {
let (_, flags) = flag_list(b"(\\Seen \\*)").unwrap();
assert_eq!(flags.len(), 1, "\\* must be filtered from flag_list");
assert_eq!(flags[0], Flag::Seen);
}
#[test]
fn flag_list_in_fetch_context_retains_recent() {
let (_, flags) = flag_list(b"(\\Seen \\Recent \\Flagged)").unwrap();
assert_eq!(flags.len(), 3, "\\Recent must be retained in flag_list");
assert!(
flags.contains(&Flag::Recent),
"\\Recent was filtered from flag_list"
);
}
#[test]
fn flag_list_in_fetch_context_basic() {
let (_, flags) = flag_list(b"(\\Seen \\Answered \\Deleted)").unwrap();
assert_eq!(flags.len(), 3);
assert_eq!(flags[0], Flag::Seen);
assert_eq!(flags[1], Flag::Answered);
assert_eq!(flags[2], Flag::Deleted);
}
#[test]
fn flag_list_in_fetch_context_empty() {
let (_, flags) = flag_list(b"()").unwrap();
assert!(flags.is_empty());
}
#[test]
fn flag_perm_list_empty() {
let (_, flags) = flag_perm_list(b"()").unwrap();
assert!(flags.is_empty());
}
#[test]
fn capability_known() {
let (_, cap) = capability(b"IMAP4rev1").unwrap();
assert_eq!(cap, Capability::Imap4Rev1);
}
#[test]
fn capability_auth() {
let (_, cap) = capability(b"AUTH=PLAIN").unwrap();
assert_eq!(cap, Capability::Auth("PLAIN".into()));
}
#[test]
fn capability_auth_empty_suffix_is_other() {
let (_, cap) = capability(b"AUTH=").unwrap();
assert!(
matches!(cap, Capability::Other(ref s) if s == "AUTH="),
"malformed AUTH= capability must stay Other(\"AUTH=\"), got {cap:?}"
);
}
#[test]
fn capability_thread_empty_suffix_is_other() {
let (_, cap) = capability(b"THREAD=").unwrap();
assert!(
matches!(cap, Capability::Other(ref s) if s == "THREAD="),
"malformed THREAD= capability must stay Other(\"THREAD=\"), got {cap:?}"
);
}
#[test]
fn capability_quota_resource_empty_suffix_is_other() {
let (_, cap) = capability(b"QUOTA=RES-").unwrap();
assert!(
matches!(cap, Capability::Other(ref s) if s == "QUOTA=RES-"),
"malformed QUOTA=RES- capability must stay Other(\"QUOTA=RES-\"), got {cap:?}"
);
}
#[test]
fn capability_rights_empty_suffix_is_other() {
let (_, cap) = capability(b"RIGHTS=").unwrap();
assert!(
matches!(cap, Capability::Other(ref s) if s == "RIGHTS="),
"malformed RIGHTS= capability must stay Other(\"RIGHTS=\"), got {cap:?}"
);
}
#[test]
fn capability_unknown() {
let (_, cap) = capability(b"XYZZY").unwrap();
assert_eq!(cap, Capability::Other("XYZZY".into()));
}
#[test]
fn response_code_uidvalidity() {
let (_, code) = response_code(b"[UIDVALIDITY 12345]").unwrap();
assert_eq!(code, ResponseCode::UidValidity(12345));
}
#[test]
fn response_code_permanentflags() {
let (_, code) = response_code(b"[PERMANENTFLAGS (\\Seen \\Flagged \\*)]").unwrap();
if let ResponseCode::PermanentFlags(flags) = &code {
assert_eq!(flags.len(), 3);
} else {
panic!("expected PermanentFlags, got {code:?}");
}
}
#[test]
fn response_code_appenduid() {
let (_, code) = response_code(b"[APPENDUID 1234 5678]").unwrap();
assert_eq!(
code,
ResponseCode::AppendUid {
uid_validity: 1234,
uids: vec![UidRange::single(5678)],
}
);
}
#[test]
fn response_code_appenduid_set() {
let (_, code) = response_code(b"[APPENDUID 1234 100:102]").unwrap();
assert_eq!(
code,
ResponseCode::AppendUid {
uid_validity: 1234,
uids: vec![UidRange {
start: 100,
end: Some(102)
}],
}
);
}
#[test]
fn response_code_appenduid_uidvalidity_zero() {
let (_, code) = response_code(b"[APPENDUID 0 42]").expect(
"APPENDUID with uidvalidity 0 must parse per Postel's law, \
consistent with UIDVALIDITY response code tolerance",
);
assert_eq!(
code,
ResponseCode::AppendUid {
uid_validity: 0,
uids: vec![UidRange::single(42)],
}
);
}
#[test]
fn response_code_copyuid_uidvalidity_zero() {
let (_, code) = response_code(b"[COPYUID 0 1 2]").expect(
"COPYUID with uidvalidity 0 must parse per Postel's law, \
consistent with UIDVALIDITY response code tolerance",
);
if let ResponseCode::CopyUid {
uid_validity,
source_uids,
dest_uids,
} = &code
{
assert_eq!(*uid_validity, 0);
assert_eq!(source_uids.len(), 1);
assert_eq!(dest_uids.len(), 1);
} else {
panic!("expected CopyUid, got {code:?}");
}
}
#[test]
fn response_code_copyuid() {
let (_, code) = response_code(b"[COPYUID 1234 1:5 10:14]").unwrap();
if let ResponseCode::CopyUid {
uid_validity,
source_uids,
dest_uids,
} = &code
{
assert_eq!(*uid_validity, 1234);
assert_eq!(source_uids.len(), 1);
assert_eq!(source_uids[0], UidRange::range(1, 5));
assert_eq!(dest_uids.len(), 1);
} else {
panic!("expected CopyUid");
}
}
#[test]
fn response_code_highestmodseq() {
let (_, code) = response_code(b"[HIGHESTMODSEQ 99999]").unwrap();
assert_eq!(code, ResponseCode::HighestModSeq(99999));
}
#[test]
fn response_code_rfc5530() {
let (_, code) = response_code(b"[UNAVAILABLE]").unwrap();
assert_eq!(code, ResponseCode::Unavailable);
let (_, code) = response_code(b"[NOPERM]").unwrap();
assert_eq!(code, ResponseCode::NoPerm);
}
#[test]
fn response_code_unknown() {
let (_, code) = response_code(b"[XFOO bar baz]").unwrap();
assert_eq!(
code,
ResponseCode::Other {
name: "XFOO".into(),
value: Some("bar baz".into()),
}
);
}
#[test]
fn uid_range_single() {
let (_, r) = uid_range(b"42").unwrap();
assert_eq!(r, UidRange::single(42));
}
#[test]
fn uid_range_pair() {
let (_, r) = uid_range(b"1:100").unwrap();
assert_eq!(r, UidRange::range(1, 100));
}
#[test]
fn uid_set_multiple() {
let (_, set) = uid_set(b"1:5,10,20:30").unwrap();
assert_eq!(set.len(), 3);
}
#[test]
fn greeting_ok() {
let (_, resp) = parse_greeting(b"* OK Dovecot ready.\r\n").unwrap();
if let Response::Greeting(g) = resp {
assert_eq!(g.status, GreetingStatus::Ok);
assert_eq!(g.text, "Dovecot ready.");
} else {
panic!("expected Greeting");
}
}
#[test]
fn greeting_preauth() {
let (_, resp) = parse_greeting(b"* PREAUTH already authed\r\n").unwrap();
if let Response::Greeting(g) = resp {
assert_eq!(g.status, GreetingStatus::PreAuth);
} else {
panic!("expected Greeting");
}
}
#[test]
fn greeting_with_capability() {
let (_, resp) = parse_greeting(b"* OK [CAPABILITY IMAP4rev1 IDLE] ready\r\n").unwrap();
if let Response::Greeting(g) = resp {
assert_eq!(g.status, GreetingStatus::Ok);
if let Some(ResponseCode::Capability(caps)) = &g.code {
assert!(caps.contains(&Capability::Imap4Rev1));
assert!(caps.contains(&Capability::Idle));
} else {
panic!("expected Capability code, got {:?}", g.code);
}
} else {
panic!("expected Greeting");
}
}
#[test]
fn greeting_bare_ok_no_text() {
let input = b"* OK\r\n";
let (_, resp) = parse_greeting(input).expect(
"bare '* OK\\r\\n' greeting should parse (Postel's law, consistent with parse_tagged)",
);
match resp {
Response::Greeting(g) => {
assert_eq!(g.status, GreetingStatus::Ok);
assert!(g.code.is_none());
assert!(g.text.is_empty());
}
other => panic!("expected Greeting, got {other:?}"),
}
}
#[test]
fn greeting_bare_bye_no_text() {
let input = b"* BYE\r\n";
let (_, resp) = parse_greeting(input).expect("bare '* BYE\\r\\n' greeting should parse");
match resp {
Response::Greeting(g) => {
assert_eq!(g.status, GreetingStatus::Bye);
assert!(g.text.is_empty());
}
other => panic!("expected Greeting, got {other:?}"),
}
}
#[test]
fn greeting_ok_with_code_no_space() {
let input = b"* OK[CAPABILITY IMAP4REV1]\r\n";
let result = parse_greeting(input);
let _ = result;
}
#[test]
fn tagged_ok() {
let (_, resp) = parse_response(b"A001 OK done\r\n").unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.tag, "A001");
assert_eq!(t.status, StatusKind::Ok);
assert_eq!(t.text, "done");
} else {
panic!("expected Tagged");
}
}
#[test]
fn tagged_no_with_code() {
let (_, resp) = parse_response(b"A002 NO [NOPERM] not allowed\r\n").unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.status, StatusKind::No);
assert_eq!(t.code, Some(ResponseCode::NoPerm));
} else {
panic!("expected Tagged");
}
}
#[test]
fn tagged_bad() {
let (_, resp) = parse_response(b"A003 BAD syntax error\r\n").unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.status, StatusKind::Bad);
} else {
panic!("expected Tagged");
}
}
#[test]
fn continuation() {
let (_, resp) = parse_response(b"+ go ahead\r\n").unwrap();
if let Response::Continuation(c) = resp {
assert_eq!(c.data, "go ahead");
} else {
panic!("expected Continuation");
}
}
#[test]
fn untagged_exists() {
let (_, resp) = parse_response(b"* 23 EXISTS\r\n").unwrap();
if let Response::Untagged(u) = resp {
assert_eq!(*u, UntaggedResponse::Exists(23));
} else {
panic!("expected Untagged");
}
}
#[test]
fn untagged_recent() {
let (_, resp) = parse_response(b"* 5 RECENT\r\n").unwrap();
if let Response::Untagged(u) = resp {
assert_eq!(*u, UntaggedResponse::Recent(5));
} else {
panic!("expected Untagged");
}
}
#[test]
fn untagged_expunge() {
let (_, resp) = parse_response(b"* 3 EXPUNGE\r\n").unwrap();
if let Response::Untagged(u) = resp {
assert_eq!(*u, UntaggedResponse::Expunge(3));
} else {
panic!("expected Untagged");
}
}
#[test]
fn untagged_capability() {
let (_, resp) = parse_response(b"* CAPABILITY IMAP4rev1 IDLE LITERAL+\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Capability(caps) = &*u {
assert!(caps.contains(&Capability::Imap4Rev1));
assert!(caps.contains(&Capability::Idle));
assert!(caps.contains(&Capability::LiteralPlus));
} else {
panic!("expected Capability");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn untagged_flags() {
let (_, resp) =
parse_response(b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Flags(flags) = &*u {
assert_eq!(flags.len(), 5);
} else {
panic!("expected Flags");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn untagged_list() {
let (_, resp) = parse_response(b"* LIST (\\HasNoChildren) \"/\" \"INBOX\"\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = &*u {
assert_eq!(info.name.as_str(), "INBOX");
assert_eq!(info.delimiter, Some('/'));
assert!(info.attributes.contains(&MailboxAttribute::HasNoChildren));
} else {
panic!("expected List");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn untagged_list_nil_delimiter() {
let (_, resp) = parse_response(b"* LIST () NIL \"INBOX\"\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = &*u {
assert_eq!(info.delimiter, None);
} else {
panic!("expected List");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn untagged_list_noinferiors() {
let (_, resp) = parse_response(b"* LIST (\\Noinferiors) \"/\" \"Leaf\"\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = &*u {
assert_eq!(info.name.as_str(), "Leaf");
assert!(info.attributes.contains(&MailboxAttribute::NoInferiors));
} else {
panic!("expected List");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn untagged_list_special_use() {
let (_, resp) = parse_response(b"* LIST (\\Sent \\HasNoChildren) \".\" \"Sent\"\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = &*u {
assert!(info.attributes.contains(&MailboxAttribute::Sent));
} else {
panic!("expected List");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn untagged_search() {
let (_, resp) = parse_response(b"* SEARCH 1 5 10\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Search { uids, mod_seq } = &*u {
assert_eq!(uids, &[1, 5, 10]);
assert!(mod_seq.is_none());
} else {
panic!("expected Search");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn untagged_search_empty() {
let (_, resp) = parse_response(b"* SEARCH\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Search { uids, .. } = &*u {
assert!(uids.is_empty());
} else {
panic!("expected Search");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn search_trailing_space_empty() {
let (_, resp) = parse_response(b"* SEARCH \r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Search { uids, mod_seq } = &*u {
assert!(
uids.is_empty(),
"trailing space should not produce phantom results"
);
assert_eq!(*mod_seq, None);
} else {
panic!("expected Search, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn search_filters_uid_zero() {
let (_, resp) = parse_response(b"* SEARCH 5 0 10\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Search { uids, .. } = &*u {
assert_eq!(
*uids,
vec![5, 10],
"UID 0 must be filtered from SEARCH results"
);
} else {
panic!("expected Search, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn sort_filters_uid_zero() {
let (_, resp) = parse_response(b"* SORT 3 0 7\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Sort { nums, .. } = &*u {
assert_eq!(
*nums,
vec![3, 7],
"UID 0 must be filtered from SORT results"
);
} else {
panic!("expected Sort, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn search_trailing_spaces_with_results() {
let (_, resp) = parse_response(b"* SEARCH 5 10 15 \r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Search { uids, .. } = &*u {
assert_eq!(*uids, vec![5, 10, 15]);
} else {
panic!("expected Search, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn search_trailing_space_followed_by_tagged() {
let (rest, resp) = parse_response(b"* SEARCH \r\nA001 OK Success\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Search { uids, .. } = &*u {
assert!(uids.is_empty());
} else {
panic!("expected Search");
}
} else {
panic!("expected Untagged");
}
assert_eq!(rest, b"A001 OK Success\r\n");
}
#[test]
fn sort_trailing_space_empty() {
let (_, resp) = parse_response(b"* SORT \r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Sort { nums, mod_seq } = &*u {
assert!(
nums.is_empty(),
"trailing space should not produce phantom results"
);
assert_eq!(*mod_seq, None);
} else {
panic!("expected Sort, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn sort_trailing_space_with_results() {
let (_, resp) = parse_response(b"* SORT 3 1 2 \r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Sort { nums, mod_seq } = &*u {
assert_eq!(*nums, vec![3, 1, 2]);
assert_eq!(*mod_seq, None);
} else {
panic!("expected Sort, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn enabled_trailing_space_empty() {
let (_, resp) = parse_response(b"* ENABLED \r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Enabled(caps) = &*u {
assert!(caps.is_empty());
} else {
panic!("expected Enabled, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn enabled_trailing_space_with_caps() {
let (_, resp) = parse_response(b"* ENABLED CONDSTORE QRESYNC \r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Enabled(caps) = &*u {
assert_eq!(caps, &["CONDSTORE", "QRESYNC"]);
} else {
panic!("expected Enabled, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn capability_trailing_space() {
let (_, resp) = parse_response(b"* CAPABILITY IMAP4rev1 IDLE \r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Capability(caps) = &*u {
assert!(
caps.len() >= 2,
"should parse capabilities despite trailing space"
);
} else {
panic!("expected Capability, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn flags_trailing_space() {
let (_, resp) = parse_response(b"* FLAGS (\\Seen \\Answered) \r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Flags(flags) = &*u {
assert_eq!(flags.len(), 2);
} else {
panic!("expected Flags, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn list_trailing_space() {
let (_, resp) = parse_response(b"* LIST (\\HasNoChildren) \"/\" INBOX \r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = &*u {
assert_eq!(info.name.as_str(), "INBOX");
} else {
panic!("expected List, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn lsub_trailing_space() {
let (_, resp) = parse_response(b"* LSUB () \"/\" INBOX \r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Lsub(info) = &*u {
assert_eq!(info.name.as_str(), "INBOX");
} else {
panic!("expected Lsub, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn vanished_trailing_space() {
let (_, resp) = parse_response(b"* VANISHED 1:5 \r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Vanished { earlier, uids } = &*u {
assert!(!earlier);
assert!(!uids.is_empty());
} else {
panic!("expected Vanished, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn esearch_with_all() {
let input = b"* ESEARCH (TAG \"A001\") UID ALL 1:3,5\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(esearch.tag.as_deref(), Some("A001"));
assert!(esearch.uid);
assert_eq!(
esearch.all,
vec![UidRange::range(1, 3), UidRange::single(5)]
);
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn esearch_with_min_max_count() {
let input = b"* ESEARCH (TAG \"A002\") UID MIN 1 MAX 100 COUNT 5\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(esearch.tag.as_deref(), Some("A002"));
assert!(esearch.uid);
assert_eq!(esearch.min, Some(1));
assert_eq!(esearch.max, Some(100));
assert_eq!(esearch.count, Some(5));
assert!(esearch.all.is_empty());
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn esearch_empty() {
let input = b"* ESEARCH (TAG \"A003\")\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(esearch.tag.as_deref(), Some("A003"));
assert!(!esearch.uid);
assert!(esearch.all.is_empty());
assert_eq!(esearch.min, None);
assert_eq!(esearch.max, None);
assert_eq!(esearch.count, None);
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn esearch_all_fields_together() {
let input = b"* ESEARCH (TAG \"A004\") UID MIN 2 MAX 50 COUNT 10 ALL 2:5,10,20:50\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(esearch.tag.as_deref(), Some("A004"));
assert!(esearch.uid);
assert_eq!(esearch.min, Some(2));
assert_eq!(esearch.max, Some(50));
assert_eq!(esearch.count, Some(10));
assert_eq!(
esearch.all,
vec![
UidRange::range(2, 5),
UidRange::single(10),
UidRange::range(20, 50),
]
);
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn esearch_no_uid_indicator() {
let input = b"* ESEARCH (TAG \"A005\") COUNT 3 ALL 1,5,10\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(esearch.tag.as_deref(), Some("A005"));
assert!(!esearch.uid);
assert_eq!(esearch.count, Some(3));
assert_eq!(
esearch.all,
vec![
UidRange::single(1),
UidRange::single(5),
UidRange::single(10),
]
);
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn esearch_no_tag() {
let input = b"* ESEARCH UID COUNT 0\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert!(esearch.tag.is_none());
assert!(esearch.uid);
assert_eq!(esearch.count, Some(0));
assert!(esearch.all.is_empty());
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn esearch_only_min() {
let input = b"* ESEARCH (TAG \"A1\") UID MIN 5\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(esearch.min, Some(5));
assert_eq!(esearch.max, None);
assert_eq!(esearch.count, None);
assert!(esearch.all.is_empty());
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn esearch_only_max() {
let input = b"* ESEARCH (TAG \"A1\") UID MAX 99\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(esearch.min, None);
assert_eq!(esearch.max, Some(99));
assert_eq!(esearch.count, None);
assert!(esearch.all.is_empty());
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn esearch_only_count() {
let input = b"* ESEARCH (TAG \"A1\") UID COUNT 0\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(esearch.min, None);
assert_eq!(esearch.max, None);
assert_eq!(esearch.count, Some(0));
assert!(esearch.all.is_empty());
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn esearch_all_single_uid() {
let input = b"* ESEARCH (TAG \"A1\") UID ALL 42\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(esearch.all, vec![UidRange::single(42)]);
assert_eq!(esearch.min, None);
assert_eq!(esearch.max, None);
assert_eq!(esearch.count, None);
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn esearch_all_and_count_no_minmax() {
let input = b"* ESEARCH (TAG \"A1\") UID COUNT 3 ALL 1,5,10\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(esearch.count, Some(3));
assert_eq!(
esearch.all,
vec![
UidRange::single(1),
UidRange::single(5),
UidRange::single(10),
]
);
assert_eq!(esearch.min, None);
assert_eq!(esearch.max, None);
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn esearch_keywords_case_insensitive() {
let input = b"* ESEARCH (TAG \"A1\") uid min 1 Max 100 count 5 all 1:3\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert!(esearch.uid);
assert_eq!(esearch.min, Some(1));
assert_eq!(esearch.max, Some(100));
assert_eq!(esearch.count, Some(5));
assert_eq!(esearch.all, vec![UidRange::range(1, 3)]);
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn untagged_status_ok_with_code() {
let (_, resp) = parse_response(b"* OK [UIDVALIDITY 3857529045] UIDs valid\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Status { status, code, text } = &*u {
assert_eq!(*status, UntaggedStatus::Ok);
assert_eq!(
code.as_ref().unwrap(),
&ResponseCode::UidValidity(3_857_529_045)
);
assert_eq!(text, "UIDs valid");
} else {
panic!("expected Status");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn untagged_bye() {
let (_, resp) = parse_response(b"* BYE server shutting down\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Status { status, .. } = &*u {
assert_eq!(*status, UntaggedStatus::Bye);
} else {
panic!("expected Status");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn rfc2047_base64() {
let result = decode_rfc2047(b"=?UTF-8?B?SGVsbG8gV29ybGQ=?=");
assert_eq!(result, "Hello World");
}
#[test]
fn rfc2047_quoted_printable() {
let result = decode_rfc2047(b"=?UTF-8?Q?Hello_World?=");
assert_eq!(result, "Hello World");
}
#[test]
fn rfc2047_iso8859() {
let result = decode_rfc2047(b"=?ISO-8859-1?Q?caf=E9?=");
assert_eq!(result, "caf\u{e9}");
}
#[test]
fn rfc2047_consecutive() {
let result = decode_rfc2047(b"=?UTF-8?Q?Hello?= =?UTF-8?Q?_World?=");
assert_eq!(result, "Hello World");
}
#[test]
fn rfc2047_mixed() {
let result = decode_rfc2047(b"Re: =?UTF-8?B?SGVsbG8=?= there");
assert_eq!(result, "Re: Hello there");
}
#[test]
fn rfc2047_plain_text() {
let result = decode_rfc2047(b"no encoding here");
assert_eq!(result, "no encoding here");
}
#[test]
fn rfc2047_garbage() {
let result = decode_rfc2047(b"=?broken");
assert_eq!(result, "=?broken");
}
#[test]
fn spec_audit_rfc2047_with_rfc2231_language_tag() {
let input = b"=?UTF-8*EN?Q?Hello_World?=";
let result = decode_rfc2047(input);
assert_eq!(
result, "Hello World",
"RFC 2231 language tag in charset must be stripped"
);
}
#[test]
fn spec_audit_rfc2047_with_rfc2231_language_tag_iso8859() {
let input = b"=?ISO-8859-1*DE?Q?Gr=FC=DFe?=";
let result = decode_rfc2047(input);
assert_eq!(
result, "Grüße",
"RFC 2231 language tag must work with non-UTF-8 charsets"
);
}
#[test]
fn spec_audit_rfc2047_with_rfc2231_language_tag_b_encoding() {
let input = b"=?UTF-8*EN?B?SGVsbG8=?=";
let result = decode_rfc2047(input);
assert_eq!(
result, "Hello",
"RFC 2231 language tag must work with B encoding"
);
}
#[test]
fn regression_rfc2047_bom_not_stripped() {
let input = b"=?UTF-16BE?B?/v8AaABlAGwAbABv?=";
let result = decode_rfc2047(input);
assert_eq!(
result, "\u{FEFF}hello",
"RFC 2047 Section 2: encoded words are header fragments, not standalone \
documents — a leading U+FEFF must be preserved as content, not stripped as BOM"
);
}
#[test]
fn spec_audit_rfc2047_unknown_encoding_preserved_verbatim() {
let input = b"=?UTF-8?X?hello?= world";
let result = decode_rfc2047(input);
assert_eq!(
result, "=?UTF-8?X?hello?= world",
"Unknown encoding 'X' must preserve entire encoded word verbatim (RFC 2047 Section 6.3)"
);
}
#[test]
fn spec_audit_rfc2047_invalid_base64_preserved_verbatim() {
let input = b"=?UTF-8?B?invalid base64!!!?= tail";
let result = decode_rfc2047(input);
assert_eq!(
result, "=?UTF-8?B?invalid base64!!!?= tail",
"Failed base64 decode must preserve entire encoded word verbatim (RFC 2047 Section 6.3)"
);
}
#[test]
fn rfc2047_whitespace_preserved_before_invalid_encoded_word() {
let input = b"=?UTF-8?Q?Hello?= =?UTF-8?X?fail?= end";
let result = decode_rfc2047(input);
assert_eq!(
result, "Hello =?UTF-8?X?fail?= end",
"Whitespace before an invalid encoded word must be preserved (RFC 2047 Section 6.3)"
);
}
#[test]
fn rfc2047_requires_lwsp_boundaries_in_text() {
let input = b"Prefix=?UTF-8?Q?caf=C3=A9?=Suffix";
let result = decode_rfc2047(input);
assert_eq!(
result, "Prefix=?UTF-8?Q?caf=C3=A9?=Suffix",
"RFC 2047 Section 5: glued encoded-words must not be decoded"
);
}
#[test]
fn imap_001_overlong_encoded_word_decoded_per_postels_law() {
use base64::Engine;
let long_text =
"Hello World! This is a long subject line for testing overlong encoded words in IMAP";
let b64 = base64::engine::general_purpose::STANDARD.encode(long_text.as_bytes());
let encoded_word = format!("=?UTF-8?B?{b64}?=");
assert!(
encoded_word.len() > 75,
"test setup: encoded word must exceed 75 chars, got {}",
encoded_word.len()
);
let result = decode_rfc2047(encoded_word.as_bytes());
assert_eq!(
result, long_text,
"Postel's law: overlong encoded words must be decoded, not preserved verbatim"
);
}
#[test]
fn imap_001_overlong_q_encoded_word_decoded() {
let input = b"=?UTF-8?Q?AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA?=";
assert!(input.len() > 75, "test setup: must exceed 75 chars");
let result = decode_rfc2047(input);
assert_eq!(
result, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"Postel's law: overlong Q-encoded words must be decoded"
);
}
#[test]
fn imap_002_empty_encoded_text_base64_decoded() {
let input = b"=?UTF-8?B??=";
let result = decode_rfc2047(input);
assert_eq!(
result, "",
"Postel's law: empty base64 encoded-text must decode to empty string"
);
}
#[test]
fn imap_002_empty_encoded_text_q_decoded() {
let input = b"=?UTF-8?Q??=";
let result = decode_rfc2047(input);
assert_eq!(
result, "",
"Postel's law: empty Q-encoded text must decode to empty string"
);
}
#[test]
fn address_simple() {
let (_, addr) = address(b"(\"Alice\" NIL \"alice\" \"example.com\")", false).unwrap();
assert_eq!(addr.name.as_deref(), Some("Alice"));
assert_eq!(addr.mailbox.as_deref(), Some("alice"));
assert_eq!(addr.host.as_deref(), Some("example.com"));
}
#[test]
fn address_all_nil() {
let (_, addr) = address(b"(NIL NIL NIL NIL)", false).unwrap();
assert!(addr.name.is_none());
assert!(addr.mailbox.is_none());
assert!(addr.host.is_none());
}
#[test]
fn address_rfc2047_name() {
let (_, addr) = address(
b"(\"=?UTF-8?B?QWxpY2U=?=\" NIL \"alice\" \"example.com\")",
false,
)
.unwrap();
assert_eq!(addr.name.as_deref(), Some("Alice"));
}
#[test]
fn address_list_nil() {
let (_, list) = address_list(b"NIL", false).unwrap();
assert!(list.is_empty());
}
#[test]
fn address_list_multi() {
let (_, list) = address_list(
b"((\"A\" NIL \"a\" \"x.com\")(\"B\" NIL \"b\" \"y.com\"))",
false,
)
.unwrap();
assert_eq!(list.len(), 2);
}
#[test]
fn envelope_full() {
let input = b"(\"Mon, 7 Feb 2022 21:52:25 -0800\" \"Test Subject\" \
((\"Sender\" NIL \"sender\" \"example.com\")) \
((\"Sender\" NIL \"sender\" \"example.com\")) \
((\"Sender\" NIL \"sender\" \"example.com\")) \
((\"Recipient\" NIL \"rcpt\" \"example.com\")) \
NIL NIL NIL \
\"<msg-id@example.com>\")";
let (_, env) = envelope(input, false).unwrap();
assert_eq!(env.subject.as_deref(), Some("Test Subject"));
assert_eq!(env.from.len(), 1);
assert_eq!(env.to.len(), 1);
assert_eq!(env.message_id.as_deref(), Some("<msg-id@example.com>"));
}
#[test]
fn envelope_all_nil() {
let input = b"(NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)";
let (_, env) = envelope(input, false).unwrap();
assert!(env.date.is_none());
assert!(env.subject.is_none());
assert!(env.from.is_empty());
}
#[test]
fn envelope_nil_sender_reply_to_preserved() {
let input = b"(\"Mon, 7 Feb 2022 21:52:25 -0800\" \"Test\" \
((\"Alice\" NIL \"alice\" \"example.com\")) \
NIL \
NIL \
((\"Bob\" NIL \"bob\" \"example.com\")) \
NIL NIL NIL \
\"<msg@example.com>\")";
let (_, env) = envelope(input, false).unwrap();
assert_eq!(env.from.len(), 1);
assert_eq!(
env.sender, env.from,
"sender must default to from when NIL (RFC 3501 Section 7.4.2)"
);
assert_eq!(
env.reply_to, env.from,
"reply_to must default to from when NIL (RFC 3501 Section 7.4.2)"
);
}
#[test]
fn envelope_preserves_nil_sender_reply_to() {
let input = b"* 1 FETCH (ENVELOPE (\"Mon, 1 Jan 2024 00:00:00 +0000\" \"Test\" ((\"Alice\" NIL \"alice\" \"ex.com\")) NIL NIL ((\"Bob\" NIL \"bob\" \"ex.com\")) NIL NIL NIL \"<msg@ex.com>\"))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Fetch(f) = &*u {
let env = f.envelope.as_ref().unwrap();
assert_eq!(env.from.len(), 1, "from should have one address");
assert_eq!(
env.sender, env.from,
"sender must default to from when NIL (RFC 3501 Section 7.4.2); got {:?}",
env.sender
);
assert_eq!(
env.reply_to, env.from,
"reply_to must default to from when NIL (RFC 3501 Section 7.4.2); got {:?}",
env.reply_to
);
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn envelope_explicit_sender_reply_to_preserved() {
let input = b"(\"Mon, 7 Feb 2022 21:52:25 -0800\" \"Test\" \
((\"Alice\" NIL \"alice\" \"example.com\")) \
((\"Secretary\" NIL \"secretary\" \"example.com\")) \
((\"ReplyAddr\" NIL \"reply\" \"example.com\")) \
((\"Bob\" NIL \"bob\" \"example.com\")) \
NIL NIL NIL \
\"<msg@example.com>\")";
let (_, env) = envelope(input, false).unwrap();
assert_eq!(env.sender.len(), 1);
assert_eq!(env.sender[0].mailbox.as_deref(), Some("secretary"));
assert_eq!(env.reply_to.len(), 1);
assert_eq!(env.reply_to[0].mailbox.as_deref(), Some("reply"));
}
#[test]
fn fetch_uid_flags() {
let input = b"(UID 42 FLAGS (\\Seen \\Flagged))";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.uid, Some(42));
let flags = fr.flags.unwrap();
assert!(flags.contains(&Flag::Seen));
assert!(flags.contains(&Flag::Flagged));
}
#[test]
fn fetch_rfc822_size() {
let input = b"(RFC822.SIZE 1234)";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.rfc822_size, Some(1234));
}
#[test]
fn fetch_with_envelope() {
let input = b"(UID 10 ENVELOPE (NIL \"Hello\" \
((\"Test\" NIL \"test\" \"example.com\")) \
NIL NIL NIL NIL NIL NIL \
\"<msg@example.com>\"))";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.uid, Some(10));
let env = fr.envelope.unwrap();
assert_eq!(env.subject.as_deref(), Some("Hello"));
}
#[test]
fn fetch_modseq() {
let input = b"(UID 5 MODSEQ (12345))";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.mod_seq, Some(12345));
}
#[test]
fn fetch_body_section() {
let input = b"(BODY[] \"hello\")";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].section, "");
assert_eq!(fr.body_sections[0].data.as_deref(), Some(b"hello".as_ref()));
}
#[test]
fn full_fetch_response() {
let input = b"* 1 FETCH (UID 100 FLAGS (\\Seen) RFC822.SIZE 500)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Fetch(fr) = &*u {
assert_eq!(fr.seq, 1);
assert_eq!(fr.uid, Some(100));
assert_eq!(fr.rfc822_size, Some(500));
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn body_structure_text_plain() {
let input = b"(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5)";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Text {
media_subtype,
params,
encoding,
size,
lines,
..
} = &bs
{
assert_eq!(media_subtype, "plain");
assert_eq!(params, &[("charset".into(), "UTF-8".into())]);
assert_eq!(encoding, "7bit");
assert_eq!(*size, 100);
assert_eq!(*lines, 5);
} else {
panic!("expected Text, got {bs:?}");
}
}
#[test]
fn body_structure_rfc2231_params_end_to_end() {
let input = b"(\"APPLICATION\" \"PDF\" (\"FILENAME*0*\" \"UTF-8''long%20\" \"FILENAME*1\" \"name.pdf\") NIL NIL \"BASE64\" 12345)";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Basic { params, .. } = &bs {
assert_eq!(params.len(), 1);
assert_eq!(params[0].0, "filename");
assert_eq!(params[0].1, "long name.pdf");
} else {
panic!("expected Basic, got {bs:?}");
}
}
#[test]
fn body_structure_basic_image() {
let input = b"(\"IMAGE\" \"PNG\" NIL NIL NIL \"BASE64\" 2048)";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Basic {
media_type,
media_subtype,
size,
..
} = &bs
{
assert_eq!(media_type, "image");
assert_eq!(media_subtype, "png");
assert_eq!(*size, 2048);
} else {
panic!("expected Basic, got {bs:?}");
}
}
#[test]
fn body_structure_multipart() {
let input = b"((\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 100 5)\
(\"TEXT\" \"HTML\" NIL NIL NIL \"QUOTED-PRINTABLE\" 200 10) \
\"ALTERNATIVE\")";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Multipart {
media_subtype,
bodies,
..
} = &bs
{
assert_eq!(media_subtype, "alternative");
assert_eq!(bodies.len(), 2);
} else {
panic!("expected Multipart, got {bs:?}");
}
}
#[test]
fn body_structure_multipart_whitespace_between_children() {
let input = b"((\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 100 5) (\"TEXT\" \"HTML\" NIL NIL NIL \"QUOTED-PRINTABLE\" 200 10) \"ALTERNATIVE\")";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Multipart {
bodies,
media_subtype,
..
} = bs
{
assert_eq!(
bodies.len(),
2,
"both child bodies must be parsed despite whitespace between them"
);
assert_eq!(media_subtype, "alternative");
} else {
panic!("expected Multipart");
}
}
#[test]
fn body_structure_with_disposition() {
let input = b"(\"APPLICATION\" \"PDF\" NIL NIL NIL \"BASE64\" 4096 \
NIL (\"ATTACHMENT\" (\"FILENAME\" \"report.pdf\")) NIL NIL)";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Basic { disposition, .. } = &bs {
let disp = disposition.as_ref().expect("expected disposition");
assert_eq!(disp.disposition_type, "attachment");
assert_eq!(disp.params, vec![("filename".into(), "report.pdf".into())]);
} else {
panic!("expected Basic, got {bs:?}");
}
}
#[test]
fn body_params_nil() {
let (_, params) = body_params(b"NIL").unwrap();
assert!(params.is_empty());
}
#[test]
fn body_params_pairs() {
let (_, params) = body_params(b"(\"CHARSET\" \"UTF-8\" \"NAME\" \"test.txt\")").unwrap();
assert_eq!(params.len(), 2);
assert_eq!(params[0], ("charset".into(), "UTF-8".into()));
}
#[test]
fn body_params_rfc2231_charset_encoded() {
let input = b"(\"CHARSET\" \"UTF-8\" \"NAME*\" \"UTF-8''t%C3%A9st%20file.txt\")";
let (_, params) = body_params(input).unwrap();
assert_eq!(params.len(), 2);
assert_eq!(params[0], ("charset".into(), "UTF-8".into()));
assert_eq!(params[1].0, "name");
assert_eq!(params[1].1, "t\u{e9}st file.txt");
}
#[test]
fn body_params_rfc2231_continuation() {
let input = b"(\"FILENAME*0\" \"very-long-\" \"FILENAME*1\" \"filename.txt\")";
let (_, params) = body_params(input).unwrap();
assert_eq!(params.len(), 1);
assert_eq!(
params[0],
("filename".into(), "very-long-filename.txt".into())
);
}
#[test]
fn body_params_rfc2231_charset_and_continuation() {
let input = b"(\"FILENAME*0*\" \"UTF-8''%C3%A9l%C3%A8ve-\" \"FILENAME*1\" \"rapport.pdf\")";
let (_, params) = body_params(input).unwrap();
assert_eq!(params.len(), 1);
assert_eq!(params[0].0, "filename");
assert_eq!(params[0].1, "\u{e9}l\u{e8}ve-rapport.pdf");
}
#[test]
fn body_disposition_rfc2231_charset_encoded() {
let input = b"(\"attachment\" (\"FILENAME*\" \"UTF-8''r%C3%A9sum%C3%A9.pdf\"))";
let (_, disp) = body_disposition(input).unwrap();
let disp = disp.expect("disposition should not be NIL");
assert_eq!(disp.disposition_type, "attachment");
assert_eq!(disp.params.len(), 1);
assert_eq!(disp.params[0].0, "filename");
assert_eq!(disp.params[0].1, "r\u{e9}sum\u{e9}.pdf");
}
#[test]
fn body_params_rfc2231_iso8859() {
let input = b"(\"FILENAME*\" \"ISO-8859-1''caf%E9.txt\")";
let (_, params) = body_params(input).unwrap();
assert_eq!(params.len(), 1);
assert_eq!(params[0].0, "filename");
assert_eq!(params[0].1, "caf\u{e9}.txt");
}
#[test]
fn body_structure_with_rfc2231_disposition() {
let input = b"(\"APPLICATION\" \"PDF\" (\"NAME\" \"file.pdf\") NIL NIL \"BASE64\" 12345 NIL (\"attachment\" (\"FILENAME*\" \"UTF-8''t%C3%A9st.pdf\")) NIL NIL)";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Basic { disposition, .. } = bs {
let disp = disposition.expect("disposition should not be NIL");
assert_eq!(disp.disposition_type, "attachment");
assert_eq!(disp.params[0].0, "filename");
assert_eq!(disp.params[0].1, "t\u{e9}st.pdf");
} else {
panic!("expected Basic body structure");
}
}
#[test]
fn untagged_enabled() {
let input = b"ENABLED CONDSTORE UTF8=ACCEPT\r\n";
let (_, resp) = parse_untagged_enabled(input).unwrap();
if let UntaggedResponse::Enabled(caps) = resp {
assert_eq!(caps, vec!["CONDSTORE", "UTF8=ACCEPT"]);
} else {
panic!("expected Enabled, got {resp:?}");
}
}
#[test]
fn untagged_enabled_empty() {
let input = b"ENABLED\r\n";
let (_, resp) = parse_untagged_enabled(input).unwrap();
if let UntaggedResponse::Enabled(caps) = resp {
assert!(caps.is_empty());
} else {
panic!("expected Enabled, got {resp:?}");
}
}
#[test]
fn untagged_vanished_earlier() {
let input = b"VANISHED (EARLIER) 1:5,10\r\n";
let (_, resp) = parse_untagged_vanished(input).unwrap();
if let UntaggedResponse::Vanished { earlier, uids } = resp {
assert!(earlier);
assert_eq!(uids.len(), 2);
assert_eq!(uids[0], UidRange::range(1, 5));
assert_eq!(uids[1], UidRange::single(10));
} else {
panic!("expected Vanished, got {resp:?}");
}
}
#[test]
fn untagged_vanished_no_earlier() {
let input = b"VANISHED 42\r\n";
let (_, resp) = parse_untagged_vanished(input).unwrap();
if let UntaggedResponse::Vanished { earlier, uids } = resp {
assert!(!earlier);
assert_eq!(uids, vec![UidRange::single(42)]);
} else {
panic!("expected Vanished, got {resp:?}");
}
}
#[test]
fn untagged_id_with_pairs() {
let input = b"ID (\"name\" \"Dovecot\" \"version\" \"2.3.16\")\r\n";
let (_, resp) = parse_untagged_id(input).unwrap();
if let UntaggedResponse::Id(params) = resp {
assert_eq!(params.len(), 2);
assert_eq!(params[0].0, "name");
assert_eq!(params[0].1, Some("Dovecot".to_owned()));
assert_eq!(params[1].0, "version");
assert_eq!(params[1].1, Some("2.3.16".to_owned()));
} else {
panic!("expected Id, got {resp:?}");
}
}
#[test]
fn untagged_id_nil() {
let input = b"ID NIL\r\n";
let (_, resp) = parse_untagged_id(input).unwrap();
if let UntaggedResponse::Id(params) = resp {
assert!(params.is_empty());
} else {
panic!("expected Id, got {resp:?}");
}
}
#[test]
fn untagged_namespace_full() {
let input = b"NAMESPACE ((\"\" \"/\")) ((\"~\" \"/\")) ((\"#shared.\" \".\"))\r\n";
let (_, resp) = parse_untagged_namespace(input, false).unwrap();
if let UntaggedResponse::Namespace {
personal,
other,
shared,
} = resp
{
assert_eq!(personal.len(), 1);
assert_eq!(personal[0].prefix, "");
assert_eq!(personal[0].delimiter, Some('/'));
assert_eq!(other.len(), 1);
assert_eq!(other[0].prefix, "~");
assert_eq!(shared.len(), 1);
assert_eq!(shared[0].prefix, "#shared.");
assert_eq!(shared[0].delimiter, Some('.'));
} else {
panic!("expected Namespace, got {resp:?}");
}
}
#[test]
fn untagged_namespace_all_nil() {
let input = b"NAMESPACE NIL NIL NIL\r\n";
let (_, resp) = parse_untagged_namespace(input, false).unwrap();
if let UntaggedResponse::Namespace {
personal,
other,
shared,
} = resp
{
assert!(personal.is_empty());
assert!(other.is_empty());
assert!(shared.is_empty());
} else {
panic!("expected Namespace, got {resp:?}");
}
}
#[test]
fn untagged_status_mailbox() {
let input = b"STATUS \"INBOX\" (MESSAGES 17 UNSEEN 5 UIDNEXT 100 UIDVALIDITY 1234)\r\n";
let (_, resp) = parse_untagged_status_mailbox(input, false).unwrap();
if let UntaggedResponse::MailboxStatus { mailbox, items } = resp {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(items.len(), 4);
assert_eq!(items[0], StatusItem::Messages(17));
assert_eq!(items[1], StatusItem::Unseen(5));
assert_eq!(items[2], StatusItem::UidNext(100));
assert_eq!(items[3], StatusItem::UidValidity(1234));
} else {
panic!("expected MailboxStatus, got {resp:?}");
}
}
#[test]
fn status_item_highestmodseq() {
let (_, items) = status_items(b"HIGHESTMODSEQ 99999").unwrap();
assert_eq!(items, vec![StatusItem::HighestModSeq(99999)]);
}
#[test]
fn status_item_size() {
let (_, items) = status_items(b"SIZE 1048576").unwrap();
assert_eq!(items, vec![StatusItem::Size(1_048_576)]);
}
#[test]
fn status_item_unknown_skipped() {
let (_, items) = status_items(b"BOGUS 42").unwrap();
assert!(items.is_empty(), "unknown attribute should be skipped");
}
#[test]
fn response_code_alert() {
let (_, code) = response_code(b"[ALERT]").unwrap();
assert_eq!(code, ResponseCode::Alert);
}
#[test]
fn response_code_read_only() {
let (_, code) = response_code(b"[READ-ONLY]").unwrap();
assert_eq!(code, ResponseCode::ReadOnly);
}
#[test]
fn response_code_read_write() {
let (_, code) = response_code(b"[READ-WRITE]").unwrap();
assert_eq!(code, ResponseCode::ReadWrite);
}
#[test]
fn response_code_trycreate() {
let (_, code) = response_code(b"[TRYCREATE]").unwrap();
assert_eq!(code, ResponseCode::TryCreate);
}
#[test]
fn response_code_parse() {
let (_, code) = response_code(b"[PARSE]").unwrap();
assert_eq!(code, ResponseCode::Parse);
}
#[test]
fn response_code_unseen() {
let (_, code) = response_code(b"[UNSEEN 17]").unwrap();
assert_eq!(code, ResponseCode::Unseen(17));
}
#[test]
fn response_code_uidnext() {
let (_, code) = response_code(b"[UIDNEXT 4392]").unwrap();
assert_eq!(code, ResponseCode::UidNext(4392));
}
#[test]
fn response_code_nomodseq() {
let (_, code) = response_code(b"[NOMODSEQ]").unwrap();
assert_eq!(code, ResponseCode::NoModSeq);
}
#[test]
fn response_code_closed() {
let (_, code) = response_code(b"[CLOSED]").unwrap();
assert_eq!(code, ResponseCode::Closed);
}
#[test]
fn response_code_modified() {
let (_, code) = response_code(b"[MODIFIED 1:5,10]").unwrap();
if let ResponseCode::Modified(uids) = code {
assert_eq!(uids.len(), 2);
assert_eq!(uids[0], UidRange::range(1, 5));
assert_eq!(uids[1], UidRange::single(10));
} else {
panic!("expected Modified, got {code:?}");
}
}
#[test]
fn response_code_modified_accepts_star_range() {
let (_, code) = response_code(b"[MODIFIED 1:*]").unwrap();
if let ResponseCode::Modified(ranges) = code {
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].start, 1);
assert_eq!(ranges[0].end, Some(u32::MAX));
} else {
panic!("expected Modified, got {code:?}");
}
}
#[test]
fn response_code_modified_accepts_bare_star() {
let (_, code) = response_code(b"[MODIFIED *]").unwrap();
if let ResponseCode::Modified(ranges) = code {
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].start, u32::MAX);
assert_eq!(ranges[0].end, None);
} else {
panic!("expected Modified, got {code:?}");
}
}
#[test]
fn response_code_modified_star_mixed_no_star() {
let (_, code) = response_code(b"[MODIFIED 1,5:20,10]").unwrap();
if let ResponseCode::Modified(ranges) = code {
assert_eq!(ranges.len(), 3);
assert_eq!(ranges[0], UidRange::single(1));
assert_eq!(ranges[1], UidRange::range(5, 20));
assert_eq!(ranges[2], UidRange::single(10));
} else {
panic!("expected Modified, got {code:?}");
}
}
#[test]
fn response_code_badcharset() {
let (_, code) = response_code(b"[BADCHARSET (\"UTF-8\" \"US-ASCII\")]").unwrap();
if let ResponseCode::BadCharset(charsets) = code {
assert_eq!(charsets, vec!["UTF-8", "US-ASCII"]);
} else {
panic!("expected BadCharset, got {code:?}");
}
}
#[test]
fn response_code_badcharset_empty() {
let (_, code) = response_code(b"[BADCHARSET]").unwrap();
if let ResponseCode::BadCharset(charsets) = code {
assert!(charsets.is_empty());
} else {
panic!("expected BadCharset, got {code:?}");
}
}
#[test]
fn spec_audit_badcharset_empty_parens_accepted() {
let (_, code) = response_code(b"[BADCHARSET ()]").unwrap();
if let ResponseCode::BadCharset(charsets) = code {
assert!(
charsets.is_empty(),
"BADCHARSET () should produce empty charset vec"
);
} else {
panic!("expected BadCharset, got {code:?}");
}
}
#[test]
fn response_code_capability() {
let (_, code) = response_code(b"[CAPABILITY IMAP4rev1 IDLE LITERAL+]").unwrap();
if let ResponseCode::Capability(caps) = code {
assert_eq!(caps.len(), 3);
assert_eq!(caps[0], Capability::Imap4Rev1);
assert_eq!(caps[1], Capability::Idle);
assert_eq!(caps[2], Capability::LiteralPlus);
} else {
panic!("expected Capability, got {code:?}");
}
}
#[test]
fn response_code_rfc5530_all() {
let codes: Vec<(&[u8], ResponseCode)> = vec![
(b"[UNAVAILABLE]", ResponseCode::Unavailable),
(
b"[AUTHENTICATIONFAILED]",
ResponseCode::AuthenticationFailed,
),
(b"[AUTHORIZATIONFAILED]", ResponseCode::AuthorizationFailed),
(b"[EXPIRED]", ResponseCode::Expired),
(b"[PRIVACYREQUIRED]", ResponseCode::PrivacyRequired),
(b"[CONTACTADMIN]", ResponseCode::ContactAdmin),
(b"[NOPERM]", ResponseCode::NoPerm),
(b"[INUSE]", ResponseCode::InUse),
(b"[EXPUNGEISSUED]", ResponseCode::ExpungeIssued),
(b"[CORRUPTION]", ResponseCode::Corruption),
(b"[SERVERBUG]", ResponseCode::ServerBug),
(b"[CLIENTBUG]", ResponseCode::ClientBug),
(b"[CANNOT]", ResponseCode::Cannot),
(b"[LIMIT]", ResponseCode::Limit),
(b"[OVERQUOTA]", ResponseCode::OverQuota),
(b"[ALREADYEXISTS]", ResponseCode::AlreadyExists),
(b"[NONEXISTENT]", ResponseCode::NonExistent),
];
for (input, expected) in codes {
let (_, code) = response_code(input).unwrap();
assert_eq!(
code,
expected,
"failed for input {:?}",
std::str::from_utf8(input)
);
}
}
#[test]
fn response_code_registry_extensions_are_typed() {
let cases: [(&[u8], &str, &str); 8] = [
(
b"[REFERRAL imap://example.com/]",
"Referral",
"imap://example.com/",
),
(b"[URLMECH INTERNAL]", "UrlMech", "INTERNAL"),
(
b"[BADURL imap://bad.example/]",
"BadUrl",
"imap://bad.example/",
),
(
b"[BADCOMPARATOR i;ascii-casemap]",
"BadComparator",
"i;ascii-casemap",
),
(b"[ANNOTATE /comment]", "Annotate", "/comment"),
(b"[MAXCONVERTMESSAGES 25]", "MaxConvertMessages", "25"),
(b"[MAXCONVERTPARTS 10]", "MaxConvertParts", "10"),
(b"[NOUPDATE]", "NoUpdate", ""),
];
for (input, expected_variant, expected_payload) in cases {
let (_, code) = response_code(input).unwrap();
assert!(
!matches!(code, ResponseCode::Other { .. }),
"registered response code {:?} must not fall through to Other: {code:?}",
std::str::from_utf8(input)
);
let dbg = format!("{code:?}");
assert!(
dbg.contains(expected_variant),
"expected {expected_variant} variant for {:?}, got {dbg}",
std::str::from_utf8(input)
);
if !expected_payload.is_empty() {
assert!(
dbg.contains(expected_payload),
"expected payload {expected_payload:?} to be preserved for {:?}, got {dbg}",
std::str::from_utf8(input)
);
}
}
}
#[test]
fn mailbox_attribute_all_base() {
assert_eq!(
parse_mailbox_attribute("\\Noinferiors"),
MailboxAttribute::NoInferiors
);
assert_eq!(
parse_mailbox_attribute("\\Noselect"),
MailboxAttribute::NoSelect
);
assert_eq!(
parse_mailbox_attribute("\\NonExistent"),
MailboxAttribute::NonExistent
);
assert_eq!(
parse_mailbox_attribute("\\HasChildren"),
MailboxAttribute::HasChildren
);
assert_eq!(
parse_mailbox_attribute("\\HasNoChildren"),
MailboxAttribute::HasNoChildren
);
assert_eq!(
parse_mailbox_attribute("\\Marked"),
MailboxAttribute::Marked
);
assert_eq!(
parse_mailbox_attribute("\\Unmarked"),
MailboxAttribute::Unmarked
);
assert_eq!(
parse_mailbox_attribute("\\Subscribed"),
MailboxAttribute::Subscribed
);
assert_eq!(
parse_mailbox_attribute("\\Remote"),
MailboxAttribute::Remote
);
}
#[test]
fn mailbox_attribute_special_use() {
assert_eq!(parse_mailbox_attribute("\\All"), MailboxAttribute::All);
assert_eq!(
parse_mailbox_attribute("\\Archive"),
MailboxAttribute::Archive
);
assert_eq!(
parse_mailbox_attribute("\\Drafts"),
MailboxAttribute::Drafts
);
assert_eq!(
parse_mailbox_attribute("\\Flagged"),
MailboxAttribute::Flagged
);
assert_eq!(parse_mailbox_attribute("\\Junk"), MailboxAttribute::Junk);
assert_eq!(parse_mailbox_attribute("\\Sent"), MailboxAttribute::Sent);
assert_eq!(parse_mailbox_attribute("\\Trash"), MailboxAttribute::Trash);
assert_eq!(
parse_mailbox_attribute("\\Important"),
MailboxAttribute::Important
);
}
#[test]
fn mailbox_attribute_case_insensitive() {
assert_eq!(
parse_mailbox_attribute("\\NOINFERIORS"),
MailboxAttribute::NoInferiors
);
assert_eq!(
parse_mailbox_attribute("\\NOSELECT"),
MailboxAttribute::NoSelect
);
assert_eq!(
parse_mailbox_attribute("\\haschildren"),
MailboxAttribute::HasChildren
);
assert_eq!(parse_mailbox_attribute("\\SENT"), MailboxAttribute::Sent);
}
#[test]
fn mailbox_attribute_custom() {
let attr = parse_mailbox_attribute("\\MyCustom");
assert_eq!(attr, MailboxAttribute::Custom("\\MyCustom".to_owned()));
}
#[test]
fn fetch_internaldate() {
let input = b"(UID 42 INTERNALDATE \"17-Jul-1996 02:44:25 -0700\")\r\n";
let (_, resp) =
parse_response(b"* 1 FETCH (UID 42 INTERNALDATE \"17-Jul-1996 02:44:25 -0700\")\r\n")
.unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(42));
assert_eq!(
fr.internal_date.as_deref(),
Some("17-Jul-1996 02:44:25 -0700")
);
} else {
panic!("expected Fetch, got {boxed:?}");
}
} else {
panic!("expected Untagged, got {resp:?}");
}
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(
fr.internal_date.as_deref(),
Some("17-Jul-1996 02:44:25 -0700")
);
}
#[test]
fn fetch_internaldate_nil_accepted() {
let input = b"* 5 FETCH (UID 100 FLAGS (\\Seen) INTERNALDATE NIL)\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Fetch(fr) => {
assert_eq!(fr.uid, Some(100));
assert_eq!(fr.flags.as_ref().map(std::vec::Vec::len), Some(1));
assert_eq!(
fr.internal_date, None,
"INTERNALDATE NIL must parse as None, not fail the FETCH response"
);
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn fetch_internaldate_nil_preserves_other_attrs() {
let input = b"* 1 FETCH (INTERNALDATE NIL UID 42 RFC822.SIZE 1024)\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Fetch(fr) => {
assert_eq!(fr.internal_date, None);
assert_eq!(fr.uid, Some(42));
assert_eq!(fr.rfc822_size, Some(1024));
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn fetch_body_header() {
let input = b"(BODY[HEADER] \"Subject: Test\\r\\n\\r\\n\")";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].section, "HEADER");
assert!(fr.body_sections[0].data.is_some());
}
#[test]
fn fetch_body_text() {
let input = b"(BODY[TEXT] \"message body here\")";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].section, "TEXT");
assert_eq!(
fr.body_sections[0].data.as_deref(),
Some(b"message body here".as_ref())
);
}
#[test]
fn fetch_body_header_fields() {
let input = b"(BODY[HEADER.FIELDS (Subject From)] \"Subject: Hi\\r\\nFrom: a@b\\r\\n\\r\\n\")";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].section, "HEADER.FIELDS (Subject From)");
}
#[test]
fn fetch_body_part_number() {
let input = b"(BODY[1.2] \"part data\")";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].section, "1.2");
assert_eq!(
fr.body_sections[0].data.as_deref(),
Some(b"part data".as_ref())
);
}
#[test]
fn fetch_body_partial_origin() {
let input = b"(BODY[]<0> \"first 100 bytes\")";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].section, "");
assert_eq!(fr.body_sections[0].origin, Some(0));
}
#[test]
fn fetch_body_nil_data() {
let input = b"(BODY[TEXT] NIL)";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].section, "TEXT");
assert!(fr.body_sections[0].data.is_none());
}
#[test]
fn fetch_multiple_body_sections() {
let input = b"(BODY[HEADER] \"hdr\" BODY[TEXT] \"body\")";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.body_sections.len(), 2);
assert_eq!(fr.body_sections[0].section, "HEADER");
assert_eq!(fr.body_sections[1].section, "TEXT");
}
#[test]
fn fetch_body_without_section_is_bodystructure() {
let input = b"(BODY (\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5))";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert!(fr.body_structure.is_some());
}
#[test]
fn fetch_body_with_literal() {
let input = b"(BODY[] {5}\r\nhello)";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].data.as_deref(), Some(b"hello".as_ref()));
}
#[test]
fn fetch_all_items_combined() {
let input = b"(UID 99 FLAGS (\\Seen) RFC822.SIZE 2048 \
INTERNALDATE \"01-Jan-2024 00:00:00 +0000\" \
BODY[HEADER] \"From: test\\r\\n\")";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.uid, Some(99));
assert_eq!(fr.rfc822_size, Some(2048));
assert_eq!(
fr.internal_date.as_deref(),
Some("01-Jan-2024 00:00:00 +0000")
);
assert_eq!(fr.body_sections.len(), 1);
let flags = fr.flags.unwrap();
assert!(flags.contains(&Flag::Seen));
}
#[test]
fn fetch_unknown_attribute_skipped() {
let input = b"(UID 1 FUTUREATTR \"value\" FLAGS (\\Seen))";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.uid, Some(1));
let flags = fr.flags.unwrap();
assert!(flags.contains(&Flag::Seen));
}
#[test]
fn fetch_binary_simple_section() {
let input = b"(BINARY[1] \"binary data\")";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.binary_sections.len(), 1);
assert_eq!(fr.binary_sections[0].section, vec![1]);
assert_eq!(fr.binary_sections[0].origin, None);
assert_eq!(
fr.binary_sections[0].data.as_deref(),
Some(b"binary data".as_ref())
);
}
#[test]
fn fetch_binary_nested_section() {
let input = b"(BINARY[1.2.3] {5}\r\nhello)";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.binary_sections.len(), 1);
assert_eq!(fr.binary_sections[0].section, vec![1, 2, 3]);
assert_eq!(fr.binary_sections[0].origin, None);
assert_eq!(
fr.binary_sections[0].data.as_deref(),
Some(b"hello".as_ref())
);
}
#[test]
fn fetch_binary_with_origin() {
let input = b"(BINARY[2]<0> \"partial\")";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.binary_sections.len(), 1);
assert_eq!(fr.binary_sections[0].section, vec![2]);
assert_eq!(fr.binary_sections[0].origin, Some(0));
assert_eq!(
fr.binary_sections[0].data.as_deref(),
Some(b"partial".as_ref())
);
}
#[test]
fn fetch_binary_nil_data() {
let input = b"(BINARY[1] NIL)";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.binary_sections.len(), 1);
assert_eq!(fr.binary_sections[0].section, vec![1]);
assert!(fr.binary_sections[0].data.is_none());
}
#[test]
fn fetch_binary_size() {
let input = b"(BINARY.SIZE[1] 2048)";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.binary_sizes.len(), 1);
assert_eq!(fr.binary_sizes[0], (vec![1], 2048));
}
#[test]
fn fetch_binary_size_nested() {
let input = b"(BINARY.SIZE[1.2] 512)";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.binary_sizes.len(), 1);
assert_eq!(fr.binary_sizes[0], (vec![1, 2], 512));
}
#[test]
fn fetch_binary_mixed_with_other_attrs() {
let input = b"(UID 42 BINARY[1] \"data\" BINARY.SIZE[2] 1024 FLAGS (\\Seen))";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.uid, Some(42));
assert_eq!(fr.binary_sections.len(), 1);
assert_eq!(fr.binary_sections[0].section, vec![1]);
assert_eq!(
fr.binary_sections[0].data.as_deref(),
Some(b"data".as_ref())
);
assert_eq!(fr.binary_sizes.len(), 1);
assert_eq!(fr.binary_sizes[0], (vec![2], 1024));
let flags = fr.flags.unwrap();
assert!(flags.contains(&Flag::Seen));
}
#[test]
fn fetch_multiple_binary_sections() {
let input = b"(BINARY[1] \"part1\" BINARY[2] \"part2\")";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.binary_sections.len(), 2);
assert_eq!(fr.binary_sections[0].section, vec![1]);
assert_eq!(
fr.binary_sections[0].data.as_deref(),
Some(b"part1".as_ref())
);
assert_eq!(fr.binary_sections[1].section, vec![2]);
assert_eq!(
fr.binary_sections[1].data.as_deref(),
Some(b"part2".as_ref())
);
}
#[test]
fn fetch_binary_empty_section() {
let input = b"(BINARY[] \"all\")";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.binary_sections.len(), 1);
assert!(
fr.binary_sections[0].section.is_empty(),
"RFC 9051: empty section-part must produce empty vec"
);
assert_eq!(fr.binary_sections[0].data.as_deref(), Some(b"all".as_ref()));
}
#[test]
fn fetch_binary_size_empty_section() {
let input = b"(BINARY.SIZE[] 4096)";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.binary_sizes.len(), 1);
let (ref section, size) = fr.binary_sizes[0];
assert!(section.is_empty(), "RFC 9051: empty section-part");
assert_eq!(size, 4096);
}
#[test]
fn binary_size_number64() {
let input = b"(BINARY.SIZE[1] 5000000000)";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.binary_sizes.len(), 1);
assert_eq!(fr.binary_sizes[0], (vec![1], 5_000_000_000u64));
}
#[test]
fn parse_fetch_binary_empty_section_rfc9051() {
let input = b"* 1 FETCH (BINARY[] ~{5}\r\nhello)\r\n";
let (rem, resp) = parse_response(input).unwrap();
assert!(rem.is_empty());
if let Response::Untagged(inner) = resp {
if let UntaggedResponse::Fetch(fetch) = *inner {
assert_eq!(fetch.binary_sections.len(), 1);
assert!(
fetch.binary_sections[0].section.is_empty(),
"RFC 9051: empty section-part must produce empty vec"
);
assert_eq!(
fetch.binary_sections[0].data.as_deref(),
Some(b"hello".as_slice())
);
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn parse_fetch_binary_size_empty_section_rfc9051() {
let input = b"* 1 FETCH (BINARY.SIZE[] 4096)\r\n";
let (rem, resp) = parse_response(input).unwrap();
assert!(rem.is_empty());
if let Response::Untagged(inner) = resp {
if let UntaggedResponse::Fetch(fetch) = *inner {
assert_eq!(fetch.binary_sizes.len(), 1);
let (ref section, size) = fetch.binary_sizes[0];
assert!(section.is_empty(), "RFC 9051: empty section-part");
assert_eq!(size, 4096);
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn quoted_string_rejects_nul() {
assert!(quoted_string(b"\"abc\x00def\"").is_err());
}
#[test]
fn quoted_string_rejects_bare_cr() {
assert!(quoted_string(b"\"abc\rdef\"").is_err());
}
#[test]
fn quoted_string_rejects_bare_lf() {
assert!(quoted_string(b"\"abc\ndef\"").is_err());
}
#[test]
fn quoted_string_rejects_escaped_cr() {
assert!(quoted_string(b"\"test\\\rmore\"").is_err());
}
#[test]
fn quoted_string_rejects_escaped_lf() {
assert!(quoted_string(b"\"test\\\nmore\"").is_err());
}
#[test]
fn quoted_string_rejects_nul_after_backslash() {
assert!(quoted_string(b"\"hello\\\x00world\"").is_err());
}
#[test]
fn quoted_string_unterminated() {
let result = quoted_string(b"\"hello");
assert!(matches!(result, Err(nom::Err::Error(_))));
}
#[test]
fn quoted_string_unterminated_escape() {
let result = quoted_string(b"\"hello\\");
assert!(matches!(result, Err(nom::Err::Error(_))));
}
#[test]
fn literal_truncated_data() {
let result = literal(b"{10}\r\nhello");
assert!(result.is_err());
}
#[test]
fn literal_missing_crlf() {
let result = literal(b"{5}hello");
assert!(result.is_err());
}
#[test]
fn literal_overflow_count() {
let result = literal(b"{99999999999999999999}\r\n");
assert!(result.is_err());
}
#[test]
fn literal_count_exceeds_i64_max() {
let result = literal(b"{9223372036854775808}\r\n");
assert!(result.is_err(), "count > i64::MAX must be rejected");
}
#[test]
fn literal_count_at_i64_max() {
let result = literal(b"{9223372036854775807}\r\n");
assert!(
result.is_err(),
"should fail from insufficient data, not range check"
);
}
#[test]
fn number_rejects_non_digit() {
assert!(number(b"abc").is_err());
}
#[test]
fn number_rejects_empty() {
assert!(number(b"").is_err());
}
#[test]
fn number_rejects_negative() {
assert!(number(b"-1").is_err());
}
#[test]
fn parse_response_garbage() {
assert!(parse_response(b"!!GARBAGE!!\r\n").is_err());
}
#[test]
fn parse_response_empty() {
assert!(parse_response(b"").is_err());
}
#[test]
fn parse_response_only_crlf() {
assert!(parse_response(b"\r\n").is_err());
}
#[test]
fn parse_response_truncated_tagged() {
assert!(parse_response(b"A001 OK done").is_err());
}
#[test]
fn parse_response_invalid_status() {
assert!(parse_response(b"A001 INVALID text\r\n").is_err());
}
#[test]
fn greeting_rejects_non_greeting() {
assert!(parse_greeting(b"* FETCH 1\r\n").is_err());
}
#[test]
fn untagged_fetch_missing_closing_paren() {
let result = parse_response(b"* 1 FETCH (UID 42\r\n");
match result {
Ok((_, Response::Untagged(boxed))) => {
assert!(
matches!(*boxed, UntaggedResponse::Unknown(_)),
"Malformed FETCH should fall through to Unknown, got {boxed:?}"
);
}
Ok((_, other)) => panic!("Expected Untagged(Unknown), got {other:?}"),
Err(_) => { }
}
}
#[test]
fn quoted_string_incomplete_does_not_block_alt_fallthrough() {
let input = b"* 1 FETCH (ENVELOPE (\"unterminated subject))\r\n";
let result = parse_response(input);
match result {
Ok((_, Response::Untagged(boxed))) => {
assert!(
matches!(*boxed, UntaggedResponse::Unknown(_)),
"Truncated quoted string in FETCH should fall through to Unknown, got {boxed:?}"
);
}
Ok((_, other)) => panic!("Expected Untagged(Unknown), got {other:?}"),
Err(nom::Err::Incomplete(_)) => {
panic!("BUG: quoted_string returned Incomplete in complete mode, blocking alt() fallthrough");
}
Err(_) => {
panic!("BUG: parse failed instead of falling through to Unknown");
}
}
}
#[test]
fn scan_section_spec_incomplete_does_not_block_alt_fallthrough() {
let input = b"* 1 FETCH (BODY[HEADER no-close\r\n";
let result = parse_response(input);
match result {
Ok((_, Response::Untagged(boxed))) => {
assert!(
matches!(*boxed, UntaggedResponse::Unknown(_)),
"Unterminated BODY section should fall through to Unknown, got {boxed:?}"
);
}
Ok((_, other)) => panic!("Expected Untagged(Unknown), got {other:?}"),
Err(nom::Err::Incomplete(_)) => {
panic!("BUG: scan_section_spec returned Incomplete in complete mode, blocking alt() fallthrough");
}
Err(_) => {
panic!("BUG: parse failed instead of falling through to Unknown");
}
}
}
#[test]
fn untagged_status_bare_ok_no_text() {
let input = b"* OK\r\n";
let result = parse_response(input);
match result {
Ok((_, Response::Untagged(boxed))) => match *boxed {
UntaggedResponse::Status { status, code, text } => {
assert_eq!(status, UntaggedStatus::Ok);
assert!(code.is_none());
assert!(text.is_empty());
}
other => panic!("Expected Status, got {other:?}"),
},
Ok((_, other)) => panic!("Expected Untagged(Status), got {other:?}"),
Err(e) => {
panic!("BUG: bare `* OK\\r\\n` should parse like bare tagged status; got error: {e:?}")
}
}
}
#[test]
fn untagged_status_bare_bye_no_text() {
let input = b"* BYE\r\n";
let result = parse_response(input);
match result {
Ok((_, Response::Untagged(boxed))) => match *boxed {
UntaggedResponse::Status { status, .. } => {
assert_eq!(status, UntaggedStatus::Bye);
}
other => panic!("Expected Status, got {other:?}"),
},
Ok((_, other)) => panic!("Expected Untagged(Status), got {other:?}"),
Err(e) => panic!("BUG: bare `* BYE\\r\\n` should parse; got error: {e:?}"),
}
}
#[test]
fn skip_paren_group_incomplete_does_not_block_alt_fallthrough() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"text\" \"plain\" NIL NIL NIL \"7bit\" 42 3 NIL NIL NIL NIL (unclosed-ext\r\n";
let result = parse_response(input);
match result {
Ok((_, Response::Untagged(boxed))) => {
assert!(
matches!(*boxed, UntaggedResponse::Unknown(_)),
"Unclosed paren in BODYSTRUCTURE should fall through to Unknown, got {boxed:?}"
);
}
Ok((_, other)) => panic!("Expected Untagged(Unknown), got {other:?}"),
Err(nom::Err::Incomplete(_)) => {
panic!("BUG: skip_paren_group returned Incomplete in complete mode, blocking alt() fallthrough");
}
Err(_) => {
panic!("BUG: parse failed instead of falling through to Unknown");
}
}
}
#[test]
fn nstring_garbage() {
assert!(nstring(b"GARBAGE").is_err());
}
#[test]
fn flag_list_mismatched_paren() {
assert!(flag_list(b"(\\Seen").is_err());
}
#[test]
fn envelope_truncated() {
let result = envelope(b"(\"date\" \"subject\"", false);
assert!(result.is_err());
}
#[test]
fn body_structure_truncated() {
let result = body_structure(b"(\"TEXT\" \"PLAIN\" NIL NIL NIL", false, 0);
assert!(result.is_err());
}
#[test]
fn high_byte_in_atom_accepted() {
let (_, val) = atom(b"\xC0\xC1\xC2 rest").unwrap();
assert_eq!(val, b"\xC0\xC1\xC2");
}
#[test]
fn response_code_case_insensitive() {
let (_, code) = response_code(b"[alert]").unwrap();
assert_eq!(code, ResponseCode::Alert);
let (_, code) = response_code(b"[read-only]").unwrap();
assert_eq!(code, ResponseCode::ReadOnly);
}
#[test]
fn skip_unknown_fetch_attribute() {
let (_, resp) =
parse_response(b"* 1 FETCH (UID 10 X-CUSTOM \"value\" FLAGS (\\Seen))\r\n").unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(10));
assert_eq!(fr.flags, Some(vec![Flag::Seen]));
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn skip_unknown_response_code() {
let (_, code) = response_code(b"[XYZFUTURE 42]").unwrap();
if let ResponseCode::Other { name, value } = code {
assert_eq!(name, "XYZFUTURE");
assert_eq!(value.as_deref(), Some("42"));
} else {
panic!("expected Other, got {code:?}");
}
}
#[test]
fn continuation_empty_text() {
let (_, cont) = parse_continuation(b"+ \r\n").unwrap();
assert_eq!(cont.data, "");
}
#[test]
fn continuation_with_response_code() {
let (_, cont) = parse_continuation(b"+ [ALERT] Please continue\r\n").unwrap();
assert_eq!(cont.code, Some(ResponseCode::Alert));
assert_eq!(cont.data, "Please continue");
}
#[test]
fn continuation_with_response_code_no_text() {
let (_, cont) = parse_continuation(b"+ [ALERT]\r\n").unwrap();
assert_eq!(cont.code, Some(ResponseCode::Alert));
assert_eq!(cont.data, "");
}
#[test]
fn continuation_base64_no_code() {
let (_, cont) = parse_continuation(b"+ dGVzdA==\r\n").unwrap();
assert_eq!(cont.code, None);
assert_eq!(cont.data, "dGVzdA==");
}
#[test]
fn continuation_plain_text_no_code() {
let (_, cont) = parse_continuation(b"+ go ahead\r\n").unwrap();
assert_eq!(cont.code, None);
assert_eq!(cont.data, "go ahead");
}
#[test]
fn untagged_list_with_all_attributes() {
let input = b"LIST (\\HasChildren \\Subscribed) \".\" \"INBOX\"\r\n";
let (_, resp) = parse_untagged_list(input, false).unwrap();
if let UntaggedResponse::List(info) = resp {
assert_eq!(info.name.as_str(), "INBOX");
assert_eq!(info.delimiter, Some('.'));
assert!(info.attributes.contains(&MailboxAttribute::HasChildren));
assert!(info.attributes.contains(&MailboxAttribute::Subscribed));
} else {
panic!("expected List, got {resp:?}");
}
}
#[test]
fn esearch_unknown_key_skipped() {
let input = b"ESEARCH (TAG \"A001\") UID ALL 1:3 XFUTURE foo\r\n";
let (_, resp) = parse_untagged_esearch(input).unwrap();
if let UntaggedResponse::Esearch(esearch) = resp {
assert_eq!(esearch.all, vec![UidRange::range(1, 3)]);
} else {
panic!("expected Esearch, got {resp:?}");
}
}
#[test]
fn namespace_with_extension_data() {
let input = b"NAMESPACE ((\"\" \"/\" \"X-EXT\" (\"val\"))) NIL NIL\r\n";
let (_, resp) = parse_untagged_namespace(input, false).unwrap();
if let UntaggedResponse::Namespace { personal, .. } = resp {
assert_eq!(personal.len(), 1);
assert_eq!(personal[0].prefix, "");
assert_eq!(personal[0].delimiter, Some('/'));
assert_eq!(
personal[0].extensions,
vec![("X-EXT".to_string(), vec!["val".to_string()])]
);
} else {
panic!("expected Namespace, got {resp:?}");
}
}
#[test]
fn lsub_basic() {
let input = b"* LSUB () \"/\" \"INBOX\"\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Lsub(info) = *u {
assert_eq!(info.name.as_str(), "INBOX");
assert_eq!(info.delimiter, Some('/'));
assert!(info.attributes.is_empty());
} else {
panic!("expected Lsub, got {u:?}");
}
} else {
panic!("expected Untagged, got {resp:?}");
}
}
#[test]
fn lsub_with_attributes() {
let input = b"* LSUB (\\NoSelect) \".\" \"Archive\"\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Lsub(info) = *u {
assert_eq!(info.name.as_str(), "Archive");
assert_eq!(info.delimiter, Some('.'));
assert!(info.attributes.contains(&MailboxAttribute::NoSelect));
} else {
panic!("expected Lsub, got {u:?}");
}
} else {
panic!("expected Untagged, got {resp:?}");
}
}
#[test]
fn lsub_preserves_extended_data() {
let input = b"* LSUB () \"/\" \"NewName\" (\"OLDNAME\" (\"OldName\"))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Lsub(info) = *u {
assert_eq!(info.name.as_str(), "NewName");
assert_eq!(
info.old_name.as_ref().map(MailboxName::as_str),
Some("OldName"),
"LSUB must parse OLDNAME extended data like LIST \
(RFC 3501 Section 7.2.3, RFC 5258 Section 6)"
);
} else {
panic!("expected Lsub, got {u:?}");
}
} else {
panic!("expected Untagged, got {resp:?}");
}
}
#[test]
fn lsub_preserves_childinfo() {
let input = b"* LSUB () \".\" \"Parent\" (\"CHILDINFO\" (\"SUBSCRIBED\"))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Lsub(info) = *u {
assert_eq!(info.name.as_str(), "Parent");
assert!(
info.child_info.contains(&"SUBSCRIBED".to_string()),
"LSUB must parse CHILDINFO extended data like LIST \
(RFC 3501 Section 7.2.3, RFC 5258 Section 4)"
);
} else {
panic!("expected Lsub, got {u:?}");
}
} else {
panic!("expected Untagged, got {resp:?}");
}
}
#[test]
fn status_messages_unseen() {
let input = b"STATUS \"INBOX\" (MESSAGES 17 UNSEEN 2)\r\n";
let (_, resp) = parse_untagged_status_mailbox(input, false).unwrap();
if let UntaggedResponse::MailboxStatus { mailbox, items } = resp {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(items.len(), 2);
assert!(items.contains(&StatusItem::Messages(17)));
assert!(items.contains(&StatusItem::Unseen(2)));
} else {
panic!("expected MailboxStatus, got {resp:?}");
}
}
#[test]
fn status_uidnext_uidvalidity() {
let input = b"STATUS \"INBOX\" (UIDNEXT 4392 UIDVALIDITY 3857529045)\r\n";
let (_, resp) = parse_untagged_status_mailbox(input, false).unwrap();
if let UntaggedResponse::MailboxStatus { items, .. } = resp {
assert!(items.contains(&StatusItem::UidNext(4392)));
assert!(items.contains(&StatusItem::UidValidity(3_857_529_045)));
} else {
panic!("expected MailboxStatus, got {resp:?}");
}
}
#[test]
fn status_recent() {
let input = b"STATUS \"INBOX\" (RECENT 5)\r\n";
let (_, resp) = parse_untagged_status_mailbox(input, false).unwrap();
if let UntaggedResponse::MailboxStatus { items, .. } = resp {
assert_eq!(items.len(), 1);
assert!(items.contains(&StatusItem::Recent(5)));
} else {
panic!("expected MailboxStatus, got {resp:?}");
}
}
#[test]
fn status_all_items() {
let input = b"STATUS \"INBOX\" (MESSAGES 10 RECENT 3 UNSEEN 2 UIDNEXT 100 UIDVALIDITY 1)\r\n";
let (_, resp) = parse_untagged_status_mailbox(input, false).unwrap();
if let UntaggedResponse::MailboxStatus { items, .. } = resp {
assert_eq!(items.len(), 5);
assert!(items.contains(&StatusItem::Messages(10)));
assert!(items.contains(&StatusItem::Recent(3)));
assert!(items.contains(&StatusItem::Unseen(2)));
assert!(items.contains(&StatusItem::UidNext(100)));
assert!(items.contains(&StatusItem::UidValidity(1)));
} else {
panic!("expected MailboxStatus, got {resp:?}");
}
}
#[test]
fn status_unknown_attr_nil_prefix_atom() {
let input = b"STATUS \"INBOX\" (MESSAGES 10 XHASH NILSIMSA UNSEEN 3)\r\n";
let (_, resp) = parse_untagged_status_mailbox(input, false).unwrap();
if let UntaggedResponse::MailboxStatus { mailbox, items } = resp {
assert_eq!(mailbox.as_str(), "INBOX");
assert!(
items.contains(&StatusItem::Messages(10)),
"MESSAGES 10 missing: {items:?}"
);
assert!(
items.contains(&StatusItem::Unseen(3)),
"UNSEEN 3 missing: {items:?}"
);
} else {
panic!("expected MailboxStatus, got {resp:?}");
}
}
#[test]
fn body_structure_nested_multipart() {
let input = b"(((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5)\
(\"TEXT\" \"HTML\" (\"CHARSET\" \"UTF-8\") NIL NIL \"QUOTED-PRINTABLE\" 200 10) \
\"ALTERNATIVE\")\
(\"APPLICATION\" \"PDF\" NIL NIL NIL \"BASE64\" 5000) \
\"MIXED\")";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Multipart {
media_subtype,
bodies,
..
} = bs
{
assert_eq!(media_subtype, "mixed");
assert_eq!(bodies.len(), 2);
if let BodyStructure::Multipart {
media_subtype: inner_sub,
bodies: inner_bodies,
..
} = &bodies[0]
{
assert_eq!(inner_sub, "alternative");
assert_eq!(inner_bodies.len(), 2);
} else {
panic!("expected inner Multipart, got {:?}", bodies[0]);
}
assert!(
matches!(&bodies[1], BodyStructure::Basic { media_type, .. } if media_type.eq_ignore_ascii_case("APPLICATION"))
);
} else {
panic!("expected Multipart, got {bs:?}");
}
}
#[test]
fn body_structure_message_rfc822() {
let input = b"(\"MESSAGE\" \"RFC822\" NIL NIL NIL \"7BIT\" 500 \
(NIL \"embedded\" NIL NIL NIL NIL NIL NIL NIL NIL) \
(\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 50 3) 20)";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Message {
size,
envelope,
body,
lines,
..
} = bs
{
assert_eq!(size, 500);
assert_eq!(envelope.subject.as_deref(), Some("embedded"));
assert_eq!(lines, 20);
assert!(matches!(*body, BodyStructure::Text { .. }));
} else {
panic!("expected Message, got {bs:?}");
}
}
#[test]
fn response_code_rfc5530_unavailable() {
let input = b"A001 NO [UNAVAILABLE] Try again later\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::Unavailable));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_overquota() {
let input = b"A001 NO [OVERQUOTA] Mailbox is full\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::OverQuota));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_alreadyexists() {
let input = b"A001 NO [ALREADYEXISTS] Mailbox already exists\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::AlreadyExists));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_nonexistent() {
let input = b"A001 NO [NONEXISTENT] Mailbox does not exist\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::NonExistent));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_noperm() {
let input = b"A001 NO [NOPERM] Permission denied\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::NoPerm));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_authenticationfailed() {
let input = b"A001 NO [AUTHENTICATIONFAILED] Invalid credentials\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::AuthenticationFailed));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_expired() {
let input = b"A001 NO [EXPIRED] Credentials expired\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::Expired));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_contactadmin() {
let input = b"A001 NO [CONTACTADMIN] Contact administrator\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::ContactAdmin));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_inuse() {
let input = b"A001 NO [INUSE] Resource locked\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::InUse));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_corruption() {
let input = b"A001 NO [CORRUPTION] Data corruption detected\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::Corruption));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_serverbug() {
let input = b"A001 NO [SERVERBUG] Internal error\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::ServerBug));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_clientbug() {
let input = b"A001 BAD [CLIENTBUG] Nonsensical request\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::ClientBug));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_cannot() {
let input = b"A001 NO [CANNOT] Operation not supported\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::Cannot));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_limit() {
let input = b"A001 NO [LIMIT] Too many messages\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::Limit));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_privacyrequired() {
let input = b"A001 NO [PRIVACYREQUIRED] Encryption required\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::PrivacyRequired));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_authorizationfailed() {
let input = b"A001 NO [AUTHORIZATIONFAILED] Not authorized\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::AuthorizationFailed));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_rfc5530_expungeissued() {
let input = b"* OK [EXPUNGEISSUED] Expunge occurred\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Status { code, .. } = *u {
assert_eq!(code, Some(ResponseCode::ExpungeIssued));
} else {
panic!("expected Status, got {u:?}");
}
} else {
panic!("expected Untagged, got {resp:?}");
}
}
#[test]
fn skip_paren_group_unclosed_quote() {
let input = b"(\"unclosed)";
let result = skip_paren_group(input);
assert!(
result.is_err(),
"unclosed quote in paren group should error"
);
}
#[test]
fn skip_paren_group_trailing_escape_in_quote() {
let input = b"(\"trail\\";
let result = skip_paren_group(input);
assert!(result.is_err(), "trailing escape should error");
}
#[test]
fn skip_paren_group_backslash_crlf_in_quoted_string() {
let input = b"(\"val\\\r\n\")";
let result = skip_paren_group(input);
if let Ok((rest, _)) = result {
assert!(
!rest.is_empty(),
"skip_paren_group consumed past CRLF boundary"
);
}
}
#[test]
fn skip_paren_group_valid() {
let (rest, content) = skip_paren_group(b"(foo \"bar\") tail").unwrap();
assert_eq!(content, b"foo \"bar\"");
assert_eq!(rest, b" tail");
}
#[test]
fn fetch_savedate_quoted() {
let (_, resp) =
parse_response(b"* 1 FETCH (UID 42 SAVEDATE \"28-Dec-2023 10:30:00 +0000\")\r\n").unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(42));
assert_eq!(fr.save_date.as_deref(), Some("28-Dec-2023 10:30:00 +0000"));
return;
}
}
panic!("expected Fetch response");
}
#[test]
fn fetch_savedate_nil() {
let (_, resp) = parse_response(b"* 5 FETCH (UID 100 SAVEDATE NIL)\r\n").unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(100));
assert!(fr.save_date.is_none());
return;
}
}
panic!("expected Fetch response");
}
#[test]
fn fetch_savedate_with_internaldate() {
let (_, resp) = parse_response(
b"* 3 FETCH (UID 7 INTERNALDATE \"01-Jan-2024 00:00:00 +0000\" SAVEDATE \"15-Feb-2024 12:00:00 +0000\")\r\n",
)
.unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(7));
assert_eq!(
fr.internal_date.as_deref(),
Some("01-Jan-2024 00:00:00 +0000")
);
assert_eq!(fr.save_date.as_deref(), Some("15-Feb-2024 12:00:00 +0000"));
return;
}
}
panic!("expected Fetch response");
}
#[test]
fn fetch_savedate_case_insensitive() {
let (_, resp) =
parse_response(b"* 1 FETCH (savedate \"01-Jan-2025 00:00:00 +0000\")\r\n").unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.save_date.as_deref(), Some("01-Jan-2025 00:00:00 +0000"));
return;
}
}
panic!("expected Fetch response");
}
#[test]
fn fetch_emailid() {
let input = b"* 1 FETCH (UID 42 EMAILID (M6d99ac3275826486))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(42));
assert_eq!(fr.email_id.as_deref(), Some("M6d99ac3275826486"));
return;
}
}
panic!("expected Fetch response with EMAILID");
}
#[test]
fn fetch_threadid() {
let input = b"* 1 FETCH (UID 42 THREADID (T64b478a75b7ea9fd))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(42));
assert_eq!(fr.thread_id.as_deref(), Some("T64b478a75b7ea9fd"));
return;
}
}
panic!("expected Fetch response with THREADID");
}
#[test]
fn fetch_threadid_nil() {
let input = b"* 1 FETCH (UID 42 THREADID NIL)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(42));
assert!(fr.thread_id.is_none());
return;
}
}
panic!("expected Fetch response with THREADID NIL");
}
#[test]
fn fetch_emailid_and_threadid_combined() {
let input = b"* 5 FETCH (UID 100 FLAGS (\\Seen) EMAILID (Mabcdef1234567890) THREADID (T0987654321fedcba))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.seq, 5);
assert_eq!(fr.uid, Some(100));
assert_eq!(fr.email_id.as_deref(), Some("Mabcdef1234567890"));
assert_eq!(fr.thread_id.as_deref(), Some("T0987654321fedcba"));
assert_eq!(fr.flags, Some(vec![Flag::Seen]));
return;
}
}
panic!("expected Fetch response with EMAILID and THREADID");
}
#[test]
fn fetch_emailid_case_insensitive() {
let input = b"* 1 FETCH (emailid (Mabc123))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.email_id.as_deref(), Some("Mabc123"));
return;
}
}
panic!("expected Fetch response");
}
#[test]
fn fetch_threadid_nil_case_insensitive() {
let input = b"* 1 FETCH (threadid nil)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert!(fr.thread_id.is_none());
return;
}
}
panic!("expected Fetch response");
}
#[test]
fn fetch_emailid_nil_rejected() {
let input = b"* 1 FETCH (EMAILID NIL)\r\n";
let result = parse_response(input);
match result {
Err(_) => {} Ok((_, Response::Untagged(boxed))) => {
if let UntaggedResponse::Fetch(fr) = *boxed {
panic!(
"EMAILID NIL must not be silently accepted as FetchResponse \
per RFC 8474 Section 7, got: {fr:?}"
);
}
}
Ok((_, other)) => {
let _ = other;
}
}
}
#[test]
fn fetch_emailid_valid_objectid() {
let input = b"* 1 FETCH (EMAILID (V001))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.email_id.as_deref(), Some("V001"));
return;
}
}
panic!("expected Fetch response with EMAILID (V001)");
}
#[test]
fn response_code_mailboxid() {
let (_, code) = response_code(b"[MAILBOXID (F2212ea87-6097-4256-9d51-71c6f)]").unwrap();
assert_eq!(
code,
ResponseCode::MailboxId("F2212ea87-6097-4256-9d51-71c6f".into())
);
}
#[test]
fn response_code_mailboxid_simple() {
let (_, code) = response_code(b"[MAILBOXID (abc123)]").unwrap();
assert_eq!(code, ResponseCode::MailboxId("abc123".into()));
}
#[test]
fn status_item_mailboxid() {
let (_, items) = status_items(b"MAILBOXID (F2212ea87-6097-4256)").unwrap();
assert_eq!(
items,
vec![StatusItem::MailboxId("F2212ea87-6097-4256".into())]
);
}
#[test]
fn objectid_valid_chars() {
let (rest, val) = objectid(b"Abc_123-xyz rest").unwrap();
assert_eq!(val, b"Abc_123-xyz");
assert_eq!(rest, b" rest");
}
#[test]
fn objectid_empty_fails() {
assert!(objectid(b" rest").is_err());
}
#[test]
fn objectid_exactly_255_chars() {
let mut input: Vec<u8> = std::iter::repeat(b'A').take(255).collect();
input.push(b')'); let (rest, val) = objectid(&input).unwrap();
assert_eq!(val.len(), 255);
assert_eq!(rest, b")");
}
#[test]
fn objectid_256_chars_accepted_in_full() {
let mut input: Vec<u8> = std::iter::repeat(b'A').take(256).collect();
input.push(b')');
let (rest, val) = objectid(&input).unwrap();
assert_eq!(val.len(), 256);
assert_eq!(rest, b")");
}
#[test]
fn objectid_oversized_accepted_via_postel() {
let mut input: Vec<u8> = std::iter::repeat(b'A').take(300).collect();
input.push(b')');
let (rest, val) = objectid(&input).unwrap();
assert_eq!(
val.len(),
300,
"oversized objectid must be accepted in full"
);
assert_eq!(
rest, b")",
"rest must be the delimiter, not leftover atom bytes"
);
}
#[test]
fn fetch_emailid_oversized_objectid_parses() {
let long_id: String = "A".repeat(300);
let input = format!("* 1 FETCH (EMAILID ({long_id}))\r\n");
let (_, resp) = parse_response(input.as_bytes()).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.email_id.as_deref(), Some(long_id.as_str()));
return;
}
}
panic!("expected Fetch response with EMAILID");
}
#[test]
fn objectid_non_compliant_falls_back_to_atom() {
let (rest, val) = objectid(b"F2212ea87.6097 rest").unwrap();
assert_eq!(val, b"F2212ea87.6097");
assert_eq!(rest, b" rest");
}
#[test]
fn fetch_emailid_rfc8474_valid_objectid() {
let input = b"* 1 FETCH (EMAILID (M_abc-XYZ_123))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.email_id.as_deref(), Some("M_abc-XYZ_123"));
return;
}
}
panic!("expected Fetch response with EMAILID");
}
#[test]
fn fetch_threadid_rfc8474_valid_objectid() {
let input = b"* 1 FETCH (THREADID (T_thread-99))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.thread_id.as_deref(), Some("T_thread-99"));
return;
}
}
panic!("expected Fetch response with THREADID");
}
#[test]
fn status_mailboxid_rfc8474_valid_objectid() {
let (_, items) = status_items(b"MAILBOXID (F_box-42)").unwrap();
assert_eq!(items, vec![StatusItem::MailboxId("F_box-42".into())]);
}
#[test]
fn response_code_mailboxid_rfc8474_valid_objectid() {
let (_, code) = response_code(b"[MAILBOXID (M_id-test_123)]").unwrap();
assert_eq!(code, ResponseCode::MailboxId("M_id-test_123".into()));
}
#[test]
fn parse_metadata_response() {
let input = b"* METADATA \"INBOX\" (\"/private/comment\" \"My comment\")\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Metadata { mailbox, entries } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "/private/comment");
assert_eq!(entries[0].value.as_deref(), Some(b"My comment".as_slice()));
return;
}
}
panic!("expected Metadata response");
}
#[test]
fn parse_metadata_multiple_entries() {
let input =
b"* METADATA \"INBOX\" (\"/private/comment\" \"hello\" \"/shared/vendor/x\" \"world\")\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Metadata { mailbox, entries } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].name, "/private/comment");
assert_eq!(entries[0].value.as_deref(), Some(b"hello".as_slice()));
assert_eq!(entries[1].name, "/shared/vendor/x");
assert_eq!(entries[1].value.as_deref(), Some(b"world".as_slice()));
return;
}
}
panic!("expected Metadata response");
}
#[test]
fn parse_metadata_nil_value() {
let input = b"* METADATA \"INBOX\" (\"/private/comment\" NIL)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Metadata { mailbox, entries } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "/private/comment");
assert!(entries[0].value.is_none());
return;
}
}
panic!("expected Metadata response");
}
#[test]
fn parse_metadata_literal_value() {
let input = b"* METADATA \"INBOX\" (\"/private/comment\" {5}\r\nhello)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Metadata { mailbox, entries } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "/private/comment");
assert_eq!(entries[0].value.as_deref(), Some(b"hello".as_slice()));
return;
}
}
panic!("expected Metadata response");
}
#[test]
fn parse_metadata_empty_entries() {
let input = b"* METADATA \"INBOX\" ()\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Metadata { mailbox, entries } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert!(entries.is_empty());
return;
}
}
panic!("expected Metadata response");
}
#[test]
fn parse_metadata_binary_literal8_value() {
let input = b"* METADATA \"INBOX\" (\"/private/vendor/bin\" ~{4}\r\n\x80\x81\x82\x83)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Metadata { mailbox, entries } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "/private/vendor/bin");
assert_eq!(
entries[0].value.as_deref(),
Some(b"\x80\x81\x82\x83".as_slice())
);
return;
}
}
panic!("expected Metadata response with binary value");
}
#[test]
fn capability_metadata() {
let (_, cap) = capability(b"METADATA").unwrap();
assert_eq!(cap, Capability::Metadata);
}
#[test]
fn capability_thread_references() {
let (_, cap) = capability(b"THREAD=REFERENCES").unwrap();
assert_eq!(cap, Capability::Thread("REFERENCES".into()));
}
#[test]
fn capability_thread_orderedsubject() {
let (_, cap) = capability(b"THREAD=ORDEREDSUBJECT").unwrap();
assert_eq!(cap, Capability::Thread("ORDEREDSUBJECT".into()));
}
#[test]
fn parse_thread_simple() {
let input = b"* THREAD (1 2 3)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Thread(threads) = *boxed {
assert_eq!(threads.len(), 1);
assert_eq!(threads[0].id, Some(1));
assert_eq!(threads[0].children.len(), 1);
assert_eq!(threads[0].children[0].id, Some(2));
assert_eq!(threads[0].children[0].children.len(), 1);
assert_eq!(threads[0].children[0].children[0].id, Some(3));
return;
}
}
panic!("expected Thread response");
}
#[test]
fn parse_thread_multiple_roots() {
let input = b"* THREAD (1)(2)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Thread(threads) = *boxed {
assert_eq!(threads.len(), 2);
assert_eq!(threads[0].id, Some(1));
assert!(threads[0].children.is_empty());
assert_eq!(threads[1].id, Some(2));
assert!(threads[1].children.is_empty());
return;
}
}
panic!("expected Thread response");
}
#[test]
fn parse_thread_nested() {
let input = b"* THREAD (1 (2)(3))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Thread(threads) = *boxed {
assert_eq!(threads.len(), 1);
assert_eq!(threads[0].id, Some(1));
assert_eq!(threads[0].children.len(), 2);
assert_eq!(threads[0].children[0].id, Some(2));
assert_eq!(threads[0].children[1].id, Some(3));
return;
}
}
panic!("expected Thread response");
}
#[test]
fn parse_thread_dummy_parent() {
let input = b"* THREAD ((1)(2))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Thread(threads) = *boxed {
assert_eq!(threads.len(), 1);
assert_eq!(threads[0].id, None);
assert_eq!(threads[0].children.len(), 2);
assert_eq!(threads[0].children[0].id, Some(1));
assert_eq!(threads[0].children[1].id, Some(2));
return;
}
}
panic!("expected Thread response");
}
#[test]
fn parse_thread_empty() {
let input = b"* THREAD\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Thread(threads) = *boxed {
assert!(threads.is_empty());
return;
}
}
panic!("expected Thread response");
}
#[test]
fn parse_thread_complex() {
let input = b"* THREAD (1 (2 3)(4 5 6))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Thread(threads) = *boxed {
assert_eq!(threads.len(), 1);
assert_eq!(threads[0].id, Some(1));
assert_eq!(threads[0].children.len(), 2);
assert_eq!(threads[0].children[0].id, Some(2));
assert_eq!(threads[0].children[0].children.len(), 1);
assert_eq!(threads[0].children[0].children[0].id, Some(3));
assert_eq!(threads[0].children[1].id, Some(4));
assert_eq!(threads[0].children[1].children.len(), 1);
assert_eq!(threads[0].children[1].children[0].id, Some(5));
assert_eq!(threads[0].children[1].children[0].children.len(), 1);
assert_eq!(threads[0].children[1].children[0].children[0].id, Some(6));
return;
}
}
panic!("expected Thread response");
}
#[test]
fn parse_thread_rfc_example() {
let input = b"* THREAD (2)(3 6 (4 23)(44 7 96))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Thread(threads) = *boxed {
assert_eq!(threads.len(), 2);
assert_eq!(threads[0].id, Some(2));
assert!(threads[0].children.is_empty());
assert_eq!(threads[1].id, Some(3));
assert_eq!(threads[1].children.len(), 1);
assert_eq!(threads[1].children[0].id, Some(6));
assert_eq!(threads[1].children[0].children.len(), 2);
assert_eq!(threads[1].children[0].children[0].id, Some(4));
assert_eq!(threads[1].children[0].children[0].children.len(), 1);
assert_eq!(threads[1].children[0].children[0].children[0].id, Some(23));
assert_eq!(threads[1].children[0].children[1].id, Some(44));
assert_eq!(threads[1].children[0].children[1].children.len(), 1);
assert_eq!(threads[1].children[0].children[1].children[0].id, Some(7));
assert_eq!(
threads[1].children[0].children[1].children[0]
.children
.len(),
1
);
assert_eq!(
threads[1].children[0].children[1].children[0].children[0].id,
Some(96)
);
return;
}
}
panic!("expected Thread response");
}
#[test]
fn spec_audit_thread_dummy_parent_uses_option() {
let input = b"* THREAD ((1)(2))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Thread(threads) = *boxed {
assert_eq!(threads.len(), 1);
assert_eq!(
threads[0].id, None,
"RFC 5256 Section 4: dummy parent has no UID, expected None, got {:?}",
threads[0].id
);
assert_eq!(threads[0].children.len(), 2);
assert_eq!(threads[0].children[0].id, Some(1));
assert_eq!(threads[0].children[1].id, Some(2));
return;
}
}
panic!("expected Thread response");
}
#[test]
fn spec_audit_thread_single_child_dummy_collapsed() {
let input = b"* THREAD ((1))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Thread(threads) = *boxed {
assert_eq!(threads.len(), 1, "should have one top-level thread");
assert_eq!(
threads[0].id,
Some(1),
"RFC 5256 Section 5: single-child dummy parent must be \
collapsed — expected id=Some(1), got id={:?}",
threads[0].id
);
assert!(
threads[0].children.is_empty(),
"collapsed thread should have no children"
);
return;
}
}
panic!("expected Thread response");
}
#[test]
fn spec_audit_thread_single_child_dummy_collapsed_chain() {
let input = b"* THREAD ((1 2))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Thread(threads) = *boxed {
assert_eq!(threads.len(), 1);
assert_eq!(
threads[0].id,
Some(1),
"RFC 5256 Section 5: single-child dummy parent must be \
collapsed — expected id=Some(1), got id={:?}",
threads[0].id
);
assert_eq!(threads[0].children.len(), 1);
assert_eq!(threads[0].children[0].id, Some(2));
return;
}
}
panic!("expected Thread response");
}
#[test]
fn spec_audit_thread_multi_child_dummy_not_collapsed() {
let input = b"* THREAD ((1)(2))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Thread(threads) = *boxed {
assert_eq!(threads.len(), 1);
assert_eq!(
threads[0].id, None,
"valid dummy parent (2+ children) must keep id=None"
);
assert_eq!(threads[0].children.len(), 2);
return;
}
}
panic!("expected Thread response");
}
#[test]
fn parse_quota_single_resource() {
let input = b"* QUOTA \"\" (STORAGE 10 512)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Quota { root, resources } = *boxed {
assert_eq!(root, "");
assert_eq!(resources.len(), 1);
assert_eq!(resources[0].name, "STORAGE");
assert_eq!(resources[0].usage, 10);
assert_eq!(resources[0].limit, 512);
return;
}
}
panic!("expected Quota response");
}
#[test]
fn parse_quota_multiple_resources() {
let input = b"* QUOTA \"user.alice\" (STORAGE 100 1024 MESSAGE 50 500)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Quota { root, resources } = *boxed {
assert_eq!(root, "user.alice");
assert_eq!(resources.len(), 2);
assert_eq!(resources[0].name, "STORAGE");
assert_eq!(resources[0].usage, 100);
assert_eq!(resources[0].limit, 1024);
assert_eq!(resources[1].name, "MESSAGE");
assert_eq!(resources[1].usage, 50);
assert_eq!(resources[1].limit, 500);
return;
}
}
panic!("expected Quota response with multiple resources");
}
#[test]
fn parse_quota_empty_resource_list() {
let input = b"* QUOTA \"\" ()\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Quota { root, resources } = *boxed {
assert_eq!(root, "");
assert!(resources.is_empty());
return;
}
}
panic!("expected Quota response with empty resources");
}
#[test]
fn parse_quotaroot_single_root() {
let input = b"* QUOTAROOT INBOX \"\"\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::QuotaRoot { mailbox, roots } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(roots, vec![""]);
return;
}
}
panic!("expected QuotaRoot response");
}
#[test]
fn parse_quotaroot_multiple_roots() {
let input = b"* QUOTAROOT INBOX \"\" \"user.bob\"\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::QuotaRoot { mailbox, roots } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(roots, vec!["", "user.bob"]);
return;
}
}
panic!("expected QuotaRoot response with multiple roots");
}
#[test]
fn parse_quotaroot_no_roots() {
let input = b"* QUOTAROOT INBOX\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::QuotaRoot { mailbox, roots } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert!(roots.is_empty());
return;
}
}
panic!("expected QuotaRoot response with no roots");
}
#[test]
fn parse_quota_case_insensitive() {
let input = b"* quota \"\" (STORAGE 5 100)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Quota { root, resources } = *boxed {
assert_eq!(root, "");
assert_eq!(resources.len(), 1);
assert_eq!(resources[0].name, "STORAGE");
return;
}
}
panic!("expected case-insensitive Quota response");
}
#[test]
fn capability_quota() {
let input = b"* CAPABILITY IMAP4rev1 QUOTA\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Capability(caps) = *boxed {
assert!(caps.contains(&Capability::Quota));
return;
}
}
panic!("expected QUOTA capability");
}
#[test]
fn capability_quotaset() {
let input = b"* CAPABILITY IMAP4rev1 QUOTASET\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Capability(caps) = *boxed {
assert!(caps.contains(&Capability::QuotaSet));
return;
}
}
panic!("expected QUOTASET capability");
}
#[test]
fn capability_quota_resource() {
let input = b"* CAPABILITY IMAP4rev1 QUOTA=RES-STORAGE\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Capability(caps) = *boxed {
assert!(caps.contains(&Capability::QuotaResource("STORAGE".into())));
return;
}
}
panic!("expected QUOTA=RES-STORAGE capability");
}
#[test]
fn parse_acl_single_entry() {
let input = b"* ACL INBOX fred lrswipcda\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Acl { mailbox, entries } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].identifier, "fred");
assert_eq!(entries[0].rights, "lrswipcda");
return;
}
}
panic!("expected ACL response");
}
#[test]
fn parse_acl_multiple_entries() {
let input = b"* ACL INBOX fred lrswipcda chris lrs\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Acl { mailbox, entries } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].identifier, "fred");
assert_eq!(entries[0].rights, "lrswipcda");
assert_eq!(entries[1].identifier, "chris");
assert_eq!(entries[1].rights, "lrs");
return;
}
}
panic!("expected ACL response with multiple entries");
}
#[test]
fn parse_acl_no_entries() {
let input = b"* ACL INBOX\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Acl { mailbox, entries } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert!(entries.is_empty());
return;
}
}
panic!("expected ACL response with no entries");
}
#[test]
fn parse_myrights() {
let input = b"* MYRIGHTS INBOX lrswipcda\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::MyRights { mailbox, rights } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(rights, "lrswipcda");
return;
}
}
panic!("expected MYRIGHTS response");
}
#[test]
fn parse_myrights_quoted_mailbox() {
let input = b"* MYRIGHTS \"Sent Items\" lrs\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::MyRights { mailbox, rights } = *boxed {
assert_eq!(mailbox.as_str(), "Sent Items");
assert_eq!(rights, "lrs");
return;
}
}
panic!("expected MYRIGHTS response with quoted mailbox");
}
#[test]
fn parse_listrights_with_optional() {
let input = b"* LISTRIGHTS INBOX fred lr s w i p c d a\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::ListRights {
mailbox,
identifier,
required,
optional,
} = *boxed
{
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(identifier, "fred");
assert_eq!(required, "lr");
assert_eq!(optional, vec!["s", "w", "i", "p", "c", "d", "a"]);
return;
}
}
panic!("expected LISTRIGHTS response");
}
#[test]
fn parse_listrights_no_optional() {
let input = b"* LISTRIGHTS INBOX fred lrswipcda\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::ListRights {
mailbox,
identifier,
required,
optional,
} = *boxed
{
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(identifier, "fred");
assert_eq!(required, "lrswipcda");
assert!(optional.is_empty());
return;
}
}
panic!("expected LISTRIGHTS response with no optional");
}
#[test]
fn parse_acl_case_insensitive() {
let input = b"* acl INBOX alice lrs\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Acl { mailbox, entries } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].identifier, "alice");
return;
}
}
panic!("expected case-insensitive ACL response");
}
#[test]
fn capability_acl() {
let input = b"* CAPABILITY IMAP4rev1 ACL\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Capability(caps) = *boxed {
assert!(caps.contains(&Capability::Acl));
return;
}
}
panic!("expected ACL capability");
}
#[test]
fn capability_rights_parsed() {
let (_, cap) = capability(b"RIGHTS=texk").unwrap();
match cap {
Capability::Rights(rights) => assert_eq!(rights, "texk"),
other => panic!("expected Rights variant, got {other:?}"),
}
}
#[test]
fn capability_rights_case_insensitive() {
let (_, cap) = capability(b"rights=TEXK").unwrap();
match cap {
Capability::Rights(rights) => assert_eq!(rights, "TEXK"),
other => panic!("expected Rights variant, got {other:?}"),
}
}
#[test]
fn capability_response_includes_rights() {
let input = b"* CAPABILITY IMAP4rev1 ACL RIGHTS=texk\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Capability(caps) = *boxed {
assert!(
caps.iter()
.any(|c| matches!(c, Capability::Rights(r) if r == "texk")),
"RIGHTS=texk not found in capabilities: {caps:?}"
);
return;
}
}
panic!("expected capability response");
}
#[test]
fn body_structure_deeply_nested_multipart() {
let input = b"* 1 FETCH (BODYSTRUCTURE \
(((\"text\" \"plain\" (\"charset\" \"utf-8\") NIL NIL \"7bit\" 100 5 NIL NIL NIL NIL)\
(\"text\" \"html\" (\"charset\" \"utf-8\") NIL NIL \"quoted-printable\" 200 10 NIL NIL NIL NIL) \
\"alternative\" NIL NIL NIL NIL)\
(\"application\" \"pdf\" (\"name\" \"doc.pdf\") NIL NIL \"base64\" 5000 NIL NIL NIL NIL) \
\"mixed\" NIL NIL NIL NIL))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fetch) = *boxed {
let body = fetch.body_structure.expect("missing BODYSTRUCTURE");
if let BodyStructure::Multipart {
media_subtype,
bodies,
..
} = &body
{
assert!(media_subtype.eq_ignore_ascii_case("mixed"));
assert_eq!(bodies.len(), 2);
if let BodyStructure::Multipart {
media_subtype: inner_sub,
bodies: inner_parts,
..
} = &bodies[0]
{
assert!(inner_sub.eq_ignore_ascii_case("alternative"));
assert_eq!(inner_parts.len(), 2);
} else {
panic!("expected inner multipart/alternative");
}
if let BodyStructure::Basic {
media_type,
media_subtype,
..
} = &bodies[1]
{
assert!(media_type.eq_ignore_ascii_case("application"));
assert!(media_subtype.eq_ignore_ascii_case("pdf"));
} else {
panic!("expected application/pdf");
}
return;
}
}
}
panic!("expected nested multipart BODYSTRUCTURE");
}
#[test]
fn body_structure_message_rfc822_nested() {
let input = b"* 1 FETCH (BODYSTRUCTURE \
(\"message\" \"rfc822\" NIL NIL NIL \"7bit\" 1000 \
(\"Mon, 01 Jan 2024 00:00:00 +0000\" \"Inner Subject\" \
((\"Sender\" NIL \"sender\" \"example.com\")) \
NIL NIL \
((\"Recipient\" NIL \"rcpt\" \"example.com\")) \
NIL NIL NIL \"<inner@example.com>\") \
(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 50 3 NIL NIL NIL NIL) \
20 NIL NIL NIL NIL))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fetch) = *boxed {
let body = fetch.body_structure.expect("missing BODYSTRUCTURE");
if let BodyStructure::Message { envelope, body, .. } = &body {
assert_eq!(envelope.subject, Some("Inner Subject".into()));
assert_eq!(envelope.message_id, Some("<inner@example.com>".into()));
if let BodyStructure::Text { media_subtype, .. } = body.as_ref() {
assert!(media_subtype.eq_ignore_ascii_case("plain"));
} else {
panic!("expected text/plain in nested body");
}
return;
}
}
}
panic!("expected message/rfc822 BODYSTRUCTURE");
}
#[test]
fn fetch_body_partial_with_origin() {
let input = b"* 1 FETCH (BODY[]<0> {10}\r\n0123456789)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fetch) = *boxed {
assert_eq!(fetch.body_sections.len(), 1);
let sec = &fetch.body_sections[0];
assert_eq!(sec.origin, Some(0));
assert_eq!(sec.data.as_deref(), Some(b"0123456789".as_slice()));
return;
}
}
panic!("expected FETCH with partial origin");
}
#[test]
fn search_empty_result() {
let input = b"* SEARCH\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Search { uids, .. } = *boxed {
assert!(uids.is_empty());
return;
}
}
panic!("expected empty SEARCH");
}
#[test]
fn search_multiple_uids() {
let input = b"* SEARCH 1 5 10 42\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Search { uids, .. } = *boxed {
assert_eq!(uids, vec![1, 5, 10, 42]);
return;
}
}
panic!("expected SEARCH with UIDs");
}
#[test]
fn esearch_all_preserves_ranges() {
let input = b"* ESEARCH (TAG \"A001\") ALL 1:3,5,10:12\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Esearch(esearch) = *boxed {
assert_eq!(esearch.tag.as_deref(), Some("A001"));
assert!(!esearch.uid);
assert_eq!(
esearch.all,
vec![
UidRange::range(1, 3),
UidRange::single(5),
UidRange::range(10, 12),
]
);
return;
}
}
panic!("expected ESEARCH with ALL ranges");
}
#[test]
fn uid_range_single_value() {
let (_, ranges) = uid_set(b"42").unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].start, 42);
assert_eq!(ranges[0].end, None);
}
#[test]
fn uid_set_complex() {
let (_, ranges) = uid_set(b"1:5,10,20:30").unwrap();
assert_eq!(ranges.len(), 3);
assert_eq!(
ranges[0],
UidRange {
start: 1,
end: Some(5)
}
);
assert_eq!(ranges[1], UidRange::single(10));
assert_eq!(
ranges[2],
UidRange {
start: 20,
end: Some(30)
}
);
}
#[test]
fn response_code_appenduid_multi() {
let (_, code) = response_code(b"[APPENDUID 1234 100,101,102]").unwrap();
if let ResponseCode::AppendUid { uid_validity, uids } = code {
assert_eq!(uid_validity, 1234);
assert_eq!(uids.len(), 3);
assert_eq!(uids[0].start, 100);
assert_eq!(uids[1].start, 101);
assert_eq!(uids[2].start, 102);
} else {
panic!("expected AppendUid");
}
}
#[test]
fn envelope_truncated_fails() {
let input = b"(\"Mon, 01 Jan 2024\" \"Subject\"";
assert!(envelope(input, false).is_err());
}
#[test]
fn body_structure_minimal_text() {
let input = b"(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 42 3)";
let (_, body) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Text {
media_subtype,
size,
lines,
..
} = body
{
assert!(media_subtype.eq_ignore_ascii_case("plain"));
assert_eq!(size, 42);
assert_eq!(lines, 3);
} else {
panic!("expected Text body");
}
}
#[test]
fn tagged_ok_no_code() {
let input = b"A001 OK NOOP completed\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(tagged) = resp {
assert_eq!(tagged.tag, "A001");
assert_eq!(tagged.status, StatusKind::Ok);
assert!(tagged.code.is_none());
assert_eq!(tagged.text, "NOOP completed");
} else {
panic!("expected tagged response");
}
}
#[test]
fn tagged_bad_response() {
let input = b"A001 BAD Command syntax error\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(tagged) = resp {
assert_eq!(tagged.status, StatusKind::Bad);
assert_eq!(tagged.text, "Command syntax error");
} else {
panic!("expected tagged BAD");
}
}
#[test]
fn untagged_bye_with_text() {
let input = b"* BYE Server shutting down\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Status { status, text, .. } = &*boxed {
assert_eq!(*status, UntaggedStatus::Bye);
assert_eq!(text, "Server shutting down");
return;
}
}
panic!("expected BYE");
}
#[test]
fn response_code_appenduid_non_contiguous() {
let (_, code) = response_code(b"[APPENDUID 67890 100,105,110:112]").unwrap();
if let ResponseCode::AppendUid { uid_validity, uids } = code {
assert_eq!(uid_validity, 67890);
assert_eq!(uids.len(), 3);
assert_eq!(uids[0], UidRange::single(100));
assert_eq!(uids[1], UidRange::single(105));
assert_eq!(uids[2], UidRange::range(110, 112));
} else {
panic!("expected AppendUid");
}
}
#[test]
fn response_code_appenduid_large_values() {
let (_, code) = response_code(b"[APPENDUID 4294967295 4294967290:4294967295]").unwrap();
if let ResponseCode::AppendUid { uid_validity, uids } = code {
assert_eq!(uid_validity, u32::MAX);
assert_eq!(uids.len(), 1);
assert_eq!(uids[0], UidRange::range(4_294_967_290, u32::MAX));
} else {
panic!("expected AppendUid");
}
}
#[test]
fn tagged_ok_appenduid_multiappend() {
let input = b"A001 OK [APPENDUID 12345 100,101,102] APPEND completed\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(tagged) = resp {
assert_eq!(tagged.tag, "A001");
assert_eq!(tagged.status, StatusKind::Ok);
if let Some(ResponseCode::AppendUid { uid_validity, uids }) = tagged.code {
assert_eq!(uid_validity, 12345);
assert_eq!(uids.len(), 3);
} else {
panic!("expected APPENDUID response code");
}
} else {
panic!("expected tagged response");
}
}
#[test]
fn response_code_appenduid_range_multiappend() {
let (_, code) = response_code(b"[APPENDUID 555 200:204]").unwrap();
if let ResponseCode::AppendUid { uid_validity, uids } = code {
assert_eq!(uid_validity, 555);
assert_eq!(uids.len(), 1);
assert_eq!(uids[0], UidRange::range(200, 204));
} else {
panic!("expected AppendUid");
}
}
#[test]
fn address_group_start() {
let input = b"(NIL NIL \"Friends\" NIL)";
let (_, addr) = address(input, false).unwrap();
assert!(addr.is_group_start());
assert!(!addr.is_group_end());
assert_eq!(addr.mailbox.as_deref(), Some("Friends"));
assert!(addr.host.is_none());
}
#[test]
fn address_group_end() {
let input = b"(NIL NIL NIL NIL)";
let (_, addr) = address(input, false).unwrap();
assert!(addr.is_group_end());
assert!(!addr.is_group_start());
}
#[test]
fn address_list_with_group() {
let input =
b"((NIL NIL \"Team\" NIL)(\"Alice\" NIL \"alice\" \"example.com\")(NIL NIL NIL NIL))";
let (_, addrs) = address_list(input, false).unwrap();
assert_eq!(addrs.len(), 3);
assert!(addrs[0].is_group_start());
assert_eq!(addrs[0].mailbox.as_deref(), Some("Team"));
assert!(addrs[1].is_address());
assert_eq!(addrs[1].email(), Some("alice@example.com".into()));
assert!(addrs[2].is_group_end());
}
#[test]
fn fetch_lowercase_attributes() {
let input = b"(uid 42 flags (\\Seen) rfc822.size 1024)";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.uid, Some(42));
assert_eq!(fr.flags, Some(vec![Flag::Seen]));
assert_eq!(fr.rfc822_size, Some(1024));
}
#[test]
fn fetch_mixed_case_attributes() {
let input = b"(Uid 99 Flags (\\Deleted \\Draft) Rfc822.Size 2048)";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.uid, Some(99));
assert_eq!(fr.flags, Some(vec![Flag::Deleted, Flag::Draft]));
assert_eq!(fr.rfc822_size, Some(2048));
}
#[test]
fn fetch_lowercase_bodystructure() {
let input =
b"(bodystructure (\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5))";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert!(fr.body_structure.is_some());
}
#[test]
fn fetch_lowercase_modseq() {
let input = b"(modseq (12345))";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.mod_seq, Some(12345));
}
#[test]
fn fetch_lowercase_internaldate() {
let input = b"(internaldate \"17-Jul-1996 02:44:25 -0700\")";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(
fr.internal_date.as_deref(),
Some("17-Jul-1996 02:44:25 -0700")
);
}
#[test]
fn body_structure_ext_md5_only() {
let input = b"(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5 \"abc123\")";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Text {
md5, disposition, ..
} = bs
{
assert_eq!(md5.as_deref(), Some("abc123"));
assert!(disposition.is_none());
} else {
panic!("expected Text body");
}
}
#[test]
fn body_structure_ext_md5_and_disposition() {
let input =
b"(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5 NIL (\"inline\" NIL))";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Text {
md5,
disposition,
language,
location,
..
} = bs
{
assert!(md5.is_none());
let disp = disposition.expect("disposition should be present");
assert_eq!(disp.disposition_type, "inline");
assert!(language.is_none());
assert!(location.is_none());
} else {
panic!("expected Text body");
}
}
#[test]
fn body_structure_ext_through_language() {
let input = b"(\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 100 5 NIL NIL (\"en\" \"fr\"))";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Text {
language, location, ..
} = bs
{
let langs = language.expect("language should be present");
assert_eq!(langs, vec!["en", "fr"]);
assert!(location.is_none());
} else {
panic!("expected Text body");
}
}
#[test]
fn body_structure_mpart_ext_params_only() {
let input = b"((\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 50 3)(\"TEXT\" \"HTML\" NIL NIL NIL \"7BIT\" 80 4) \"ALTERNATIVE\" (\"BOUNDARY\" \"----=_Part\"))";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Multipart {
params,
disposition,
..
} = bs
{
assert_eq!(params.len(), 1);
assert_eq!(params[0].0, "boundary");
assert!(disposition.is_none());
} else {
panic!("expected Multipart body");
}
}
#[test]
fn body_structure_mpart_no_extension() {
let input = b"((\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 50 3) \"MIXED\")";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Multipart {
params,
disposition,
language,
location,
..
} = bs
{
assert!(params.is_empty());
assert!(disposition.is_none());
assert!(language.is_none());
assert!(location.is_none());
} else {
panic!("expected Multipart body");
}
}
#[test]
fn body_type_mpart_rejects_zero_children() {
let input = b" \"MIXED\" NIL NIL NIL NIL)";
let result = body_type_mpart(input, false, 0);
assert!(
result.is_err(),
"body_type_mpart must reject zero children per RFC 3501 Section 9 (1*body)"
);
}
#[test]
fn fetch_multiple_unknown_attributes_skipped() {
let input = b"(UID 42 X-CUSTOM1 \"value1\" X-CUSTOM2 (nested data) FLAGS (\\Seen))";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.uid, Some(42));
assert_eq!(fr.flags, Some(vec![Flag::Seen]));
}
#[test]
fn fetch_unknown_attribute_nil_value() {
let input = b"(UID 10 X-UNKNOWN NIL FLAGS ())";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.uid, Some(10));
assert_eq!(fr.flags, Some(vec![]));
}
#[test]
fn fetch_unknown_attribute_literal_value() {
let input = b"(UID 7 X-DATA {5}\r\nhello FLAGS (\\Flagged))";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.uid, Some(7));
assert_eq!(fr.flags, Some(vec![Flag::Flagged]));
}
#[test]
fn literal_large_payload() {
let size = 100_000;
let mut input = format!("{{{size}}}\r\n").into_bytes();
input.extend(vec![b'X'; size]);
let (rest, val) = literal(&input).unwrap();
assert_eq!(val.len(), size);
assert!(rest.is_empty());
}
#[test]
fn literal_u32_max_count_insufficient_data() {
let input = b"{4294967295}\r\nshort";
assert!(literal(input).is_err());
}
#[test]
fn literal_count_overflow() {
let input = b"{99999999999}\r\ndata";
assert!(literal(input).is_err());
}
#[test]
fn tagged_response_no_space_after_tag() {
assert!(parse_response(b"A001OK done\r\n").is_err());
}
#[test]
fn tagged_response_empty_status() {
assert!(parse_response(b"A001 done\r\n").is_err());
}
#[test]
fn tagged_response_invalid_tag_char() {
assert!(parse_response(b"{BAD} OK done\r\n").is_err());
}
#[test]
fn tagged_response_no_text_after_status() {
let (rem, resp) = parse_response(b"A001 OK\r\n").unwrap();
assert!(rem.is_empty());
if let Response::Tagged(t) = resp {
assert_eq!(t.tag, "A001");
assert_eq!(t.status, StatusKind::Ok);
assert!(t.code.is_none());
assert!(t.text.is_empty());
} else {
panic!("expected Tagged response, got {resp:?}");
}
}
#[test]
fn parse_response_empty_input() {
assert!(parse_response(b"").is_err());
}
#[test]
fn parse_response_garbage_special_chars() {
assert!(parse_response(b"!@#$%^&\r\n").is_err());
}
#[test]
fn tagged_response_bare_lf() {
assert!(parse_response(b"A001 OK done\n").is_err());
}
#[test]
fn quota_named_root_single_resource() {
let input = b"QUOTA \"user.alice\" (STORAGE 200 1024)\r\n";
let (_, resp) = parse_untagged_quota(input).unwrap();
if let UntaggedResponse::Quota { root, resources } = resp {
assert_eq!(root, "user.alice");
assert_eq!(resources.len(), 1);
assert_eq!(resources[0].name, "STORAGE");
assert_eq!(resources[0].usage, 200);
assert_eq!(resources[0].limit, 1024);
} else {
panic!("expected Quota, got {resp:?}");
}
}
#[test]
fn quota_multiple_resources_triplets() {
let input = b"QUOTA \"\" (STORAGE 500 2048 MESSAGE 100 1000 MAILBOX 5 10)\r\n";
let (_, resp) = parse_untagged_quota(input).unwrap();
if let UntaggedResponse::Quota { root, resources } = resp {
assert_eq!(root, "");
assert_eq!(resources.len(), 3);
assert_eq!(resources[0].name, "STORAGE");
assert_eq!(resources[0].usage, 500);
assert_eq!(resources[0].limit, 2048);
assert_eq!(resources[1].name, "MESSAGE");
assert_eq!(resources[1].usage, 100);
assert_eq!(resources[1].limit, 1000);
assert_eq!(resources[2].name, "MAILBOX");
assert_eq!(resources[2].usage, 5);
assert_eq!(resources[2].limit, 10);
} else {
panic!("expected Quota, got {resp:?}");
}
}
#[test]
fn quota_empty_resources() {
let input = b"QUOTA \"root\" ()\r\n";
let (_, resp) = parse_untagged_quota(input).unwrap();
if let UntaggedResponse::Quota { root, resources } = resp {
assert_eq!(root, "root");
assert!(resources.is_empty());
} else {
panic!("expected Quota, got {resp:?}");
}
}
#[test]
fn quota_large_values() {
let input = b"QUOTA \"\" (STORAGE 4294967296 8589934592)\r\n";
let (_, resp) = parse_untagged_quota(input).unwrap();
if let UntaggedResponse::Quota { resources, .. } = resp {
assert_eq!(resources[0].usage, 4_294_967_296);
assert_eq!(resources[0].limit, 8_589_934_592);
} else {
panic!("expected Quota, got {resp:?}");
}
}
#[test]
fn quota_truncated_input() {
assert!(parse_untagged_quota(b"QUOTA \"\" (STORAGE 10 512)").is_err());
assert!(parse_untagged_quota(b"QUOTA \"\" (STORAGE 10 512\r\n").is_err());
assert!(parse_untagged_quota(b"QUOTA \"\" (STORAGE\r\n").is_err());
}
#[test]
fn quota_garbage_data() {
assert!(parse_untagged_quota(b"QUOTA !!!GARBAGE!!!\r\n").is_err());
}
#[test]
fn quotaroot_single_root_atom() {
let input = b"QUOTAROOT INBOX \"root\"\r\n";
let (_, resp) = parse_untagged_quotaroot(input, false).unwrap();
if let UntaggedResponse::QuotaRoot { mailbox, roots } = resp {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(roots, vec!["root"]);
} else {
panic!("expected QuotaRoot, got {resp:?}");
}
}
#[test]
fn quotaroot_multiple_roots_list() {
let input = b"QUOTAROOT INBOX \"root1\" \"root2\" \"root3\"\r\n";
let (_, resp) = parse_untagged_quotaroot(input, false).unwrap();
if let UntaggedResponse::QuotaRoot { mailbox, roots } = resp {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(roots, vec!["root1", "root2", "root3"]);
} else {
panic!("expected QuotaRoot, got {resp:?}");
}
}
#[test]
fn quotaroot_no_roots_at_all() {
let input = b"QUOTAROOT \"Sent Items\"\r\n";
let (_, resp) = parse_untagged_quotaroot(input, false).unwrap();
if let UntaggedResponse::QuotaRoot { mailbox, roots } = resp {
assert_eq!(mailbox.as_str(), "Sent Items");
assert!(roots.is_empty());
} else {
panic!("expected QuotaRoot, got {resp:?}");
}
}
#[test]
fn quotaroot_truncated_input() {
assert!(parse_untagged_quotaroot(b"QUOTAROOT INBOX", false).is_err());
}
#[test]
fn acl_single_entry_quoted_mailbox() {
let input = b"ACL \"Sent Items\" alice lrswipcda\r\n";
let (_, resp) = parse_untagged_acl(input, false).unwrap();
if let UntaggedResponse::Acl { mailbox, entries } = resp {
assert_eq!(mailbox.as_str(), "Sent Items");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].identifier, "alice");
assert_eq!(entries[0].rights, "lrswipcda");
} else {
panic!("expected Acl, got {resp:?}");
}
}
#[test]
fn acl_multiple_entries_varied() {
let input = b"ACL INBOX alice lrswipcda bob lr chris lrswip\r\n";
let (_, resp) = parse_untagged_acl(input, false).unwrap();
if let UntaggedResponse::Acl { mailbox, entries } = resp {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].identifier, "alice");
assert_eq!(entries[0].rights, "lrswipcda");
assert_eq!(entries[1].identifier, "bob");
assert_eq!(entries[1].rights, "lr");
assert_eq!(entries[2].identifier, "chris");
assert_eq!(entries[2].rights, "lrswip");
} else {
panic!("expected Acl, got {resp:?}");
}
}
#[test]
fn acl_no_entries_empty() {
let input = b"ACL \"Archive\"\r\n";
let (_, resp) = parse_untagged_acl(input, false).unwrap();
if let UntaggedResponse::Acl { mailbox, entries } = resp {
assert_eq!(mailbox.as_str(), "Archive");
assert!(entries.is_empty());
} else {
panic!("expected Acl, got {resp:?}");
}
}
#[test]
fn acl_truncated_input() {
assert!(parse_untagged_acl(b"ACL INBOX alice lrs", false).is_err());
}
#[test]
fn acl_garbage_data() {
assert!(parse_untagged_acl(b"GARBAGE data\r\n", false).is_err());
}
#[test]
fn listrights_required_and_optional() {
let input = b"LISTRIGHTS \"Sent Items\" bob lr s w i p c d a\r\n";
let (_, resp) = parse_untagged_listrights(input, false).unwrap();
if let UntaggedResponse::ListRights {
mailbox,
identifier,
required,
optional,
} = resp
{
assert_eq!(mailbox.as_str(), "Sent Items");
assert_eq!(identifier, "bob");
assert_eq!(required, "lr");
assert_eq!(optional.len(), 7);
assert_eq!(optional, vec!["s", "w", "i", "p", "c", "d", "a"]);
} else {
panic!("expected ListRights, got {resp:?}");
}
}
#[test]
fn listrights_required_only() {
let input = b"LISTRIGHTS INBOX alice lrswipcda\r\n";
let (_, resp) = parse_untagged_listrights(input, false).unwrap();
if let UntaggedResponse::ListRights {
required, optional, ..
} = resp
{
assert_eq!(required, "lrswipcda");
assert!(optional.is_empty());
} else {
panic!("expected ListRights, got {resp:?}");
}
}
#[test]
fn listrights_truncated_input() {
assert!(parse_untagged_listrights(b"LISTRIGHTS INBOX fred", false).is_err());
assert!(parse_untagged_listrights(b"LISTRIGHTS INBOX fred lr", false).is_err());
}
#[test]
fn listrights_garbage_data() {
assert!(parse_untagged_listrights(b"LISTRIGHTS !!!GARBAGE\r\n", false).is_err());
}
#[test]
fn myrights_atom_mailbox() {
let input = b"MYRIGHTS INBOX lr\r\n";
let (_, resp) = parse_untagged_myrights(input, false).unwrap();
if let UntaggedResponse::MyRights { mailbox, rights } = resp {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(rights, "lr");
} else {
panic!("expected MyRights, got {resp:?}");
}
}
#[test]
fn myrights_truncated_input() {
assert!(parse_untagged_myrights(b"MYRIGHTS INBOX", false).is_err());
assert!(parse_untagged_myrights(b"MYRIGHTS INBOX lrs", false).is_err());
}
#[test]
fn myrights_garbage_data() {
assert!(parse_untagged_myrights(b"MYRIGHTS !!!GARBAGE\r\n", false).is_err());
}
#[test]
fn metadata_single_entry() {
let input = b"METADATA \"INBOX\" (\"/private/comment\" \"My folder\")\r\n";
let (_, resp) = parse_untagged_metadata(input, false).unwrap();
if let UntaggedResponse::Metadata { mailbox, entries } = resp {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "/private/comment");
assert_eq!(entries[0].value.as_deref(), Some(b"My folder".as_slice()));
} else {
panic!("expected Metadata, got {resp:?}");
}
}
#[test]
fn metadata_multiple_entries_mixed() {
let input = b"METADATA \"INBOX\" (\"/private/comment\" \"hello\" \"/shared/comment\" \"world\" \"/private/vendor/foo\" \"bar\")\r\n";
let (_, resp) = parse_untagged_metadata(input, false).unwrap();
if let UntaggedResponse::Metadata { mailbox, entries } = resp {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].name, "/private/comment");
assert_eq!(entries[0].value.as_deref(), Some(b"hello".as_slice()));
assert_eq!(entries[1].name, "/shared/comment");
assert_eq!(entries[1].value.as_deref(), Some(b"world".as_slice()));
assert_eq!(entries[2].name, "/private/vendor/foo");
assert_eq!(entries[2].value.as_deref(), Some(b"bar".as_slice()));
} else {
panic!("expected Metadata, got {resp:?}");
}
}
#[test]
fn metadata_complex_vendor_keys() {
let input = b"METADATA \"INBOX\" (\"/shared/vendor/example.com/widget\" \"config-value\")\r\n";
let (_, resp) = parse_untagged_metadata(input, false).unwrap();
if let UntaggedResponse::Metadata { entries, .. } = resp {
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "/shared/vendor/example.com/widget");
assert_eq!(
entries[0].value.as_deref(),
Some(b"config-value".as_slice())
);
} else {
panic!("expected Metadata, got {resp:?}");
}
}
#[test]
fn metadata_nil_value_deleted() {
let input =
b"METADATA \"INBOX\" (\"/private/comment\" NIL \"/shared/comment\" \"present\")\r\n";
let (_, resp) = parse_untagged_metadata(input, false).unwrap();
if let UntaggedResponse::Metadata { entries, .. } = resp {
assert_eq!(entries.len(), 2);
assert!(entries[0].value.is_none());
assert_eq!(entries[1].value.as_deref(), Some(b"present".as_slice()));
} else {
panic!("expected Metadata, got {resp:?}");
}
}
#[test]
fn metadata_all_nil_values() {
let input = b"METADATA \"INBOX\" (\"/private/comment\" NIL \"/shared/comment\" NIL)\r\n";
let (_, resp) = parse_untagged_metadata(input, false).unwrap();
if let UntaggedResponse::Metadata { entries, .. } = resp {
assert_eq!(entries.len(), 2);
assert!(entries[0].value.is_none());
assert!(entries[1].value.is_none());
} else {
panic!("expected Metadata, got {resp:?}");
}
}
#[test]
fn metadata_empty_list_standalone() {
let input = b"METADATA \"Archive\" ()\r\n";
let (_, resp) = parse_untagged_metadata(input, false).unwrap();
if let UntaggedResponse::Metadata { mailbox, entries } = resp {
assert_eq!(mailbox.as_str(), "Archive");
assert!(entries.is_empty());
} else {
panic!("expected Metadata, got {resp:?}");
}
}
#[test]
fn metadata_truncated_input() {
assert!(parse_untagged_metadata(
b"METADATA \"INBOX\" (\"/private/comment\" \"val\"\r\n",
false
)
.is_err());
assert!(
parse_untagged_metadata(b"METADATA \"INBOX\" (\"/private/comment\" \"val\")", false)
.is_err()
);
assert!(parse_untagged_metadata(b"METADATA \"INBOX\" (\"/private/comment\"", false).is_err());
}
#[test]
fn metadata_garbage_data() {
assert!(parse_untagged_metadata(b"METADATA !!!GARBAGE\r\n", false).is_err());
}
#[test]
fn metadata_via_parse_response() {
let input = b"* METADATA \"INBOX\" (\"/private/comment\" \"test\")\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Metadata { mailbox, entries } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "/private/comment");
return;
}
}
panic!("expected Metadata via parse_response");
}
#[test]
fn thread_single_flat() {
let input = b"THREAD (5)\r\n";
let (_, resp) = parse_untagged_thread(input).unwrap();
if let UntaggedResponse::Thread(threads) = resp {
assert_eq!(threads.len(), 1);
assert_eq!(threads[0].id, Some(5));
assert!(threads[0].children.is_empty());
} else {
panic!("expected Thread, got {resp:?}");
}
}
#[test]
fn thread_chained() {
let input = b"THREAD (10 20 30)\r\n";
let (_, resp) = parse_untagged_thread(input).unwrap();
if let UntaggedResponse::Thread(threads) = resp {
assert_eq!(threads.len(), 1);
assert_eq!(threads[0].id, Some(10));
assert_eq!(threads[0].children.len(), 1);
assert_eq!(threads[0].children[0].id, Some(20));
assert_eq!(threads[0].children[0].children.len(), 1);
assert_eq!(threads[0].children[0].children[0].id, Some(30));
assert!(threads[0].children[0].children[0].children.is_empty());
} else {
panic!("expected Thread, got {resp:?}");
}
}
#[test]
fn thread_nested_with_branches() {
let input = b"THREAD (1 2 (3)(4))\r\n";
let (_, resp) = parse_untagged_thread(input).unwrap();
if let UntaggedResponse::Thread(threads) = resp {
assert_eq!(threads.len(), 1);
assert_eq!(threads[0].id, Some(1));
assert_eq!(threads[0].children.len(), 1);
let child = &threads[0].children[0];
assert_eq!(child.id, Some(2));
assert_eq!(child.children.len(), 2);
assert_eq!(child.children[0].id, Some(3));
assert_eq!(child.children[1].id, Some(4));
} else {
panic!("expected Thread, got {resp:?}");
}
}
#[test]
fn thread_multiple_top_level() {
let input = b"THREAD (1)(2)(3)\r\n";
let (_, resp) = parse_untagged_thread(input).unwrap();
if let UntaggedResponse::Thread(threads) = resp {
assert_eq!(threads.len(), 3);
assert_eq!(threads[0].id, Some(1));
assert_eq!(threads[1].id, Some(2));
assert_eq!(threads[2].id, Some(3));
assert!(threads[0].children.is_empty());
assert!(threads[1].children.is_empty());
assert!(threads[2].children.is_empty());
} else {
panic!("expected Thread, got {resp:?}");
}
}
#[test]
fn thread_empty_response() {
let input = b"THREAD\r\n";
let (_, resp) = parse_untagged_thread(input).unwrap();
if let UntaggedResponse::Thread(threads) = resp {
assert!(threads.is_empty());
} else {
panic!("expected Thread, got {resp:?}");
}
}
#[test]
fn thread_dummy_root_nodes() {
let input = b"THREAD ((5)(10)(15))\r\n";
let (_, resp) = parse_untagged_thread(input).unwrap();
if let UntaggedResponse::Thread(threads) = resp {
assert_eq!(threads.len(), 1);
assert_eq!(threads[0].id, None); assert_eq!(threads[0].children.len(), 3);
assert_eq!(threads[0].children[0].id, Some(5));
assert_eq!(threads[0].children[1].id, Some(10));
assert_eq!(threads[0].children[2].id, Some(15));
} else {
panic!("expected Thread, got {resp:?}");
}
}
#[test]
fn thread_deeply_nested() {
let input = b"THREAD (1 (2 (3 (4 5))))\r\n";
let (_, resp) = parse_untagged_thread(input).unwrap();
if let UntaggedResponse::Thread(threads) = resp {
assert_eq!(threads.len(), 1);
assert_eq!(threads[0].id, Some(1));
let n2 = &threads[0].children[0];
assert_eq!(n2.id, Some(2));
let n3 = &n2.children[0];
assert_eq!(n3.id, Some(3));
let n4 = &n3.children[0];
assert_eq!(n4.id, Some(4));
assert_eq!(n4.children.len(), 1);
assert_eq!(n4.children[0].id, Some(5));
} else {
panic!("expected Thread, got {resp:?}");
}
}
#[test]
fn thread_truncated_input() {
assert!(parse_untagged_thread(b"THREAD (1 2").is_err());
assert!(parse_untagged_thread(b"THREAD (1 2)").is_err());
}
#[test]
fn build_thread_tree_empty() {
let result = build_thread_tree(&[], &[]);
assert!(result.is_empty());
}
#[test]
fn build_thread_tree_single_uid() {
let result = build_thread_tree(&[Some(42)], &[vec![]]);
assert_eq!(result.len(), 1);
assert_eq!(result[0].id, Some(42));
assert!(result[0].children.is_empty());
}
#[test]
fn thread_rejects_excessive_nesting_depth() {
let depth = 256;
let mut input = String::from("THREAD ");
for i in 1..=depth {
input.push('(');
input.push_str(&i.to_string());
input.push(' ');
}
for _ in 1..=depth {
input.push(')');
}
input.push_str("\r\n");
let result = parse_untagged_thread(input.as_bytes());
assert!(
result.is_err(),
"deeply nested THREAD response (depth {depth}) should be rejected"
);
}
#[test]
fn body_params_nil_standalone() {
let (_, params) = body_params(b"NIL").unwrap();
assert!(params.is_empty());
}
#[test]
fn body_params_nil_case_insensitive() {
let (_, params) = body_params(b"nil").unwrap();
assert!(params.is_empty());
let (_, params) = body_params(b"Nil").unwrap();
assert!(params.is_empty());
}
#[test]
fn body_params_empty_list_accepted() {
let (_, params) = body_params(b"()").unwrap();
assert!(
params.is_empty(),
"empty () should produce empty params vec, got: {params:?}"
);
}
#[test]
fn body_params_single_pair() {
let (_, params) = body_params(b"(\"CHARSET\" \"UTF-8\")").unwrap();
assert_eq!(params.len(), 1);
assert_eq!(params[0], ("charset".into(), "UTF-8".into()));
}
#[test]
fn body_params_multiple_pairs() {
let (_, params) =
body_params(b"(\"CHARSET\" \"UTF-8\" \"NAME\" \"test.txt\" \"FORMAT\" \"flowed\")")
.unwrap();
assert_eq!(params.len(), 3);
assert_eq!(params[0], ("charset".into(), "UTF-8".into()));
assert_eq!(params[1], ("name".into(), "test.txt".into()));
assert_eq!(params[2], ("format".into(), "flowed".into()));
}
#[test]
fn body_params_rfc2231_continuation_standalone() {
let input = b"(\"FILENAME*0\" \"very-\" \"FILENAME*1\" \"long-\" \"FILENAME*2\" \"name.txt\")";
let (_, params) = body_params(input).unwrap();
assert_eq!(params.len(), 1);
assert_eq!(params[0].0, "filename");
assert_eq!(params[0].1, "very-long-name.txt");
}
#[test]
fn body_disposition_nil() {
let (_, disp) = body_disposition(b"NIL").unwrap();
assert!(disp.is_none());
}
#[test]
fn body_disposition_nil_case_insensitive() {
let (_, disp) = body_disposition(b"nil").unwrap();
assert!(disp.is_none());
}
#[test]
fn body_disposition_no_params() {
let (_, disp) = body_disposition(b"(\"inline\" NIL)").unwrap();
let disp = disp.expect("should not be None");
assert_eq!(disp.disposition_type, "inline");
assert!(disp.params.is_empty());
}
#[test]
fn body_disposition_with_params() {
let (_, disp) =
body_disposition(b"(\"attachment\" (\"FILENAME\" \"report.pdf\" \"SIZE\" \"1024\"))")
.unwrap();
let disp = disp.expect("should not be None");
assert_eq!(disp.disposition_type, "attachment");
assert_eq!(disp.params.len(), 2);
assert_eq!(disp.params[0], ("filename".into(), "report.pdf".into()));
assert_eq!(disp.params[1], ("size".into(), "1024".into()));
}
#[test]
fn body_language_nil() {
let (_, lang) = body_language(b"NIL").unwrap();
assert!(lang.is_none());
}
#[test]
fn body_language_single_string() {
let (_, lang) = body_language(b"\"en\"").unwrap();
let langs = lang.expect("should not be None");
assert_eq!(langs, vec!["en"]);
}
#[test]
fn body_language_list() {
let (_, lang) = body_language(b"(\"en\" \"fr\" \"de\")").unwrap();
let langs = lang.expect("should not be None");
assert_eq!(langs, vec!["en", "fr", "de"]);
}
#[test]
fn body_language_empty_list() {
let (_, lang) = body_language(b"()").unwrap();
let langs = lang.expect("empty () should be Some(vec![]), not None");
assert!(
langs.is_empty(),
"empty () should produce empty language vec"
);
}
#[test]
fn spec_audit_body_language_empty_parenthesized() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 100 5 NIL NIL () NIL))\r\n";
let result = parse_response(input);
assert!(
result.is_ok(),
"body-fld-lang `()` must be accepted per Postel's law; got parse error"
);
let (_, resp) = result.unwrap();
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Fetch(fr) => {
let bs = fr
.body_structure
.as_ref()
.expect("should have body_structure");
match bs {
BodyStructure::Text { language, .. } => {
let langs = language
.as_ref()
.expect("empty () should be Some(vec![]), not None");
assert!(
langs.is_empty(),
"empty () should produce empty language vec"
);
}
other => panic!("expected Text variant, got {other:?}"),
}
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn spec_audit_multipart_empty_language() {
let input = b"* 1 FETCH (BODYSTRUCTURE ((\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 10 1)(\"TEXT\" \"HTML\" NIL NIL NIL \"7BIT\" 20 2) \"ALTERNATIVE\" NIL NIL () NIL))\r\n";
let result = parse_response(input);
assert!(
result.is_ok(),
"multipart body-fld-lang `()` must be accepted; got parse error"
);
let (_, resp) = result.unwrap();
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Fetch(fr) => {
let bs = fr
.body_structure
.as_ref()
.expect("should have body_structure");
match bs {
BodyStructure::Multipart { language, .. } => {
let langs = language
.as_ref()
.expect("empty () should be Some(vec![]), not None");
assert!(
langs.is_empty(),
"empty () should produce empty language vec"
);
}
other => panic!("expected Multipart variant, got {other:?}"),
}
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn binary_section_spec_simple() {
let (rest, parts) = binary_section_spec(b"[1]").unwrap();
assert!(rest.is_empty());
assert_eq!(parts, vec![1]);
}
#[test]
fn binary_section_spec_nested() {
let (rest, parts) = binary_section_spec(b"[1.2.3]").unwrap();
assert!(rest.is_empty());
assert_eq!(parts, vec![1, 2, 3]);
}
#[test]
fn binary_section_spec_empty() {
let (rest, parts) = binary_section_spec(b"[]").unwrap();
assert!(rest.is_empty());
assert!(parts.is_empty());
}
#[test]
fn binary_section_spec_deep() {
let (rest, parts) = binary_section_spec(b"[1.2.3.4.5]").unwrap();
assert!(rest.is_empty());
assert_eq!(parts, vec![1, 2, 3, 4, 5]);
}
#[test]
fn binary_section_spec_truncated() {
assert!(binary_section_spec(b"[1.2").is_err());
assert!(binary_section_spec(b"1]").is_err());
}
#[test]
fn binary_section_with_origin_offset() {
let (rest, (parts, origin)) = binary_section_with_origin(b"[1]<100>").unwrap();
assert!(rest.is_empty());
assert_eq!(parts, vec![1]);
assert_eq!(origin, Some(100));
}
#[test]
fn binary_section_with_origin_no_origin() {
let (rest, (parts, origin)) = binary_section_with_origin(b"[1.2]").unwrap();
assert!(rest.is_empty());
assert_eq!(parts, vec![1, 2]);
assert!(origin.is_none());
}
#[test]
fn binary_section_with_origin_zero() {
let (rest, (parts, origin)) = binary_section_with_origin(b"[2]<0>").unwrap();
assert!(rest.is_empty());
assert_eq!(parts, vec![2]);
assert_eq!(origin, Some(0));
}
#[test]
fn binary_section_with_origin_empty_section() {
let (rest, (parts, origin)) = binary_section_with_origin(b"[]<500>").unwrap();
assert!(rest.is_empty());
assert!(parts.is_empty());
assert_eq!(origin, Some(500));
}
#[test]
fn fetch_binary_size_via_parse_response() {
let input = b"* 1 FETCH (BINARY.SIZE[1] 4096)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.binary_sizes.len(), 1);
assert_eq!(fr.binary_sizes[0], (vec![1], 4096));
return;
}
}
panic!("expected BINARY.SIZE via FETCH");
}
#[test]
fn q_encoding_simple_hex() {
let result = decode_q_encoding("=E9");
assert_eq!(result, vec![0xE9]);
}
#[test]
fn q_encoding_underscore_to_space() {
let result = decode_q_encoding("Hello_World");
assert_eq!(result, b"Hello World");
}
#[test]
fn q_encoding_mixed_literal_and_encoded() {
let result = decode_q_encoding("caf=E9");
assert_eq!(result, b"caf\xE9");
}
#[test]
fn q_encoding_consecutive_encoded() {
let result = decode_q_encoding("=C3=A9");
assert_eq!(result, vec![0xC3, 0xA9]);
}
#[test]
fn q_encoding_trailing_incomplete() {
let result = decode_q_encoding("test=E");
assert_eq!(result, b"test=E");
}
#[test]
fn q_encoding_invalid_hex_digits() {
let result = decode_q_encoding("test=GG");
assert_eq!(result, b"test=GG");
}
#[test]
fn q_encoding_empty_input() {
let result = decode_q_encoding("");
assert!(result.is_empty());
}
#[test]
fn q_encoding_all_encoded() {
let result = decode_q_encoding("=48=65=6C=6C=6F");
assert_eq!(result, b"Hello");
}
#[test]
fn q_encoding_lowercase_hex() {
let result = decode_q_encoding("=e9=c3");
assert_eq!(result, vec![0xE9, 0xC3]);
}
#[test]
fn q_encoding_underscore_and_encoded_mixed() {
let result = decode_q_encoding("=E9l=E8ve_du_coll=E8ge");
assert_eq!(
result,
vec![
0xE9, b'l', 0xE8, b'v', b'e', b' ', b'd', b'u', b' ', b'c', b'o', b'l', b'l', 0xE8,
b'g', b'e'
]
);
}
#[test]
fn hex_digit_valid() {
assert_eq!(hex_digit(b'0'), Some(0));
assert_eq!(hex_digit(b'5'), Some(5));
assert_eq!(hex_digit(b'9'), Some(9));
assert_eq!(hex_digit(b'A'), Some(10));
assert_eq!(hex_digit(b'F'), Some(15));
assert_eq!(hex_digit(b'a'), Some(10));
assert_eq!(hex_digit(b'f'), Some(15));
}
#[test]
fn hex_digit_invalid() {
assert_eq!(hex_digit(b'G'), None);
assert_eq!(hex_digit(b'g'), None);
assert_eq!(hex_digit(b'z'), None);
assert_eq!(hex_digit(b' '), None);
assert_eq!(hex_digit(b'!'), None);
assert_eq!(hex_digit(b'\x00'), None);
}
#[test]
fn hex_digit_boundaries() {
assert_eq!(hex_digit(b'/'), None);
assert_eq!(hex_digit(b':'), None);
assert_eq!(hex_digit(b'@'), None);
assert_eq!(hex_digit(b'G'), None);
assert_eq!(hex_digit(b'`'), None);
assert_eq!(hex_digit(b'g'), None);
}
#[test]
fn q_encoding_soft_line_break_crlf() {
let result = decode_q_encoding("Hel=\r\nlo");
assert_eq!(result, b"Hello");
}
#[test]
fn q_encoding_soft_line_break_lf() {
let result = decode_q_encoding("Hel=\nlo");
assert_eq!(result, b"Hello");
}
#[test]
fn q_encoding_soft_line_break_at_end() {
let result = decode_q_encoding("Hello=\r\n");
assert_eq!(result, b"Hello");
}
#[test]
fn q_encoding_soft_break_with_hex() {
let result = decode_q_encoding("caf=\r\n=E9");
assert_eq!(result, b"caf\xE9");
}
#[test]
fn spec_audit_q_encoding_soft_line_break_is_postel_leniency() {
let result = decode_q_encoding("Hello=\r\nWorld");
assert_eq!(result, b"HelloWorld");
}
#[test]
fn fetch_savedate_via_parse_response() {
let input = b"* 3 FETCH (UID 10 SAVEDATE \"15-Mar-2026 12:00:00 +0000\")\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(10));
assert_eq!(fr.save_date.as_deref(), Some("15-Mar-2026 12:00:00 +0000"));
return;
}
}
panic!("expected Fetch with SAVEDATE");
}
#[test]
fn fetch_savedate_with_flags_and_uid() {
let input = b"* 1 FETCH (UID 42 FLAGS (\\Seen) SAVEDATE \"01-Jan-2025 00:00:00 +0000\" RFC822.SIZE 1234)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(42));
assert_eq!(fr.flags, Some(vec![Flag::Seen]));
assert_eq!(fr.save_date.as_deref(), Some("01-Jan-2025 00:00:00 +0000"));
assert_eq!(fr.rfc822_size, Some(1234));
return;
}
}
panic!("expected Fetch with SAVEDATE and other attrs");
}
#[test]
fn fetch_savedate_unusual_timezone() {
let input = b"* 1 FETCH (SAVEDATE \"31-Dec-2099 23:59:59 -1200\")\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.save_date.as_deref(), Some("31-Dec-2099 23:59:59 -1200"));
return;
}
}
panic!("expected Fetch with SAVEDATE");
}
#[test]
fn bodystructure_deeply_nested() {
let mut input = Vec::new();
input.extend_from_slice(b"* 1 FETCH (BODYSTRUCTURE ");
input.resize(input.len() + 10, b'(');
input.extend_from_slice(b"\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 0 0");
for _ in 0..10 {
input.extend_from_slice(b" \"MIXED\")");
}
input.extend_from_slice(b")\r\n");
let (_, resp) = parse_response(&input).unwrap();
assert!(
matches!(resp, Response::Untagged(_)),
"expected Untagged Fetch"
);
}
#[test]
fn fetch_uid_u32_max() {
let input = b"* 1 FETCH (UID 4294967295)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(u32::MAX));
return;
}
}
panic!("expected Fetch with max UID");
}
#[test]
fn number_overflow_u32() {
assert!(number(b"4294967296").is_err());
}
#[test]
fn number_overflow_u64() {
assert!(number64(b"18446744073709551616").is_err());
}
#[test]
fn exists_large_count() {
let input = b"* 4294967295 EXISTS\r\n";
let (_, resp) = parse_response(input).unwrap();
assert_eq!(
resp,
Response::Untagged(Box::new(UntaggedResponse::Exists(u32::MAX)))
);
}
#[test]
fn fetch_unknown_attribute_skipped_stress() {
let input = b"* 1 FETCH (UID 42 X-CUSTOM-ATTR \"some value\" FLAGS (\\Seen))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(42));
assert_eq!(fr.flags, Some(vec![Flag::Seen]));
return;
}
}
panic!("expected Fetch with unknown attr skipped");
}
#[test]
fn fetch_truncated_returns_error() {
assert!(parse_response(b"* 1 FETCH (UID 42").is_err());
}
#[test]
fn garbage_data_returns_error() {
assert!(parse_response(b"GARBAGE\r\n").is_err());
assert!(parse_response(b"\x00\x01\x02\r\n").is_err());
assert!(parse_response(b"").is_err());
}
#[test]
fn response_code_very_long_atom() {
let long_name: String = "X".repeat(200);
let input = format!("[{long_name}]");
let (_, code) = response_code(input.as_bytes()).unwrap();
match code {
ResponseCode::Other { name, value } => {
assert_eq!(name.len(), 200);
assert!(value.is_none());
}
_ => panic!("expected Other response code"),
}
}
#[test]
fn esearch_unknown_key_skipped_stress() {
let input = b"* ESEARCH (TAG \"A001\") UID XFUTURE somevalue\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Esearch(esearch) = *boxed {
assert!(esearch.all.is_empty(), "unknown key should be skipped");
assert!(esearch.uid);
return;
}
}
panic!("expected Esearch response");
}
#[test]
fn vanished_overlapping_ranges() {
let input = b"* VANISHED (EARLIER) 1:5,3:8,10\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Vanished { earlier, uids } = *boxed {
assert!(earlier);
assert_eq!(uids.len(), 3);
assert_eq!(uids[0], UidRange::range(1, 5));
assert_eq!(uids[1], UidRange::range(3, 8));
assert_eq!(uids[2], UidRange::single(10));
return;
}
}
panic!("expected Vanished response");
}
#[test]
fn vanished_sequence_set_with_star() {
let input = b"* VANISHED 1:5,*\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Vanished { .. } => {
panic!("VANISHED must reject `*` in known-uids (RFC 7162 Section 6)");
}
UntaggedResponse::Unknown(_) => {} other => panic!("unexpected variant: {other:?}"),
},
other => panic!("expected Untagged, got: {other:?}"),
}
}
#[test]
fn vanished_rejects_star_in_known_uids() {
let input = b"* VANISHED *\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Vanished { .. } => {
panic!("VANISHED must reject bare `*` in known-uids (RFC 7162 Section 6)");
}
UntaggedResponse::Unknown(_) => {} other => panic!("unexpected variant: {other:?}"),
},
other => panic!("expected Untagged, got: {other:?}"),
}
let input = b"* VANISHED 1:*\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Vanished { .. } => {
panic!("VANISHED must reject `*` in range in known-uids (RFC 7162 Section 6)");
}
UntaggedResponse::Unknown(_) => {} other => panic!("unexpected variant: {other:?}"),
},
other => panic!("expected Untagged, got: {other:?}"),
}
let input = b"* VANISHED (EARLIER) 1:5,*\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Vanished { .. } => {
panic!("VANISHED EARLIER must reject `*` in known-uids (RFC 7162 Section 6)");
}
UntaggedResponse::Unknown(_) => {} other => panic!("unexpected variant: {other:?}"),
},
other => panic!("expected Untagged, got: {other:?}"),
}
}
#[test]
fn quoted_string_non_ascii_bytes() {
let input =
b"* 1 FETCH (UID 1 ENVELOPE (NIL \"caf\xc3\xa9\" NIL NIL NIL NIL NIL NIL NIL NIL))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(
fr.envelope.as_ref().and_then(|e| e.subject.as_deref()),
Some("café")
);
return;
}
}
panic!("expected Fetch with non-ASCII envelope subject");
}
#[test]
fn fetch_binary_large_origin() {
let input = b"* 1 FETCH (BINARY[1]<4294967295> NIL)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.binary_sections.len(), 1);
assert_eq!(fr.binary_sections[0].origin, Some(u64::from(u32::MAX)));
assert!(fr.binary_sections[0].data.is_none());
return;
}
}
panic!("expected Fetch with large binary origin");
}
#[test]
fn fetch_modseq_i64_max() {
let input = b"* 1 FETCH (MODSEQ (9223372036854775807))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.mod_seq, Some(i64::MAX as u64));
return;
}
}
panic!("expected Fetch with max MODSEQ");
}
#[test]
fn fetch_modseq_u64_max_rejected() {
let input = b"* 1 FETCH (MODSEQ (18446744073709551615))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = &*boxed {
assert_eq!(
fr.mod_seq, None,
"u64::MAX MODSEQ should be skipped (None), not stored"
);
} else {
panic!("expected Fetch with mod_seq=None, got {boxed:?}");
}
} else {
panic!("Expected Untagged, got {resp:?}");
}
}
#[test]
fn response_code_highestmodseq_i64_max() {
let (_, code) = response_code(b"[HIGHESTMODSEQ 9223372036854775807]").unwrap();
assert_eq!(code, ResponseCode::HighestModSeq(i64::MAX as u64));
}
#[test]
fn response_code_highestmodseq_zero_accepted() {
let (_, code) = response_code(b"[HIGHESTMODSEQ 0]").unwrap();
assert_eq!(
code,
ResponseCode::HighestModSeq(0),
"HIGHESTMODSEQ 0 must parse as HighestModSeq(0) per Postel's law \
(RFC 7162 Section 3.1.2 — real servers send this for empty mailboxes)"
);
}
#[test]
fn highestmodseq_zero_in_full_response_preserved() {
let input = b"* OK [HIGHESTMODSEQ 0] Strstrumpf\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Status { code, text, .. } => {
assert_eq!(
code,
Some(ResponseCode::HighestModSeq(0)),
"HIGHESTMODSEQ 0 response code must be preserved structurally, \
not consumed into the text field"
);
assert_eq!(text, "Strstrumpf");
}
other => panic!("expected Status, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn literal8_simple() {
let (rest, val) = literal(b"~{5}\r\nhello rest").unwrap();
assert_eq!(val, b"hello");
assert_eq!(rest, b" rest");
}
#[test]
fn literal8_plus_tolerated() {
let (rest, val) = literal(b"~{3+}\r\nabc rest").unwrap();
assert_eq!(val, b"abc");
assert_eq!(rest, b" rest");
}
#[test]
fn literal8_zero_length() {
let (_, val) = literal(b"~{0}\r\n").unwrap();
assert!(val.is_empty());
}
#[test]
fn literal8_utf8_content() {
let data = "日本語";
let header = format!("~{{{}}}\r\n{}", data.len(), data);
let (_, val) = literal(header.as_bytes()).unwrap();
assert_eq!(val, data.as_bytes());
}
#[test]
fn spec_audit_literal8_tolerates_non_sync() {
assert!(
literal(b"{5+}\r\nhello").is_ok(),
"LITERAL+ must be accepted"
);
assert!(
literal(b"~{5}\r\nhello").is_ok(),
"literal8 must be accepted"
);
assert!(
literal(b"~{5+}\r\nhello").is_ok(),
"literal8 with + must be tolerated per Postel's law"
);
}
#[test]
fn nstring_literal8() {
let (_, val) = nstring(b"~{4}\r\ntest").unwrap();
assert_eq!(val, Some(b"test".to_vec()));
}
#[test]
fn envelope_utf8_mode_subject_passthrough() {
let input = b"(\"Mon, 1 Jan 2024\" \"=?UTF-8?B?QWxpY2U=?=\" \
((\"Sender\" NIL \"s\" \"x.com\")) \
((\"Sender\" NIL \"s\" \"x.com\")) \
((\"Sender\" NIL \"s\" \"x.com\")) \
((\"To\" NIL \"t\" \"x.com\")) \
NIL NIL NIL \"<id@x.com>\")";
let (_, env) = envelope(input, true).unwrap();
assert_eq!(env.subject.as_deref(), Some("=?UTF-8?B?QWxpY2U=?="));
}
#[test]
fn envelope_non_utf8_mode_subject_decoded() {
let input = b"(\"Mon, 1 Jan 2024\" \"=?UTF-8?B?QWxpY2U=?=\" \
((\"Sender\" NIL \"s\" \"x.com\")) \
((\"Sender\" NIL \"s\" \"x.com\")) \
((\"Sender\" NIL \"s\" \"x.com\")) \
((\"To\" NIL \"t\" \"x.com\")) \
NIL NIL NIL \"<id@x.com>\")";
let (_, env) = envelope(input, false).unwrap();
assert_eq!(env.subject.as_deref(), Some("Alice"));
}
#[test]
fn address_utf8_mode_name_passthrough() {
let (_, addr) = address(
b"(\"=?UTF-8?B?QWxpY2U=?=\" NIL \"alice\" \"example.com\")",
true,
)
.unwrap();
assert_eq!(addr.name.as_deref(), Some("=?UTF-8?B?QWxpY2U=?="));
}
#[test]
fn address_non_utf8_mode_name_decoded() {
let (_, addr) = address(
b"(\"=?UTF-8?B?QWxpY2U=?=\" NIL \"alice\" \"example.com\")",
false,
)
.unwrap();
assert_eq!(addr.name.as_deref(), Some("Alice"));
}
#[test]
fn address_utf8_mode_raw_utf8_name() {
let raw_name = "日本太郎";
let input = format!("(\"{raw_name}\" NIL \"taro\" \"example.jp\")");
let (_, addr) = address(input.as_bytes(), true).unwrap();
assert_eq!(addr.name.as_deref(), Some("日本太郎"));
}
#[test]
fn parse_response_utf8_fetch_envelope() {
let input = b"* 1 FETCH (ENVELOPE (\"Mon, 1 Jan 2024\" \"=?UTF-8?B?QWxpY2U=?=\" \
((\"=?UTF-8?B?QWxpY2U=?=\" NIL \"a\" \"x.com\")) \
((\"=?UTF-8?B?QWxpY2U=?=\" NIL \"a\" \"x.com\")) \
((\"=?UTF-8?B?QWxpY2U=?=\" NIL \"a\" \"x.com\")) \
NIL NIL NIL NIL \"<id@x.com>\"))\r\n";
let (_, resp) = parse_response_utf8(input, true).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Fetch(fr) = *u {
let env = fr.envelope.unwrap();
assert_eq!(env.subject.as_deref(), Some("=?UTF-8?B?QWxpY2U=?="));
assert_eq!(env.from[0].name.as_deref(), Some("=?UTF-8?B?QWxpY2U=?="));
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn parse_response_utf8_false_decodes_rfc2047() {
let input = b"* 1 FETCH (ENVELOPE (\"Mon, 1 Jan 2024\" \"=?UTF-8?B?QWxpY2U=?=\" \
((\"=?UTF-8?B?QWxpY2U=?=\" NIL \"a\" \"x.com\")) \
((\"=?UTF-8?B?QWxpY2U=?=\" NIL \"a\" \"x.com\")) \
((\"=?UTF-8?B?QWxpY2U=?=\" NIL \"a\" \"x.com\")) \
NIL NIL NIL NIL \"<id@x.com>\"))\r\n";
let (_, resp) = parse_response_utf8(input, false).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Fetch(fr) = *u {
let env = fr.envelope.unwrap();
assert_eq!(env.subject.as_deref(), Some("Alice"));
assert_eq!(env.from[0].name.as_deref(), Some("Alice"));
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn fetch_literal8_body_section() {
let body = b"Hello world";
let input = format!(
"(BODY[] ~{{{}}}\r\n{})",
body.len(),
std::str::from_utf8(body).unwrap()
);
let (_, fr) = fetch_response_inner(input.as_bytes(), false).unwrap();
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].data, Some(body.to_vec()));
}
#[test]
fn fetch_envelope_literal8_subject() {
let subject = "テスト件名";
let lit = format!("~{{{}}}\r\n{}", subject.len(), subject);
let input = format!(
"(ENVELOPE (\"date\" {lit} \
((\"From\" NIL \"f\" \"x.com\")) \
((\"From\" NIL \"f\" \"x.com\")) \
((\"From\" NIL \"f\" \"x.com\")) \
NIL NIL NIL NIL \"<id@x.com>\"))"
);
let (_, fr) = fetch_response_inner(input.as_bytes(), true).unwrap();
let env = fr.envelope.unwrap();
assert_eq!(env.subject.as_deref(), Some("テスト件名"));
}
#[test]
fn address_list_accepts_empty_parens() {
let (rest, addrs) = address_list(b"()", false).unwrap();
assert!(rest.is_empty());
assert!(addrs.is_empty());
}
#[test]
fn address_list_accepts_nil() {
let (rest, addrs) = address_list(b"NIL", false).unwrap();
assert!(rest.is_empty());
assert!(addrs.is_empty());
}
#[test]
fn esearch_min_rejects_zero() {
let input = b"* ESEARCH (TAG \"A1\") MIN 0\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => {
assert!(
matches!(*boxed, UntaggedResponse::Unknown(_)),
"ESEARCH MIN 0 should fall through to Unknown, got {boxed:?}"
);
}
other => panic!("Expected Untagged(Unknown), got {other:?}"),
}
}
#[test]
fn esearch_max_rejects_zero() {
let input = b"* ESEARCH (TAG \"A1\") MAX 0\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => {
assert!(
matches!(*boxed, UntaggedResponse::Unknown(_)),
"ESEARCH MAX 0 should fall through to Unknown, got {boxed:?}"
);
}
other => panic!("Expected Untagged(Unknown), got {other:?}"),
}
}
#[test]
fn esearch_min_accepts_one() {
let input = b"* ESEARCH (TAG \"A1\") MIN 1\r\n";
let result = parse_response(input);
assert!(result.is_ok());
}
#[test]
fn list_delimiter_multibyte_utf8() {
let input = b"LIST (\\HasNoChildren) \"\xC3\xB7\" INBOX\r\n";
let (_, resp) = parse_untagged_list(input, false).unwrap();
if let UntaggedResponse::List(info) = resp {
assert_eq!(
info.delimiter,
Some('\u{00F7}'),
"multi-byte UTF-8 delimiter should be decoded as full char, not first byte"
);
} else {
panic!("expected List response");
}
}
#[test]
fn list_delimiter_single_char_valid() {
let input = b"LIST () \"/\" INBOX\r\n";
let (_, resp) = parse_untagged_list(input, false).unwrap();
if let UntaggedResponse::List(info) = resp {
assert_eq!(info.delimiter, Some('/'));
} else {
panic!("expected List response");
}
}
#[test]
fn list_delimiter_nil_valid() {
let input = b"LIST () NIL INBOX\r\n";
let (_, resp) = parse_untagged_list(input, false).unwrap();
if let UntaggedResponse::List(info) = resp {
assert_eq!(info.delimiter, None);
} else {
panic!("expected List response");
}
}
#[test]
fn list_delimiter_multi_char_tolerant() {
let input = b"LIST () \"//\" INBOX\r\n";
let (_, resp) = parse_untagged_list(input, false).expect(
"multi-character delimiter must be tolerated per Postel's law \
(RFC 3501 Section 9): take the first char, not a hard error",
);
if let UntaggedResponse::List(info) = resp {
assert_eq!(
info.delimiter,
Some('/'),
"multi-char delimiter should take the first character"
);
} else {
panic!("expected List response");
}
}
#[test]
fn tagged_response_rejects_plus_in_tag() {
let result = parse_response(b"A+01 OK done\r\n");
assert!(
result.is_err(),
"tag containing '+' should be rejected per RFC 3501 Section 9"
);
}
#[test]
fn tagged_response_rejects_bare_plus_tag() {
let result = parse_response(b"+ OK done\r\n");
if let Ok((_, Response::Tagged(t))) = result {
panic!("'+' should not be accepted as a tag, got tagged response: {t:?}");
}
}
#[test]
fn list_with_oldname_extended_data() {
let input = b"LIST () \"/\" \"NewMailbox\" (\"OLDNAME\" (\"OldMailbox\"))\r\n";
let result = parse_untagged_list(input, false);
assert!(
result.is_ok(),
"LIST with OLDNAME extended data should parse successfully \
per RFC 5258 / RFC 9051 Section 6.3.9.7, got: {result:?}"
);
let (_, resp) = result.unwrap();
if let UntaggedResponse::List(info) = resp {
assert_eq!(info.name.as_str(), "NewMailbox");
assert_eq!(info.delimiter, Some('/'));
} else {
panic!("expected List response, got {resp:?}");
}
}
#[test]
fn list_with_childinfo_extended_data() {
let input = b"LIST (\\HasChildren) \"/\" \"INBOX\" (\"CHILDINFO\" (\"SUBSCRIBED\"))\r\n";
let result = parse_untagged_list(input, false);
assert!(
result.is_ok(),
"LIST with CHILDINFO extended data should parse successfully \
per RFC 5258, got: {result:?}"
);
}
#[test]
fn search_with_modseq_trailing() {
let input = b"* SEARCH 2 5 6 7 11 12 18 19 20 23 (MODSEQ 917162500)\r\n";
let result = parse_response(input);
assert!(
result.is_ok(),
"SEARCH with trailing (MODSEQ n) should parse per RFC 7162 Section 3.1.5, \
got: {result:?}"
);
let (_, resp) = result.unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Search { uids, mod_seq } = *u {
assert_eq!(uids, vec![2, 5, 6, 7, 11, 12, 18, 19, 20, 23]);
assert_eq!(mod_seq, Some(917_162_500));
} else {
panic!("expected Search, got {u:?}");
}
} else {
panic!("expected untagged response");
}
}
#[test]
fn search_modseq_rejects_zero() {
let input = b"* SEARCH 1 5 (MODSEQ 0)\r\n";
let (_, resp) = parse_response(input)
.expect("SEARCH with MODSEQ 0 must still parse — UIDs must be preserved");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Search { uids, mod_seq } = &*u {
assert_eq!(*uids, vec![1, 5], "UIDs must be preserved");
assert_ne!(
*mod_seq,
Some(0),
"SEARCH MODSEQ 0 must be rejected per RFC 7162 Section 3.1.6"
);
} else {
panic!("expected Search, got {u:?} — must not fall through to Unknown");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn search_modseq_accepts_one() {
let input = b"* SEARCH 1 5 (MODSEQ 1)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Search { uids, mod_seq } = *u {
assert_eq!(uids, vec![1, 5]);
assert_eq!(
mod_seq,
Some(1),
"SEARCH MODSEQ 1 must be accepted per RFC 7162 Section 3.1.6"
);
} else {
panic!("expected Search, got {u:?}");
}
} else {
panic!("expected untagged response");
}
}
#[test]
fn esearch_with_modseq_result() {
let input = b"ESEARCH (TAG \"A002\") UID ALL 2,10:15 MODSEQ 917162500\r\n";
let result = parse_untagged_esearch(input);
assert!(
result.is_ok(),
"ESEARCH with MODSEQ result should parse per RFC 7162 Section 3.1.10, \
got: {result:?}"
);
let (_, resp) = result.unwrap();
if let UntaggedResponse::Esearch(esearch) = resp {
assert_eq!(
esearch.mod_seq,
Some(917_162_500),
"ESEARCH MODSEQ value must be retained per RFC 7162 Section 3.1.10"
);
} else {
panic!("expected Esearch, got {resp:?}");
}
}
#[test]
fn spec_audit_esearch_all_accepts_wildcard() {
let input = b"* ESEARCH (TAG \"A1\") ALL 1:*\r\n";
let (rem, resp) = parse_response(input).unwrap();
assert!(rem.is_empty());
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(es) = &*u {
assert_eq!(es.all.len(), 1);
assert_eq!(es.all[0].start, 1);
assert_eq!(es.all[0].end, Some(u32::MAX));
} else {
panic!("Expected Esearch, got {u:?}");
}
} else {
panic!("Expected Untagged response");
}
}
#[test]
fn esearch_uid_keyword_boundary() {
let input = b"ESEARCH (TAG \"A1\") UIDFOO 42 COUNT 3\r\n";
let result = parse_untagged_esearch(input);
assert!(
result.is_ok(),
"ESEARCH with atom starting with 'UID' must not fail \
(RFC 4731 Section 3.1 token boundary), got: {result:?}"
);
let (_, resp) = result.unwrap();
if let UntaggedResponse::Esearch(es) = resp {
assert!(
!es.uid,
"uid must be false when the atom is 'UIDFOO', not 'UID'"
);
assert_eq!(
es.count,
Some(3),
"COUNT must still be parsed after skipping unknown 'UIDFOO' key"
);
} else {
panic!("expected Esearch, got {resp:?}");
}
}
#[test]
fn namespace_nested_extension_data() {
let input = b"NAMESPACE ((\"\" \"/\" \"X-PARAM\" (\"FLAG1\" \"FLAG2\"))) NIL NIL\r\n";
let result = parse_untagged_namespace(input, false);
assert!(
result.is_ok(),
"NAMESPACE with nested extension parens should parse per RFC 2342, \
got: {result:?}"
);
let (_, resp) = result.unwrap();
if let UntaggedResponse::Namespace {
personal,
other,
shared,
} = resp
{
assert_eq!(personal.len(), 1, "should have one personal namespace");
assert_eq!(personal[0].prefix, "");
assert_eq!(personal[0].delimiter, Some('/'));
assert!(other.is_empty());
assert!(shared.is_empty());
} else {
panic!("expected Namespace, got {resp:?}");
}
}
#[test]
fn namespace_multiple_descriptors_nested_ext() {
let input =
b"NAMESPACE ((\"\" \"/\")(\"#mh/\" \"/\" \"X-PARAM\" (\"FLAG1\" \"FLAG2\"))) NIL NIL\r\n";
let result = parse_untagged_namespace(input, false);
assert!(
result.is_ok(),
"NAMESPACE with multiple descriptors and nested extensions should parse, \
got: {result:?}"
);
let (_, resp) = result.unwrap();
if let UntaggedResponse::Namespace { personal, .. } = resp {
assert_eq!(personal.len(), 2);
assert_eq!(personal[0].prefix, "");
assert_eq!(personal[1].prefix, "#mh/");
} else {
panic!("expected Namespace, got {resp:?}");
}
}
#[test]
fn namespace_extension_data_preserved() {
let input = b"NAMESPACE ((\"\" \"/\" \"TRANSLATION\" (\"strstrumpf\"))) NIL NIL\r\n";
let (_, resp) = parse_untagged_namespace(input, false).unwrap();
if let UntaggedResponse::Namespace {
personal,
other,
shared,
} = resp
{
assert_eq!(personal.len(), 1);
assert_eq!(personal[0].prefix, "");
assert_eq!(personal[0].delimiter, Some('/'));
assert_eq!(
personal[0].extensions,
vec![("TRANSLATION".to_string(), vec!["strstrumpf".to_string()])]
);
assert!(other.is_empty());
assert!(shared.is_empty());
} else {
panic!("expected Namespace, got {resp:?}");
}
}
#[test]
fn namespace_multiple_extensions_preserved() {
let input = b"NAMESPACE ((\"\" \"/\" \"TRANSLATION\" (\"strstrumpf\") \"X-PARAM\" (\"FLAG1\" \"FLAG2\"))) NIL NIL\r\n";
let (_, resp) = parse_untagged_namespace(input, false).unwrap();
if let UntaggedResponse::Namespace { personal, .. } = resp {
assert_eq!(personal.len(), 1);
assert_eq!(personal[0].extensions.len(), 2);
assert_eq!(
personal[0].extensions[0],
("TRANSLATION".to_string(), vec!["strstrumpf".to_string()])
);
assert_eq!(
personal[0].extensions[1],
(
"X-PARAM".to_string(),
vec!["FLAG1".to_string(), "FLAG2".to_string()]
)
);
} else {
panic!("expected Namespace, got {resp:?}");
}
}
#[test]
fn namespace_no_extension_data_empty_vec() {
let input = b"NAMESPACE ((\"\" \"/\")) NIL NIL\r\n";
let (_, resp) = parse_untagged_namespace(input, false).unwrap();
if let UntaggedResponse::Namespace { personal, .. } = resp {
assert_eq!(personal.len(), 1);
assert!(personal[0].extensions.is_empty());
} else {
panic!("expected Namespace, got {resp:?}");
}
}
#[test]
fn atom_accepts_open_bracket() {
let (rest, val) = atom(b"BODY[TEXT] rest").unwrap();
assert_eq!(val, b"BODY[TEXT");
assert_eq!(rest, b"] rest");
}
#[test]
fn atom_still_stops_at_close_bracket() {
let (rest, val) = atom(b"FOO]bar").unwrap();
assert_eq!(val, b"FOO");
assert_eq!(rest, b"]bar");
}
#[test]
fn fetch_attr_atom_stops_at_open_bracket() {
let (rest, val) = fetch_attr_atom(b"BODY[TEXT] rest").unwrap();
assert_eq!(val, b"BODY");
assert_eq!(rest, b"[TEXT] rest");
}
#[test]
fn fetch_attr_atom_no_bracket() {
let (rest, val) = fetch_attr_atom(b"FLAGS rest").unwrap();
assert_eq!(val, b"FLAGS");
assert_eq!(rest, b" rest");
}
#[test]
fn search_accepts_zero_in_results() {
let input = b"* SEARCH 1 0 5\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Search { uids, mod_seq } = &*u {
assert_eq!(
*uids,
vec![1, 5],
"0 must be filtered; valid UIDs must be preserved"
);
assert_eq!(*mod_seq, None);
} else {
panic!("expected Search response, got {u:?} — valid UIDs must not be lost");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn search_empty_results_still_works() {
let input = b"* SEARCH\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Search { uids, .. } = *boxed {
assert!(uids.is_empty());
return;
}
}
panic!("expected Search");
}
#[test]
fn fetch_uid_zero_rejected() {
let input = b"* 1 FETCH (UID 0)\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => {
assert!(
matches!(*boxed, UntaggedResponse::Unknown(_)),
"FETCH UID 0 should fall through to Unknown, got {boxed:?}"
);
}
other => panic!("Expected Untagged(Unknown), got {other:?}"),
}
}
#[test]
fn uid_range_zero_start_rejected() {
assert!(uid_range(b"0:5").is_err());
}
#[test]
fn uid_range_zero_end_rejected() {
assert!(uid_range(b"1:0").is_err());
}
#[test]
fn uid_range_reversed_normalized() {
let (_, r) = uid_range(b"100:1").unwrap();
assert_eq!(r.start, 1, "reversed range must normalize start to min");
assert_eq!(r.end, Some(100), "reversed range must normalize end to max");
}
#[test]
fn seq_range_reversed_normalized() {
let (_, r) = seq_range(b"100:1").unwrap();
assert_eq!(
r.start, 1,
"seq_range reversed range must normalize start to min"
);
assert_eq!(
r.end,
Some(100),
"seq_range reversed range must normalize end to max"
);
}
#[test]
fn seq_range_wildcard_reversed_normalized() {
let (_, r) = seq_range(b"*:1").unwrap();
assert_eq!(r.start, 1, "seq_range *:1 must normalize start to 1");
assert_eq!(
r.end,
Some(u32::MAX),
"seq_range *:1 must normalize end to u32::MAX (wildcard sentinel)"
);
}
#[test]
fn response_code_uidvalidity_zero_accepted() {
let (_, code) = response_code(b"[UIDVALIDITY 0]").unwrap();
assert_eq!(
code,
ResponseCode::UidValidity(0),
"UIDVALIDITY 0 must parse per Postel's law, matching STATUS tolerance"
);
}
#[test]
fn response_code_uidnext_zero_accepted() {
let (_, code) = response_code(b"[UIDNEXT 0]").unwrap();
assert_eq!(
code,
ResponseCode::UidNext(0),
"UIDNEXT 0 must parse per Postel's law, matching STATUS tolerance"
);
}
#[test]
fn response_code_unseen_zero_accepted() {
let (_, code) = response_code(b"[UNSEEN 0]").unwrap();
assert_eq!(
code,
ResponseCode::Unseen(0),
"UNSEEN 0 must parse as Unseen(0) per Postel's law \
(RFC 3501 Section 7.1 — real servers send this for empty mailboxes)"
);
}
#[test]
fn unseen_zero_in_full_response_preserved() {
let input = b"* OK [UNSEEN 0] No unseen messages\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Status { code, text, .. } => {
assert_eq!(
code,
Some(ResponseCode::Unseen(0)),
"UNSEEN 0 response code must be preserved structurally, \
not consumed into the text field"
);
assert_eq!(text, "No unseen messages");
}
other => panic!("expected Status, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn response_code_appenduid_zero_uidvalidity_accepted() {
let (_, code) = response_code(b"[APPENDUID 0 5678]")
.expect("APPENDUID with uidvalidity 0 must be accepted");
assert_eq!(
code,
ResponseCode::AppendUid {
uid_validity: 0,
uids: vec![UidRange::single(5678)],
}
);
}
#[test]
fn uid_set_empty_rejected() {
assert!(uid_set(b"").is_err());
}
#[test]
fn response_code_appenduid_empty_uid_set_rejected() {
assert!(response_code(b"[APPENDUID 1234 ]").is_err());
}
#[test]
fn response_code_appenduid_valid_uid_set_with_ranges() {
let (_, code) = response_code(b"[APPENDUID 1234 5:10,15]").unwrap();
if let ResponseCode::AppendUid { uid_validity, uids } = code {
assert_eq!(uid_validity, 1234);
assert_eq!(uids.len(), 2);
assert_eq!(uids[0], UidRange::range(5, 10));
assert_eq!(uids[1], UidRange::single(15));
} else {
panic!("expected AppendUid response code");
}
}
#[test]
fn response_code_copyuid_zero_uidvalidity_accepted() {
let (_, code) = response_code(b"[COPYUID 0 1:5 10:14]")
.expect("COPYUID with uidvalidity 0 must be accepted");
if let ResponseCode::CopyUid { uid_validity, .. } = &code {
assert_eq!(*uid_validity, 0);
} else {
panic!("expected CopyUid, got {code:?}");
}
}
#[test]
fn expunge_zero_rejected() {
let input = b"* 0 EXPUNGE\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => {
assert!(
matches!(*boxed, UntaggedResponse::Unknown(_)),
"EXPUNGE 0 should fall through to Unknown, got {boxed:?}"
);
}
other => panic!("Expected Untagged(Unknown), got {other:?}"),
}
}
#[test]
fn fetch_seq_zero_rejected() {
let input = b"* 0 FETCH (UID 1 FLAGS (\\Seen))\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => {
assert!(
matches!(*boxed, UntaggedResponse::Unknown(_)),
"FETCH seq 0 should fall through to Unknown, got {boxed:?}"
);
}
other => panic!("Expected Untagged(Unknown), got {other:?}"),
}
}
#[test]
fn exists_zero_still_valid() {
let input = b"* 0 EXISTS\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
assert_eq!(*boxed, UntaggedResponse::Exists(0));
} else {
panic!("expected Exists(0)");
}
}
#[test]
fn recent_zero_still_valid() {
let input = b"* 0 RECENT\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
assert_eq!(*boxed, UntaggedResponse::Recent(0));
} else {
panic!("expected Recent(0)");
}
}
#[test]
fn continuation_bare_plus_crlf() {
let input = b"+\r\n";
let (remaining, response) = parse_response(input).unwrap();
assert!(remaining.is_empty());
match response {
Response::Continuation(c) => assert_eq!(c.data, ""),
_ => panic!("expected Continuation"),
}
}
#[test]
fn continuation_with_space_and_text() {
let input = b"+ Ready for literal\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Continuation(c) => assert_eq!(c.data, "Ready for literal"),
_ => panic!("expected Continuation"),
}
}
#[test]
fn continuation_bare_plus_base64() {
let input = b"+ dGVzdA==\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Continuation(c) => assert_eq!(c.data, "dGVzdA=="),
_ => panic!("expected Continuation"),
}
}
#[test]
fn bodystructure_message_global() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"MESSAGE\" \"GLOBAL\" (\"charset\" \"utf-8\") NIL NIL \"7BIT\" 500 (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL) (\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 100 5) 20))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
match fr.body_structure.unwrap() {
BodyStructure::Message { lines, size, .. } => {
assert_eq!(lines, 20);
assert_eq!(size, 500);
}
other => panic!("expected Message variant, got {other:?}"),
}
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn bodystructure_ext_nested_parens() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" (\"charset\" \"utf-8\") NIL NIL \"7BIT\" 100 5 NIL NIL NIL NIL (\"ext\" (\"nested\" \"value\"))))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert!(fr.body_structure.is_some());
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn bodystructure_mpart_ext_nested_parens() {
let input = b"* 1 FETCH (BODYSTRUCTURE ((\"TEXT\" \"PLAIN\" (\"charset\" \"utf-8\") NIL NIL \"7BIT\" 100 5) \"MIXED\" (\"boundary\" \"----=_Part\") NIL NIL NIL (\"future-ext\" (\"a\" \"b\"))))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert!(fr.body_structure.is_some());
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn fetch_rfc822_size_large_u64() {
let input = b"* 1 FETCH (RFC822.SIZE 5000000000)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.rfc822_size, Some(5_000_000_000u64));
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn bodystructure_text_lines_u64() {
let input =
b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 100 5000000000))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
match fr.body_structure.unwrap() {
BodyStructure::Text { lines, .. } => assert_eq!(lines, 5_000_000_000u64),
other => panic!("expected Text, got {other:?}"),
}
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn fetch_rfc822_full_message() {
let input = b"* 1 FETCH (RFC822 \"From: a@b.com\\r\\nHello\")\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].section, "");
assert!(fr.body_sections[0].data.is_some());
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn fetch_rfc822_header() {
let input = b"* 1 FETCH (RFC822.HEADER \"Subject: Test\\r\\n\")\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].section, "HEADER");
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn fetch_rfc822_text() {
let input = b"* 1 FETCH (RFC822.TEXT \"Body content\")\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].section, "TEXT");
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn status_deleted_item() {
let input = b"* STATUS INBOX (MESSAGES 10 DELETED 3)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::MailboxStatus { items, .. } = *boxed {
assert!(items.iter().any(|i| matches!(i, StatusItem::Deleted(3))));
} else {
panic!("expected MailboxStatus");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn status_uidnext_zero_accepted() {
let input = b"* STATUS INBOX (UIDNEXT 0)\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => {
if let UntaggedResponse::MailboxStatus { mailbox, items } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(items, vec![StatusItem::UidNext(0)]);
} else {
panic!("expected MailboxStatus, got {boxed:?}");
}
}
other => panic!("Expected Untagged(MailboxStatus), got {other:?}"),
}
}
#[test]
fn status_uidvalidity_zero_accepted() {
let input = b"* STATUS INBOX (UIDVALIDITY 0)\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => {
if let UntaggedResponse::MailboxStatus { mailbox, items } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(items, vec![StatusItem::UidValidity(0)]);
} else {
panic!("expected MailboxStatus, got {boxed:?}");
}
}
other => panic!("Expected Untagged(MailboxStatus), got {other:?}"),
}
}
#[test]
fn special_use_all_attribute() {
use crate::types::mailbox::{MailboxAttribute, MailboxInfo, SpecialUse};
let info = MailboxInfo {
name: MailboxName::new("All Mail").unwrap(),
delimiter: Some('/'),
attributes: vec![MailboxAttribute::All],
..Default::default()
};
assert_eq!(info.special_use(), Some(SpecialUse::All));
}
#[test]
fn namespace_multibyte_delimiter() {
let mut input: Vec<u8> = b"* NAMESPACE ((\"\" ".to_vec();
input.push(b'"');
input.extend_from_slice(&[0xC3, 0xB7]); input.push(b'"');
input.extend_from_slice(b")) NIL NIL\r\n");
let (_, resp) = parse_response(&input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Namespace { personal, .. } = *boxed {
assert_eq!(personal[0].delimiter, Some('\u{00F7}'));
} else {
panic!("expected Namespace");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn response_code_uidnotsticky() {
let input = b"A001 OK [UIDNOTSTICKY] done\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::UidNotSticky));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_notsaved() {
let input = b"A001 NO [NOTSAVED] search result empty\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::NotSaved));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_haschildren() {
let input = b"A001 NO [HASCHILDREN] mailbox has children\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::HasChildren));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_unknowncte() {
let input = b"A001 NO [UNKNOWN-CTE] unknown content-transfer-encoding\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::UnknownCte));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn spec_audit_unknown_cte_response_code_has_hyphen() {
let input = b"* NO [UNKNOWN-CTE] unsupported encoding\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Status { code, .. } = &*u {
assert_eq!(
code.as_ref().unwrap(),
&ResponseCode::UnknownCte,
"UNKNOWN-CTE (with hyphen) should map to ResponseCode::UnknownCte"
);
} else {
panic!("expected Status, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn response_code_toobig() {
let (_, code) = response_code(b"[TOOBIG]").unwrap();
assert_eq!(code, ResponseCode::TooBig);
}
#[test]
fn response_code_toobig_full_response() {
let input = b"A001 NO [TOOBIG] Message too large\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(t.code, Some(ResponseCode::TooBig));
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn response_code_compressionactive() {
let input = b"A001 NO [COMPRESSIONACTIVE] DEFLATE active via TLS\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Tagged(t) = resp {
assert_eq!(
t.code,
Some(ResponseCode::CompressionActive),
"COMPRESSIONACTIVE must be parsed as a typed variant, \
not Other (RFC 4978 Section 3)"
);
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn spec_audit_useattr_response_code() {
let input = b"* NO [USEATTR] \\All not supported\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Status { code, .. } = *boxed {
assert_eq!(
code,
Some(ResponseCode::UseAttr),
"USEATTR must be parsed as a typed variant, \
not Other (RFC 6154 Section 6)"
);
} else {
panic!("expected Status, got {boxed:?}");
}
} else {
panic!("expected Untagged, got {resp:?}");
}
}
#[test]
fn capability_utf8_only() {
let input = b"* CAPABILITY IMAP4rev1 UTF8=ONLY\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Capability(caps) = *boxed {
assert!(caps.contains(&Capability::Imap4Rev1));
assert!(caps.contains(&Capability::Utf8Only));
} else {
panic!("expected Capability, got {boxed:?}");
}
} else {
panic!("expected Untagged, got {resp:?}");
}
}
#[test]
fn quoted_string_lenient_escapes() {
let input = b"\"hello\\nworld\"\r\n";
let (_, val) = super::quoted_string(input).unwrap();
assert_eq!(val, b"hello\\nworld");
}
#[test]
fn namespace_with_extension_data_l13() {
let input = b"* NAMESPACE ((\"\" \"/\" \"X-PARAM\" (\"FLAG1\" \"FLAG2\"))) NIL NIL\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Namespace { personal, .. } = *boxed {
assert_eq!(personal.len(), 1);
assert_eq!(personal[0].prefix, "");
assert_eq!(personal[0].delimiter, Some('/'));
} else {
panic!("expected Namespace");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn number64_rejects_above_i64_max() {
let input = b"* 1 FETCH (MODSEQ (18446744073709551615))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = &*boxed {
assert_eq!(
fr.mod_seq, None,
"u64::MAX MODSEQ should be skipped (None), not stored"
);
} else {
panic!("expected Fetch with mod_seq=None, got {boxed:?}");
}
} else {
panic!("Expected Untagged, got {resp:?}");
}
}
#[test]
fn regression_literal_count_exceeds_number64_range() {
let input = b"{9223372036854775808}\r\n";
let result = literal(input);
assert!(
result.is_err(),
"literal count exceeding i64::MAX must be rejected \
(RFC 9051 Section 9: number64 = 0..2^63-1); got: {result:?}"
);
}
#[test]
fn spec_audit_m1_unknown_cte_hyphenated() {
let input = b"* OK [UNKNOWN-CTE] unsupported encoding\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Status { code, .. } = &*u {
assert_eq!(
code.as_ref().unwrap(),
&ResponseCode::UnknownCte,
"UNKNOWN-CTE (with hyphen) should map to ResponseCode::UnknownCte, \
not Other"
);
} else {
panic!("expected Status, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn spec_audit_m2_esearch_parenthesized_unknown_value() {
let input = b"* ESEARCH (TAG \"A001\") XFOO (1 2) COUNT 5\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(
esearch.count,
Some(5),
"COUNT after an unknown key with parenthesized value should be parsed"
);
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn spec_audit_m5_sort_response() {
let input = b"* SORT 2 3 6\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Sort { nums, mod_seq } = &*u {
assert_eq!(nums, &[2, 3, 6], "SORT response should contain [2, 3, 6]");
assert_eq!(*mod_seq, None);
} else {
panic!("expected Sort, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn sort_with_modseq() {
let (_, resp) = parse_response(b"* SORT 2 5 6 (MODSEQ 917162500)\r\n").unwrap();
if let Response::Untagged(u) = resp {
match &*u {
UntaggedResponse::Sort { nums, mod_seq } => {
assert_eq!(nums, &[2, 5, 6]);
assert_eq!(*mod_seq, Some(917162500));
}
other => panic!("expected Sort, got {other:?}"),
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn spec_audit_l4_number64_exceeds_63bit() {
let result = number64(b"9999999999999999999 rest");
assert!(
result.is_err(),
"number64 should reject values > 2^63-1 per RFC 9051 Section 4, \
but it accepted the value"
);
}
#[test]
fn spec_audit_l7_status_unknown_attribute_skipped() {
let input = b"* STATUS \"INBOX\" (MESSAGES 10 XFOO 42 UNSEEN 3)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::MailboxStatus { mailbox, items } = &*u {
assert_eq!(mailbox.as_str(), "INBOX");
let messages = items.iter().find(|i| matches!(i, StatusItem::Messages(_)));
let unseen = items.iter().find(|i| matches!(i, StatusItem::Unseen(_)));
assert_eq!(
messages,
Some(&StatusItem::Messages(10)),
"MESSAGES should be parsed despite unknown XFOO"
);
assert_eq!(
unseen,
Some(&StatusItem::Unseen(3)),
"UNSEEN should be parsed despite unknown XFOO"
);
} else {
panic!("expected MailboxStatus, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn spec_audit_status_unknown_parenthesized_value() {
let input = b"* STATUS \"INBOX\" (MESSAGES 17 XFUTURE (some data) UNSEEN 3)\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(u) => match *u {
UntaggedResponse::MailboxStatus {
ref mailbox,
ref items,
} => {
assert_eq!(mailbox.as_str(), "INBOX");
let messages = items.iter().find_map(|i| match i {
StatusItem::Messages(n) => Some(*n),
_ => None,
});
let unseen = items.iter().find_map(|i| match i {
StatusItem::Unseen(n) => Some(*n),
_ => None,
});
assert_eq!(messages, Some(17), "MESSAGES should be parsed");
assert_eq!(
unseen,
Some(3),
"UNSEEN after unknown parenthesized value should be parsed"
);
}
other => panic!("expected MailboxStatus, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn spec_audit_status_unknown_nested_parenthesized_value() {
let input = b"* STATUS \"INBOX\" (MESSAGES 5 XCOMPLEX (a (b c) d) UIDNEXT 100)\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(u) => match *u {
UntaggedResponse::MailboxStatus { ref items, .. } => {
let messages = items.iter().find_map(|i| match i {
StatusItem::Messages(n) => Some(*n),
_ => None,
});
let uidnext = items.iter().find_map(|i| match i {
StatusItem::UidNext(n) => Some(*n),
_ => None,
});
assert_eq!(messages, Some(5));
assert_eq!(
uidnext,
Some(100),
"UIDNEXT after nested parenthesized value should be parsed"
);
}
other => panic!("expected MailboxStatus, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn spec_audit_l15_body_extension_literal() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") \
NIL NIL \"7BIT\" 100 5 NIL NIL NIL NIL {3}\r\na)b))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Fetch(_fr) = &*u {
} else {
panic!("expected Fetch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn spec_audit_l3_nonstandard_escape_accepted() {
let input = b"\"hello\\nworld\"";
let (rest, val) = quoted_string(input).unwrap();
assert!(rest.is_empty());
assert_eq!(val, b"hello\\nworld");
}
#[test]
fn spec_audit_l5_continuation_without_sp() {
let input = b"+\r\n";
let (rest, cont) = parse_continuation(input).unwrap();
assert!(rest.is_empty());
assert_eq!(cont.data, "");
}
#[test]
fn spec_audit_l6_atom_with_high_bytes() {
let input = b"\xc0\xe9abc rest";
let (rest, val) = atom(input).unwrap();
assert_eq!(rest, b" rest");
assert_eq!(val, b"\xc0\xe9abc");
}
#[test]
fn audit_finding4_list_extended_oldname_parses_without_error() {
let input = b"* LIST (\\HasNoChildren) \"/\" \"NewName\" (\"OLDNAME\" (\"OldName\"))\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty(), "should consume entire input");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = &*u {
assert_eq!(info.name.as_str(), "NewName");
assert_eq!(info.delimiter, Some('/'));
assert_eq!(
info.old_name.as_ref().map(MailboxName::as_str),
Some("OldName"),
"OLDNAME should be captured in MailboxInfo"
);
} else {
panic!("expected List, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn audit_finding4_list_extended_childinfo_parsed() {
let input = b"* LIST (\\HasChildren) \"/\" \"INBOX\" (\"CHILDINFO\" (\"SUBSCRIBED\"))\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = &*u {
assert_eq!(info.name.as_str(), "INBOX");
assert_eq!(
info.child_info,
vec!["SUBSCRIBED"],
"CHILDINFO should be captured in MailboxInfo"
);
} else {
panic!("expected List, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn spec_audit_childinfo_empty_accepted() {
let input = b"* LIST (\\HasChildren) \"/\" \"INBOX\" (\"CHILDINFO\" ())\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty(), "parse must consume the full response");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = &*u {
assert!(
info.child_info.is_empty(),
"empty CHILDINFO () should produce empty vec"
);
} else {
panic!("expected List, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn audit_finding4_list_extended_multiple_items_parsed() {
let input = b"* LIST (\\HasChildren) \"/\" \"folder\" (\"OLDNAME\" (\"old\") \"CHILDINFO\" (\"SUBSCRIBED\"))\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = &*u {
assert_eq!(info.name.as_str(), "folder");
assert_eq!(info.old_name.as_ref().map(MailboxName::as_str), Some("old"));
assert_eq!(info.child_info, vec!["SUBSCRIBED"]);
} else {
panic!("expected List, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn audit_finding4_list_without_extended_data_still_works() {
let input = b"* LIST (\\Noselect) \"/\" \"Archive\"\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = &*u {
assert_eq!(info.name.as_str(), "Archive");
assert_eq!(info.delimiter, Some('/'));
} else {
panic!("expected List, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn test_list_extended_unknown_simple_value() {
let input = b"* LIST (\\HasNoChildren) \"/\" \"INBOX\" (\"x-future-ext\" 42)\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty(), "should consume entire input");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = &*u {
assert_eq!(info.name.as_str(), "INBOX");
assert_eq!(info.delimiter, Some('/'));
} else {
panic!("expected List, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn test_list_extended_unknown_sequence_set_value() {
let input = b"* LIST (\\HasNoChildren) \"/\" \"INBOX\" (\"x-seq-ext\" 1:5,8:*)\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty(), "should consume entire input");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = &*u {
assert_eq!(info.name.as_str(), "INBOX");
} else {
panic!("expected List, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn test_list_extended_unknown_simple_then_known() {
let input =
b"* LIST (\\HasChildren) \"/\" \"folder\" (\"x-count\" 99 \"OLDNAME\" (\"old\"))\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty(), "should consume entire input");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = &*u {
assert_eq!(info.name.as_str(), "folder");
assert_eq!(
info.old_name.as_ref().map(MailboxName::as_str),
Some("old"),
"OLDNAME after unknown simple item should still be parsed"
);
} else {
panic!("expected List, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn audit_finding7_metadata_longentries_response_code() {
let input = b"A001 OK [METADATA LONGENTRIES 2048] completed\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Tagged(tagged) = resp {
assert_eq!(tagged.status, StatusKind::Ok);
assert_eq!(
tagged.code,
Some(ResponseCode::MetadataLongEntries(2048)),
"METADATA LONGENTRIES should be parsed as a distinct variant"
);
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn audit_finding7_metadata_maxsize_response_code() {
let input = b"A001 NO [METADATA MAXSIZE 1024] value too large\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Tagged(tagged) = resp {
assert_eq!(tagged.code, Some(ResponseCode::MetadataMaxSize(1024)));
} else {
panic!("expected Tagged");
}
}
#[test]
fn audit_finding7_metadata_toomany_response_code() {
let input = b"A001 NO [METADATA TOOMANY] too many annotations\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Tagged(tagged) = resp {
assert_eq!(tagged.code, Some(ResponseCode::MetadataTooMany));
} else {
panic!("expected Tagged");
}
}
#[test]
fn audit_finding7_metadata_noprivate_response_code() {
let input = b"A001 NO [METADATA NOPRIVATE] private annotations not supported\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Tagged(tagged) = resp {
assert_eq!(tagged.code, Some(ResponseCode::MetadataNoPrivate));
} else {
panic!("expected Tagged");
}
}
#[test]
fn metadata_unknown_subcode_with_value() {
let input = b"A001 NO [METADATA XFOO 42] some text\r\n";
let (rest, resp) = parse_response(input).expect(
"METADATA unknown sub-code with value must parse \
(forward compatibility per RFC 5464)",
);
assert!(rest.is_empty());
if let Response::Tagged(tagged) = resp {
assert!(
tagged.code.is_some(),
"response code must be preserved, got None"
);
} else {
panic!("expected Tagged");
}
}
#[test]
fn metadata_sub_atom_non_ascii_uses_lossy_conversion() {
let input = b"A001 NO [METADATA TOOMAN\x80Y] too many annotations\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Tagged(tagged) = resp {
assert_eq!(tagged.status, StatusKind::No);
assert!(
tagged.code.is_some(),
"response code should be Some (not None); \
unwrap_or(\"\") causes the response code parser to fail entirely"
);
match tagged.code {
Some(ResponseCode::Other { ref name, .. }) => {
assert!(
name.starts_with("METADATA TOOMAN"),
"sub-atom should be preserved via lossy conversion, got: {name:?}"
);
}
other => {
panic!("expected ResponseCode::Other with METADATA prefix, got {other:?}");
}
}
} else {
panic!("expected Tagged, got {resp:?}");
}
}
#[test]
fn audit_h1_metadata_entry_names_accept_astring() {
let input = b"* METADATA \"INBOX\" (/shared/comment \"A shared comment\")\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Metadata { mailbox, entries } = &*u {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "/shared/comment");
} else {
panic!("expected Metadata, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn audit_h2_metadata_unsolicited_entry_list_no_parens() {
let input = b"* METADATA \"INBOX\" /shared/comment /private/comment\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Metadata { mailbox, entries } = &*u {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].name, "/shared/comment");
assert_eq!(entries[1].name, "/private/comment");
assert!(entries[0].value.is_none());
assert!(entries[1].value.is_none());
} else {
panic!("expected Metadata, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn audit_m1_esearch_tag_string_accepts_astring() {
let input = b"* ESEARCH (TAG A001) UID COUNT 5\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(e) = &*u {
assert_eq!(e.tag.as_deref(), Some("A001"));
assert!(e.uid);
assert_eq!(e.count, Some(5));
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn audit_m2_threadid_mixed_case_nil() {
let input = b"(THREADID Nil UID 42)";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert!(fr.thread_id.is_none(), "THREADID Nil should parse as None");
assert_eq!(fr.uid, Some(42));
}
#[test]
fn threadid_nil_requires_token_boundary() {
let input = b"(UID 5 THREADID NIL2 42)";
assert!(
fetch_response_inner(input, false).is_err(),
"THREADID NIL2 must not be parsed as THREADID NIL: \
'NIL2' is not the NIL token (RFC 8474 Section 4, RFC 3501 Section 9)"
);
}
#[test]
fn threadid_nil_followed_by_sp_attr() {
let input = b"(THREADID NIL UID 42)";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert!(fr.thread_id.is_none(), "THREADID NIL should parse as None");
assert_eq!(fr.uid, Some(42));
}
#[test]
fn threadid_nil_at_end_of_fetch() {
let input = b"(UID 5 THREADID NIL)";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.uid, Some(5));
assert!(fr.thread_id.is_none());
}
#[test]
fn audit_m3_skip_paren_group_handles_literal() {
let input = b"({5}\r\nhel)o) rest";
let (rest, _) = skip_paren_group(input).unwrap();
assert_eq!(rest, b" rest");
}
#[test]
fn audit_m4_thread_rejects_zero_uid() {
let input = b"* THREAD (5 0 3)\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => {
assert!(
matches!(*boxed, UntaggedResponse::Unknown(_)),
"THREAD with UID 0 should fall through to Unknown, got {boxed:?}"
);
}
other => panic!("Expected Untagged(Unknown), got {other:?}"),
}
}
#[test]
fn audit_m5_binary_section_rejects_zero_part() {
assert!(binary_section_spec(b"[0]").is_err());
}
#[test]
fn audit_m5_binary_section_rejects_zero_in_dotted_path() {
assert!(binary_section_spec(b"[1.0.3]").is_err());
}
#[test]
fn binary_section_spec_accepts_empty() {
let (rest, parts) = binary_section_spec(b"[]").unwrap();
assert!(rest.is_empty());
assert!(parts.is_empty());
}
#[test]
fn binary_section_spec_valid_dotted_path() {
let (rest, parts) = binary_section_spec(b"[1.2.3]").unwrap();
assert!(rest.is_empty());
assert_eq!(parts, vec![1, 2, 3]);
}
#[test]
fn audit_m11_metadata_server_capability() {
let (_, cap) = capability(b"METADATA-SERVER").unwrap();
assert!(matches!(cap, Capability::MetadataServer));
}
#[test]
fn audit_l5_nz_number_rejects_leading_zeros() {
assert!(nz_number(b"007 ").is_err());
}
#[test]
fn audit_l6_fetch_modseq_accepts_zero() {
let input = b"(UID 1 MODSEQ (0))";
let result = fetch_response_inner(input, false);
let (_, fr) = result.expect("FETCH with MODSEQ (0) must parse — 0 accepted per Postel's law");
assert_eq!(
fr.mod_seq,
Some(0),
"MODSEQ 0 must be accepted per Postel's law (should be Some(0), not None)"
);
assert_eq!(
fr.uid,
Some(1),
"UID must be preserved alongside MODSEQ (0)"
);
}
#[test]
fn audit_l6_highestmodseq_response_code_accepts_zero() {
let input = b"* OK [HIGHESTMODSEQ 0] done\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(u) => match *u {
UntaggedResponse::Status { code, text, .. } => {
assert_eq!(code, Some(ResponseCode::HighestModSeq(0)));
assert_eq!(text, "done");
}
other => panic!("expected Status, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn audit_l7_namespace_accepts_empty_parens() {
let input = b"* NAMESPACE () NIL NIL\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => {
if let UntaggedResponse::Namespace {
personal,
other,
shared,
} = *boxed
{
assert!(
personal.is_empty(),
"`()` should produce empty personal namespace"
);
assert!(other.is_empty());
assert!(shared.is_empty());
} else {
panic!("expected Namespace, got {boxed:?}");
}
}
other => panic!("Expected Untagged(Namespace), got {other:?}"),
}
}
#[test]
fn audit_l11_starttls_capability_variant() {
let (_, cap) = capability(b"STARTTLS").unwrap();
assert!(matches!(cap, Capability::StartTls));
}
#[test]
fn audit_l11_logindisabled_capability_variant() {
let (_, cap) = capability(b"LOGINDISABLED").unwrap();
assert!(matches!(cap, Capability::LoginDisabled));
}
#[test]
fn audit_l13_emailid_nil_rejected() {
let input = b"(EMAILID NIL UID 42)";
let result = fetch_response_inner(input, false);
assert!(
result.is_err(),
"EMAILID NIL must be rejected per RFC 8474 Section 7: \
only THREADID allows NIL, got: {result:?}"
);
}
#[test]
fn audit_l14_body_language_rejects_empty_parens() {
let result = body_language(b"()");
if let Ok((_, val)) = result {
assert!(
val.as_ref().map_or(true, std::vec::Vec::is_empty),
"empty () should not produce non-empty language list"
);
}
}
#[test]
fn audit_l17_sort_display_capability() {
let (_, cap) = capability(b"SORT=DISPLAY").unwrap();
assert!(
!matches!(cap, Capability::Other(_)),
"SORT=DISPLAY should not be Other: {cap:?}"
);
}
#[test]
fn audit_sort_unknown_variant_preserved_as_other() {
let (_, cap) = capability(b"SORT=XYZZY").unwrap();
assert!(
matches!(cap, Capability::Other(ref raw) if raw == "SORT=XYZZY"),
"unknown SORT=* capability must be preserved as Other, got {cap:?}"
);
}
#[test]
fn spec_audit_sequence_set_rejects_empty() {
assert!(
sequence_set(b"").is_err(),
"empty sequence-set must be rejected per RFC 9051 Section 9"
);
}
#[test]
fn spec_audit_nz_number64_rejects_leading_zeros() {
assert!(
nz_number64(b"01").is_err(),
"nz-number64 must reject leading zeros per RFC 9051 Section 9"
);
assert!(
nz_number64(b"007").is_err(),
"nz-number64 must reject leading zeros per RFC 9051 Section 9"
);
assert!(nz_number64(b"0").is_err(), "nz-number64 must reject zero");
assert_eq!(nz_number64(b"1").unwrap().1, 1);
assert_eq!(nz_number64(b"42").unwrap().1, 42);
}
#[test]
fn spec_audit_unknown_untagged_response() {
let input = b"* XFOOBAR some extension data\r\n";
let (rem, resp) = parse_response(input).unwrap();
assert!(rem.is_empty());
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Unknown(text) => {
assert_eq!(text, "XFOOBAR some extension data");
}
other => panic!("Expected Unknown, got {other:?}"),
},
other => panic!("Expected Untagged, got {other:?}"),
}
}
#[test]
fn message_global_subtype() {
let input = b"(\"MESSAGE\" \"GLOBAL\" (\"charset\" \"utf-8\") NIL NIL \"7BIT\" 500 \
(NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL) \
(\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 100 5) 20)";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Message {
media_subtype,
size,
lines,
..
} = bs
{
assert_eq!(
media_subtype, "global",
"subtype must be lowercase per RFC 2045 Section 5.1"
);
assert_eq!(size, 500);
assert_eq!(lines, 20);
} else {
panic!("expected Message variant, got {bs:?}");
}
}
#[test]
fn spec_audit_unknown_untagged_with_parens() {
let input = b"* XANNOTATION \"inbox\" (value.shared \"test\")\r\n";
let (rem, resp) = parse_response(input).unwrap();
assert!(rem.is_empty());
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Unknown(text) => {
assert!(text.starts_with("XANNOTATION"));
}
other => panic!("Expected Unknown, got {other:?}"),
},
other => panic!("Expected Untagged, got {other:?}"),
}
}
#[test]
fn tagged_ok_no_text() {
let input = b"A001 OK\r\n";
let (rem, resp) = parse_response(input).unwrap();
assert!(rem.is_empty());
if let Response::Tagged(t) = resp {
assert_eq!(t.tag, "A001");
assert_eq!(t.status, StatusKind::Ok);
assert!(t.code.is_none());
assert!(t.text.is_empty());
} else {
panic!("expected Tagged response, got {resp:?}");
}
}
#[test]
fn appendlimit_large_value_parsed_as_u64() {
let input = b"* CAPABILITY IMAP4rev1 APPENDLIMIT=99999999999\r\n";
let (rem, resp) = parse_response(input).unwrap();
assert!(rem.is_empty());
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Capability(caps) = &*boxed {
assert!(
caps.contains(&Capability::AppendLimit(Some(99_999_999_999))),
"large value must be parsed as AppendLimit(Some(99999999999)), got: {caps:?}"
);
return;
}
}
panic!("expected Capabilities untagged response");
}
#[test]
fn regression_appendlimit_large_value_parsed_as_u64() {
let input = b"* CAPABILITY IMAP4rev1 APPENDLIMIT=5000000000\r\n";
let (rem, resp) = parse_response(input).unwrap();
assert!(rem.is_empty());
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Capability(caps) = &*boxed {
assert!(
caps.contains(&Capability::AppendLimit(Some(5_000_000_000))),
"APPENDLIMIT=5000000000 must be parsed as AppendLimit(Some(5000000000)), \
got: {caps:?}"
);
return;
}
}
panic!("expected Capabilities untagged response");
}
#[test]
fn spec_audit_search_modseq_case_insensitive() {
let input = b"* SEARCH 2 5 (modseq 917162500)\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(u) => match *u {
UntaggedResponse::Search { uids, mod_seq } => {
assert_eq!(uids, vec![2, 5]);
assert_eq!(mod_seq, Some(917162500));
}
other => panic!("expected Search, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn spec_audit_sort_modseq_case_insensitive() {
let input = b"* SORT 2 3 6 (Modseq 917162500)\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(u) => match *u {
UntaggedResponse::Sort { nums, mod_seq } => {
assert_eq!(nums, vec![2, 3, 6]);
assert_eq!(mod_seq, Some(917162500));
}
other => panic!("expected Sort, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn test_namespace_tolerates_multichar_delimiter() {
let input = b"* NAMESPACE ((\"\" \"ab\")) NIL NIL\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => {
if let UntaggedResponse::Namespace { personal, .. } = *boxed {
assert_eq!(personal.len(), 1);
assert_eq!(
personal[0].delimiter,
Some('a'),
"multi-char namespace delimiter should take the first character"
);
} else {
panic!("expected Namespace response, got {boxed:?}");
}
}
other => panic!("expected Untagged(Namespace), got {other:?}"),
}
}
#[test]
fn spec_audit_namespace_empty_parens() {
let input = b"* NAMESPACE ((\"\" \"/\")) () NIL\r\n";
let result = parse_response(input);
assert!(
result.is_ok(),
"namespace `()` must be accepted per Postel's law; got parse error: {result:?}"
);
let (_, resp) = result.unwrap();
match resp {
Response::Untagged(inner) => {
if let UntaggedResponse::Namespace {
personal,
other,
shared,
} = *inner
{
assert_eq!(personal.len(), 1, "should have one personal namespace");
assert_eq!(personal[0].prefix, "");
assert_eq!(personal[0].delimiter, Some('/'));
assert!(
other.is_empty(),
"`()` should produce empty other namespace"
);
assert!(
shared.is_empty(),
"NIL should produce empty shared namespace"
);
} else {
panic!("expected Namespace, got {inner:?}");
}
}
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn spec_audit_body_params_empty_parenthesized() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" () NIL NIL \"7BIT\" 42 3))\r\n";
let result = parse_response(input);
assert!(
result.is_ok(),
"body-fld-param `()` must be accepted per Postel's law; got parse error"
);
let (_, resp) = result.unwrap();
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Fetch(fr) => {
let bs = fr
.body_structure
.as_ref()
.expect("should have body_structure");
match bs {
BodyStructure::Text { params, .. } => {
assert!(
params.is_empty(),
"empty () should produce empty params vec"
);
}
other => panic!("expected Text variant, got {other:?}"),
}
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn spec_audit_body_disposition_empty_params() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 100 5 NIL (\"inline\" ()) NIL NIL))\r\n";
let result = parse_response(input);
assert!(
result.is_ok(),
"disposition with empty params `()` must be accepted; got parse error"
);
let (_, resp) = result.unwrap();
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Fetch(fr) => {
let bs = fr
.body_structure
.as_ref()
.expect("should have body_structure");
match bs {
BodyStructure::Text { disposition, .. } => {
let disp = disposition.as_ref().expect("should have disposition");
assert_eq!(disp.disposition_type, "inline");
assert!(
disp.params.is_empty(),
"empty () params should produce empty vec"
);
}
other => panic!("expected Text variant, got {other:?}"),
}
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn spec_audit_multipart_empty_ext_params() {
let input = b"* 1 FETCH (BODYSTRUCTURE ((\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 10 1)(\"TEXT\" \"HTML\" NIL NIL NIL \"7BIT\" 20 2) \"ALTERNATIVE\" () NIL NIL NIL))\r\n";
let result = parse_response(input);
assert!(
result.is_ok(),
"multipart body-fld-param `()` must be accepted; got parse error"
);
let (_, resp) = result.unwrap();
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Fetch(fr) => {
let bs = fr
.body_structure
.as_ref()
.expect("should have body_structure");
match bs {
BodyStructure::Multipart { params, .. } => {
assert!(
params.is_empty(),
"empty () should produce empty params vec"
);
}
other => panic!("expected Multipart variant, got {other:?}"),
}
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn fetch_response_preserves_seq_and_flags_for_idle() {
let input = b"* 5 FETCH (FLAGS (\\Seen \\Answered) UID 300)\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty(), "parser should consume all input");
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Fetch(fr) => {
assert_eq!(fr.seq, 5, "sequence number must be preserved");
assert_eq!(fr.uid, Some(300), "UID must be preserved");
let flags = fr.flags.as_ref().expect("FLAGS must be present");
assert!(
flags.contains(&Flag::Seen),
"expected \\Seen in flags, got {flags:?}"
);
assert!(
flags.contains(&Flag::Answered),
"expected \\Answered in flags, got {flags:?}"
);
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn spec_audit_bodystructure_size_number64() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 5000000000 100))\r\n";
let (_, resp) = parse_response(input).expect("should parse");
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Fetch(fr) => {
let bs = fr.body_structure.expect(
"BODYSTRUCTURE must be present — size > u32::MAX must not \
cause a parse failure per RFC 9051 Section 7.5.2",
);
match bs {
BodyStructure::Text { size, lines, .. } => {
assert_eq!(
size, 5_000_000_000u64,
"body-fld-octcnt must support number64 per \
RFC 9051 Section 7.5.2"
);
assert_eq!(lines, 100);
}
other => panic!("expected Text variant, got {other:?}"),
}
}
UntaggedResponse::Unknown(_) => {
panic!(
"BODYSTRUCTURE with size > u32::MAX was silently discarded \
as Unknown — body-fld-octcnt must be number64 per \
RFC 9051 Section 7.5.2"
);
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn spec_audit_bodystructure_basic_size_number64() {
let input =
b"* 1 FETCH (BODYSTRUCTURE (\"IMAGE\" \"PNG\" NIL NIL NIL \"BASE64\" 6000000000))\r\n";
let (_, resp) = parse_response(input).expect("should parse");
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Fetch(fr) => {
let bs = fr.body_structure.expect("BODYSTRUCTURE must be present");
match bs {
BodyStructure::Basic { size, .. } => {
assert_eq!(
size, 6_000_000_000u64,
"body-fld-octcnt must support number64 per \
RFC 9051 Section 7.5.2"
);
}
other => panic!("expected Basic variant, got {other:?}"),
}
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn spec_audit_encoding_case_normalized() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" \
(\"CHARSET\" \"UTF-8\") NIL NIL \"Quoted-Printable\" 100 5))\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Fetch(fr) => {
let bs = fr.body_structure.expect("missing BODYSTRUCTURE");
match bs {
BodyStructure::Text { encoding, .. } => {
assert_eq!(
encoding, "quoted-printable",
"RFC 2045 Section 6: encoding must be lowercased"
);
}
other => panic!("expected Text variant, got {other:?}"),
}
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn spec_audit_disposition_type_case_normalized() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" NIL NIL NIL \
\"7BIT\" 100 5 NIL (\"Attachment\" (\"FILENAME\" \"test.txt\")) NIL NIL))\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Fetch(fr) => {
let bs = fr.body_structure.expect("missing BODYSTRUCTURE");
match bs {
BodyStructure::Text { disposition, .. } => {
let disp = disposition.expect("missing disposition");
assert_eq!(
disp.disposition_type, "attachment",
"RFC 2183 Section 2: disposition type must be lowercased"
);
}
other => panic!("expected Text variant, got {other:?}"),
}
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn disposition_type_lowercased_for_conventional_comparison() {
let (_, disp) = body_disposition(b"(\"ATTACHMENT\" NIL)").unwrap();
let disp = disp.expect("should not be None");
assert_eq!(
disp.disposition_type, "attachment",
"RFC 2183 Section 2: disposition type must be lowercased to \
conventional form for consistent comparison"
);
let (_, disp) = body_disposition(b"(\"Inline\" NIL)").unwrap();
let disp = disp.expect("should not be None");
assert_eq!(
disp.disposition_type, "inline",
"RFC 2183 Section 2: disposition type must be lowercased to \
conventional form for consistent comparison"
);
}
#[test]
fn spec_audit_param_names_lowercased() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" \
(\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5))\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Fetch(fr) => {
let bs = fr.body_structure.expect("missing BODYSTRUCTURE");
match bs {
BodyStructure::Text { params, .. } => {
assert_eq!(params.len(), 1);
assert_eq!(
params[0].0, "charset",
"RFC 2045 Section 5.1: parameter names must be lowercased"
);
}
other => panic!("expected Text variant, got {other:?}"),
}
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn fetch_flags_must_exclude_wildcard() {
let input = b"(FLAGS (\\Seen \\*))";
let (_, fr) = fetch_response_inner(input, false).unwrap();
let flags = fr.flags.expect("flags must be present");
assert!(
!flags.contains(&Flag::Wildcard),
"FETCH FLAGS must not accept \\* — \\* is only valid in \
flag-perm (PERMANENTFLAGS), not flag-fetch (RFC 3501 Section 9). \
Got: {flags:?}"
);
}
#[test]
fn untagged_flags_must_exclude_wildcard() {
let input = b"* FLAGS (\\Seen \\Answered \\*)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Flags(flags) = *boxed {
assert!(
!flags.contains(&Flag::Wildcard),
"* FLAGS must not accept \\* — \\* is only valid in \
flag-perm (PERMANENTFLAGS), not flag (RFC 3501 Section 9). \
Got: {flags:?}"
);
} else {
panic!("expected Flags, got {boxed:?}");
}
} else {
panic!("expected Untagged, got {resp:?}");
}
}
#[test]
fn permanentflags_must_accept_wildcard() {
let input = b"[PERMANENTFLAGS (\\Seen \\Flagged \\*)]";
let (_, code) = response_code(input).unwrap();
if let ResponseCode::PermanentFlags(flags) = &code {
assert!(
flags.contains(&Flag::Wildcard),
"PERMANENTFLAGS must accept \\* per RFC 3501 Section 7.1 \
(flag-perm = flag / \"\\*\"). Got: {flags:?}"
);
} else {
panic!("expected PermanentFlags, got {code:?}");
}
}
#[test]
fn body_section_origin_u64() {
let input = b"* 1 FETCH (BODY[]<5000000000> {5}\r\nhello)\r\n";
let (_, resp) = parse_response(input).expect("should parse u64 origin");
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Fetch(fr) => {
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].origin, Some(5_000_000_000u64));
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn binary_section_origin_u64() {
let input = b"* 1 FETCH (BINARY[1]<5000000000> {5}\r\nhello)\r\n";
let (_, resp) = parse_response(input).expect("should parse u64 BINARY origin");
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Fetch(fr) => {
assert_eq!(fr.binary_sections.len(), 1);
assert_eq!(fr.binary_sections[0].origin, Some(5_000_000_000u64));
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn unknown_with_literal() {
let input = b"* XFOO {5}\r\nhello\r\n";
let (rem, resp) = parse_response(input).expect("should parse unknown with literal");
assert!(
rem.is_empty(),
"remaining bytes should be empty, got {:?}",
String::from_utf8_lossy(rem)
);
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Unknown(text) => {
assert!(
text.starts_with("XFOO"),
"Unknown text should start with XFOO, got: {text}"
);
}
other => panic!("Expected Unknown, got {other:?}"),
},
other => panic!("Expected Untagged, got {other:?}"),
}
}
#[test]
fn unknown_with_quoted_string() {
let input = b"* XBAR \"quoted\"\r\n";
let (rem, resp) = parse_response(input).expect("should parse unknown with quoted string");
assert!(rem.is_empty());
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Unknown(text) => {
assert!(text.starts_with("XBAR"));
}
other => panic!("Expected Unknown, got {other:?}"),
},
other => panic!("Expected Untagged, got {other:?}"),
}
}
#[test]
fn unknown_with_literal8() {
let input = b"* XBAZ ~{3}\r\nabc\r\n";
let (rem, resp) = parse_response(input).expect("should parse unknown with literal8");
assert!(
rem.is_empty(),
"remaining bytes should be empty, got {:?}",
String::from_utf8_lossy(rem)
);
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Unknown(text) => {
assert!(text.starts_with("XBAZ"));
}
other => panic!("Expected Unknown, got {other:?}"),
},
other => panic!("Expected Untagged, got {other:?}"),
}
}
#[test]
fn unknown_with_truncated_quoted_backslash_no_panic() {
let input = b"* XFOO \"trail\\\r\n";
let result = parse_response(input);
match result {
Ok((rem, Response::Untagged(boxed))) => {
assert!(
rem.is_empty(),
"remaining bytes should be empty, got {:?}",
String::from_utf8_lossy(rem)
);
match *boxed {
UntaggedResponse::Unknown(ref text) => {
assert!(
text.starts_with("XFOO"),
"Unknown text should start with XFOO, got: {text}"
);
}
other => panic!("Expected Unknown, got {other:?}"),
}
}
Ok((_, other)) => panic!("Expected Untagged(Unknown), got {other:?}"),
Err(e) => panic!(
"BUG: parse_response failed on truncated quoted escape: {e:?} — \
the backslash swallowed the CRLF terminator"
),
}
let direct_input = b"XFOO \"trail\\";
let result2 = scan_unknown_response(direct_input);
assert!(
result2.is_err(),
"scan_unknown_response on truncated input with trailing backslash \
should return Err (no CRLF), got {result2:?}"
);
}
#[test]
fn unknown_with_literal_plus() {
let input = b"* XQUX {3+}\r\nabc\r\n";
let (rem, resp) = parse_response(input).expect("should parse unknown with LITERAL+");
assert!(
rem.is_empty(),
"remaining bytes should be empty, got {:?}",
String::from_utf8_lossy(rem)
);
match resp {
Response::Untagged(boxed) => match *boxed {
UntaggedResponse::Unknown(text) => {
assert!(text.starts_with("XQUX"));
}
other => panic!("Expected Unknown, got {other:?}"),
},
other => panic!("Expected Untagged, got {other:?}"),
}
}
#[test]
fn body_section_header_fields_literal() {
let input = b"* 1 FETCH (BODY[HEADER.FIELDS (Subject)] {15}\r\nSubject: Hi\r\n\r\n)\r\n";
let (_, resp) = parse_response(input).expect("should parse HEADER.FIELDS with literal");
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Fetch(fr) => {
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].section, "HEADER.FIELDS (Subject)");
assert_eq!(
fr.body_sections[0].data.as_deref(),
Some(b"Subject: Hi\r\n\r\n".as_ref())
);
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn body_section_quoted_bracket_in_header_field_name() {
let input = b"(BODY[HEADER.FIELDS (\"X]Field\")] \"data\")";
let (_, fr) = fetch_response_inner(input, false)
.expect("should parse HEADER.FIELDS with quoted ] in field name");
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].section, "HEADER.FIELDS (\"X]Field\")");
assert_eq!(fr.body_sections[0].data.as_deref(), Some(b"data".as_ref()));
}
#[test]
fn body_section_mime() {
let input = b"(BODY[1.MIME] \"Content-Type: text/plain\")";
let (_, fr) = fetch_response_inner(input, false).unwrap();
assert_eq!(fr.body_sections.len(), 1);
assert_eq!(fr.body_sections[0].section, "1.MIME");
assert_eq!(
fr.body_sections[0].data.as_deref(),
Some(b"Content-Type: text/plain".as_ref())
);
}
#[test]
fn spec_audit_flags_response_retains_recent() {
let input = b"* FLAGS (\\Seen \\Answered \\Recent \\Flagged)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Flags(flags) = *boxed {
assert!(
flags.contains(&Flag::Recent),
"\\Recent must be retained in FLAGS for Postel's law compatibility: {flags:?}"
);
assert!(flags.contains(&Flag::Seen));
assert!(flags.contains(&Flag::Answered));
assert!(flags.contains(&Flag::Flagged));
return;
}
}
panic!("expected FLAGS response");
}
#[test]
fn spec_audit_fetch_flags_includes_recent() {
let input = b"* 1 FETCH (FLAGS (\\Seen \\Recent))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fetch) = *boxed {
let flags = fetch.flags.as_ref().expect("FLAGS should be present");
assert!(
flags.contains(&Flag::Recent),
"RFC 3501 Section 9: flag-fetch includes \\Recent, \
but parser excluded it: {flags:?}"
);
assert!(flags.contains(&Flag::Seen));
return;
}
}
panic!("expected FETCH response");
}
#[test]
fn try_skip_literal_overflow_returns_none() {
let count_str = format!("{{{}}}\r\n", usize::MAX);
let input = count_str.as_bytes();
assert_eq!(
try_skip_literal(input),
None,
"try_skip_literal must return None when literal count overflows usize"
);
}
#[test]
fn skip_balanced_parens_unbalanced_depth_returns_error() {
let input = b"(\"nested\" (\"inner\")";
let result = skip_balanced_parens(input);
assert!(
result.is_err(),
"skip_balanced_parens must return Err when input is exhausted with \
depth > 0 (unbalanced parens), got Ok with rest = {:?}",
result
.ok()
.map(|(r, ())| String::from_utf8_lossy(r).to_string())
);
}
#[test]
fn skip_balanced_parens_empty_at_zero_depth_returns_ok() {
let input = b"";
let result = skip_balanced_parens(input);
assert!(
result.is_ok(),
"skip_balanced_parens must return Ok when input is empty at depth 0"
);
}
#[test]
fn skip_balanced_parens_literal_overflow_no_wrap() {
let literal = format!("{{{}}}\r\n", usize::MAX);
let mut input = literal.into_bytes();
input.push(b')'); let result = skip_balanced_parens(&input);
assert!(
result.is_ok(),
"skip_balanced_parens must handle overflow gracefully"
);
let (rest, ()) = result.unwrap();
assert!(
rest.ends_with(b")"),
"skip_balanced_parens must not consume past overflow point; rest = {:?}",
String::from_utf8_lossy(rest)
);
}
#[test]
fn skip_paren_group_literal_overflow_no_wrap() {
let literal = format!("({{{}}}\r\n)", usize::MAX);
let input = literal.as_bytes();
let result = skip_paren_group(input);
assert!(
result.is_err(),
"skip_paren_group must return Err on literal overflow \
(RFC 3501 Section 9 / RFC 9051 Section 9)"
);
}
#[test]
fn scan_section_spec_literal_overflow_does_not_panic() {
let input = b"* 1 FETCH (BODY[HEADER.FIELDS ({999}\r\nXX)] \"test\")\r\n";
let result = parse_response(input);
assert!(
result.is_ok() || result.is_err(),
"scan_section_spec must not panic on oversized literal count"
);
let input2 = format!(
"* 1 FETCH (BODY[HEADER.FIELDS ({{{}}}\r\nAB)] \"x\")\r\n",
usize::MAX
);
let result2 = parse_response(input2.as_bytes());
assert!(
result2.is_ok() || result2.is_err(),
"scan_section_spec must not panic on usize::MAX literal count"
);
}
#[test]
fn scan_section_spec_backslash_crlf_in_quoted_string() {
let input = b"HEADER.FIELDS (\"val\\\r\nmore\")]rest";
let result = scan_section_spec(input);
if let Ok((rest, section)) = result {
let section_str = String::from_utf8_lossy(section);
assert!(
!section_str.contains("more"),
"scan_section_spec consumed past CRLF: section = {section_str:?}",
);
assert!(
rest == b"rest" || rest.starts_with(b"]") || rest.contains(&b'\n'),
"unexpected rest after scan_section_spec: {:?}",
String::from_utf8_lossy(rest)
);
}
}
#[test]
fn regression_search_modseq_zero_preserves_uids() {
let input = b"* SEARCH 1 5 10 (MODSEQ 0)\r\n";
let (_, resp) = parse_response(input)
.expect("SEARCH with malformed MODSEQ must still parse — UIDs must not be silently lost");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Search { uids, mod_seq } = &*u {
assert_eq!(
*uids,
vec![1, 5, 10],
"SEARCH UIDs must be preserved even when MODSEQ suffix is malformed"
);
assert_eq!(
*mod_seq, None,
"malformed MODSEQ should be discarded (None), not Some(0)"
);
} else {
panic!(
"expected Search response, got {u:?} — malformed MODSEQ must not \
cause fallthrough to Unknown"
);
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn regression_fetch_modseq_zero_preserves_other_attrs() {
let input = b"* 1 FETCH (UID 42 FLAGS (\\Seen) MODSEQ (0) RFC822.SIZE 1024)\r\n";
let (_, resp) = parse_response(input)
.expect("FETCH with MODSEQ (0) must parse — 0 accepted per Postel's law");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Fetch(fr) = &*u {
assert_eq!(fr.seq, 1, "sequence number must be preserved");
assert_eq!(fr.uid, Some(42), "UID must be preserved");
assert!(
fr.flags
.as_ref()
.is_some_and(|f| f.contains(&crate::types::Flag::Seen)),
"FLAGS must be preserved"
);
assert_eq!(fr.rfc822_size, Some(1024), "RFC822.SIZE must be preserved");
assert_eq!(
fr.mod_seq,
Some(0),
"MODSEQ (0) should be preserved as Some(0) per Postel's law"
);
} else {
panic!("expected Fetch, got {u:?} — MODSEQ (0) must not drop the response");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn regression_sort_modseq_zero_preserves_nums() {
let input = b"* SORT 3 1 2 (MODSEQ 0)\r\n";
let (_, resp) = parse_response(input)
.expect("SORT with malformed MODSEQ must still parse — numbers must not be silently lost");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Sort { nums, mod_seq } = &*u {
assert_eq!(
*nums,
vec![3, 1, 2],
"SORT numbers must be preserved even when MODSEQ suffix is malformed"
);
assert_eq!(
*mod_seq, None,
"malformed MODSEQ should be discarded (None), not Some(0)"
);
} else {
panic!(
"expected Sort response, got {u:?} — malformed MODSEQ must not \
cause fallthrough to Unknown"
);
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn regression_search_modseq_overflow_preserves_uids() {
let input = b"* SEARCH 42 (MODSEQ 9223372036854775808)\r\n";
let (_, resp) = parse_response(input).expect("SEARCH with overflowing MODSEQ must still parse");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Search { uids, mod_seq } = &*u {
assert_eq!(*uids, vec![42]);
assert_eq!(*mod_seq, None);
} else {
panic!("expected Search, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn regression_esearch_modseq_zero_preserves_results() {
let input = b"* ESEARCH (TAG \"A1\") UID COUNT 5 ALL 1:3,7,9 MODSEQ 0\r\n";
let (_, resp) = parse_response(input).expect("ESEARCH with malformed MODSEQ must still parse");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(esearch.count, Some(5), "COUNT must be preserved");
assert_eq!(
esearch.all,
vec![
UidRange::range(1, 3),
UidRange::single(7),
UidRange::single(9)
],
"ALL must be preserved"
);
assert!(esearch.uid, "UID indicator must be preserved");
assert_eq!(esearch.tag.as_deref(), Some("A1"), "TAG must be preserved");
} else {
panic!(
"expected Esearch, got {u:?} — malformed MODSEQ must not cause \
fallthrough to Unknown"
);
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn regression_esearch_modseq_overflow_preserves_results() {
let input = b"* ESEARCH (TAG \"A2\") UID MIN 1 MAX 100 MODSEQ 9223372036854775808\r\n";
let (_, resp) =
parse_response(input).expect("ESEARCH with overflowing MODSEQ must still parse");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(esearch.min, Some(1), "MIN must be preserved");
assert_eq!(esearch.max, Some(100), "MAX must be preserved");
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn regression_esearch_modseq_missing_space_preserves_results() {
let input = b"* ESEARCH (TAG \"A1\") UID COUNT 5 MODSEQ\r\n";
let (_, resp) = parse_response(input).expect("ESEARCH with space-less MODSEQ must still parse");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(esearch.count, Some(5), "COUNT must be preserved");
assert!(esearch.uid, "UID indicator must be preserved");
assert_eq!(esearch.tag.as_deref(), Some("A1"), "TAG must be preserved");
assert_eq!(
esearch.mod_seq, None,
"MODSEQ should be None when space is missing"
);
} else {
panic!(
"expected Esearch, got {u:?} — malformed MODSEQ must not cause \
fallthrough to Unknown"
);
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn regression_fetch_modseq_overflow_preserves_other_attrs() {
let input = b"* 5 FETCH (UID 100 MODSEQ (9223372036854775808) FLAGS (\\Flagged))\r\n";
let (_, resp) = parse_response(input).expect("FETCH with overflowing MODSEQ must still parse");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Fetch(fr) = &*u {
assert_eq!(fr.uid, Some(100), "UID must be preserved");
assert!(
fr.flags
.as_ref()
.is_some_and(|f| f.contains(&crate::types::Flag::Flagged)),
"FLAGS must be preserved"
);
assert_eq!(fr.mod_seq, None, "overflowing MODSEQ should be None");
} else {
panic!("expected Fetch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn fetch_modseq_zero() {
let input = b"* 1 FETCH (MODSEQ (0))\r\n";
let (_, resp) = parse_response(input).expect("FETCH with MODSEQ (0) must parse");
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Fetch(fr) = &*u {
assert_eq!(
fr.mod_seq,
Some(0),
"MODSEQ (0) should be preserved as Some(0), not silently discarded"
);
} else {
panic!("expected Fetch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn bodystructure_text_trailing_space_before_close_paren() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" (\"charset\" \"utf-8\") NIL NIL \"7BIT\" 100 5 ))\r\n";
let (_, resp) = parse_response(input)
.expect("BODYSTRUCTURE with trailing space before ) should parse (Postel's law)");
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Fetch(ref fr) => {
let bs = fr
.body_structure
.as_ref()
.expect("should have body_structure");
match bs {
BodyStructure::Text {
media_subtype,
lines,
size,
..
} => {
assert_eq!(media_subtype, "plain");
assert_eq!(*size, 100);
assert_eq!(*lines, 5);
}
other => panic!("expected Text, got {other:?}"),
}
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn bodystructure_basic_ext_data_trailing_space() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"IMAGE\" \"PNG\" NIL NIL NIL \"BASE64\" 5000 NIL NIL NIL NIL ))\r\n";
let (_, resp) = parse_response(input)
.expect("BODYSTRUCTURE with trailing space after extension data should parse");
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Fetch(ref fr) => {
let bs = fr
.body_structure
.as_ref()
.expect("should have body_structure");
assert!(matches!(bs, BodyStructure::Basic { .. }));
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn bodystructure_multipart_trailing_space() {
let input = b"* 1 FETCH (BODYSTRUCTURE ((\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 50 3)(\"TEXT\" \"HTML\" NIL NIL NIL \"7BIT\" 100 5) \"ALTERNATIVE\" NIL ))\r\n";
let (_, resp) =
parse_response(input).expect("multipart BODYSTRUCTURE with trailing space should parse");
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Fetch(ref fr) => {
let bs = fr
.body_structure
.as_ref()
.expect("should have body_structure");
match bs {
BodyStructure::Multipart {
media_subtype,
bodies,
..
} => {
assert_eq!(media_subtype, "alternative");
assert_eq!(bodies.len(), 2);
}
other => panic!("expected Multipart, got {other:?}"),
}
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn bodystructure_1part_all_four_ext_fields() {
let input = b"(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5 \"abc123\" (\"inline\" NIL) \"en\" \"http://example.com\")";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Text {
md5,
disposition,
language,
location,
..
} = bs
{
assert_eq!(md5.as_deref(), Some("abc123"));
let disp = disposition.expect("disposition should be present");
assert_eq!(disp.disposition_type, "inline");
let langs = language.expect("language should be present");
assert_eq!(langs, vec!["en"]);
assert_eq!(location.as_deref(), Some("http://example.com"));
} else {
panic!("expected Text body");
}
}
#[test]
fn bodystructure_mpart_all_four_ext_fields() {
let input = b"((\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 50 3)(\"TEXT\" \"HTML\" NIL NIL NIL \"7BIT\" 80 4) \"MIXED\" (\"BOUNDARY\" \"----=_Part\") (\"inline\" NIL) (\"en\" \"fr\") \"http://example.com\")";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Multipart {
media_subtype,
params,
disposition,
language,
location,
..
} = bs
{
assert_eq!(media_subtype, "mixed");
assert_eq!(params.len(), 1);
assert_eq!(params[0].0, "boundary");
let disp = disposition.expect("disposition should be present");
assert_eq!(disp.disposition_type, "inline");
let langs = language.expect("language should be present");
assert_eq!(langs, vec!["en", "fr"]);
assert_eq!(location.as_deref(), Some("http://example.com"));
} else {
panic!("expected Multipart body");
}
}
#[test]
fn expunge_rejects_leading_zero() {
let input = b"* 01 EXPUNGE\r\n";
let result = parse_response(input);
assert!(
result.is_err(),
"EXPUNGE with leading-zero sequence number '01' should be rejected \
(RFC 3501 Section 9: nz-number = digit-nz *DIGIT)"
);
}
#[test]
fn fetch_rejects_leading_zero() {
let input = b"* 01 FETCH (UID 5)\r\n";
let result = parse_response(input);
assert!(
result.is_err(),
"FETCH with leading-zero sequence number '01' should be rejected \
(RFC 3501 Section 9: nz-number = digit-nz *DIGIT)"
);
}
#[test]
fn exists_accepts_leading_zero() {
let input = b"* 01 EXISTS\r\n";
let (_, resp) = parse_response(input)
.expect("EXISTS with leading-zero number should parse (number allows leading zeros)");
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Exists(n) => assert_eq!(n, 1),
other => panic!("expected Exists, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn recent_accepts_leading_zero() {
let input = b"* 01 RECENT\r\n";
let (_, resp) = parse_response(input).expect("RECENT with leading-zero number should parse");
match resp {
Response::Untagged(inner) => match *inner {
UntaggedResponse::Recent(n) => assert_eq!(n, 1),
other => panic!("expected Recent, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[test]
fn bodystructure_rejects_excessive_nesting_depth() {
let depth = 256_usize;
let mut input = Vec::new();
input.extend_from_slice(b"* 1 FETCH (BODYSTRUCTURE ");
for _ in 0..depth {
input.push(b'(');
}
input.extend_from_slice(
b"(\"text\" \"plain\" (\"charset\" \"utf-8\") NIL NIL \"7bit\" 10 1 NIL NIL NIL NIL)",
);
for _ in 0..depth {
input.extend_from_slice(b" \"MIXED\")");
}
input.extend_from_slice(b")\r\n");
let result = parse_response(&input);
assert!(
result.is_err(),
"parsing a BODYSTRUCTURE nested {depth} levels deep should fail, \
but it succeeded"
);
}
#[test]
fn bodystructure_description_rfc2047_decoded() {
let input =
b"(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL \"=?UTF-8?B?SGVsbG8=?=\" \"7BIT\" 100 5)";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Text { description, .. } = &bs {
assert_eq!(
description.as_deref(),
Some("Hello"),
"RFC 2047 encoded words in Content-Description must be decoded"
);
} else {
panic!("expected Text, got {bs:?}");
}
let input = b"(\"IMAGE\" \"PNG\" NIL NIL \"=?UTF-8?Q?=C3=A9t=C3=A9?=\" \"BASE64\" 2048)";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Basic { description, .. } = &bs {
assert_eq!(
description.as_deref(),
Some("\u{e9}t\u{e9}"),
"RFC 2047 encoded description in Basic part must decode to 'été'"
);
} else {
panic!("expected Basic, got {bs:?}");
}
}
#[test]
fn spec_audit_bodystructure_description_rfc2047_utf8_mode() {
let input =
b"(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL \"=?UTF-8?B?SGVsbG8=?=\" \"7BIT\" 100 5)";
let (_, bs) = body_structure(input, true, 0).unwrap();
if let BodyStructure::Text { description, .. } = &bs {
assert_eq!(
description.as_deref(),
Some("Hello"),
"RFC 2047 encoded Content-Description must be decoded even in UTF8=ACCEPT mode \
(RFC 6855 Section 3.1 only affects ENVELOPE, not BODYSTRUCTURE description \
per RFC 2045 Section 8)"
);
} else {
panic!("expected Text, got {bs:?}");
}
let input = b"(\"IMAGE\" \"PNG\" NIL NIL \"=?UTF-8?Q?=C3=A9t=C3=A9?=\" \"BASE64\" 2048)";
let (_, bs) = body_structure(input, true, 0).unwrap();
if let BodyStructure::Basic { description, .. } = &bs {
assert_eq!(
description.as_deref(),
Some("\u{e9}t\u{e9}"),
"RFC 2047 Q-encoded Content-Description must decode in UTF8=ACCEPT mode"
);
} else {
panic!("expected Basic, got {bs:?}");
}
let input =
b"(\"TEXT\" \"HTML\" (\"CHARSET\" \"UTF-8\") NIL \"\xc3\xa9t\xc3\xa9\" \"7BIT\" 50 2)";
let (_, bs) = body_structure(input, true, 0).unwrap();
if let BodyStructure::Text { description, .. } = &bs {
assert_eq!(
description.as_deref(),
Some("\u{e9}t\u{e9}"),
"Plain UTF-8 Content-Description must be preserved in UTF8=ACCEPT mode"
);
} else {
panic!("expected Text, got {bs:?}");
}
}
#[test]
fn spec_audit_address_list_empty_parens() {
let (rest, addrs) = address_list(b"()", false).unwrap();
assert!(rest.is_empty());
assert!(
addrs.is_empty(),
"empty parens should yield empty Vec<EnvelopeAddress>"
);
}
#[test]
fn spec_audit_envelope_empty_paren_address_field() {
let input = b"* 1 FETCH (ENVELOPE (\"Mon, 1 Jan 2024 00:00:00 +0000\" \"Subject\" ((NIL NIL \"user\" \"example.com\")) ((NIL NIL \"user\" \"example.com\")) ((NIL NIL \"user\" \"example.com\")) () NIL NIL NIL \"<msg@example.com>\"))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Fetch(f) = &*u {
let env = f.envelope.as_ref().unwrap();
assert_eq!(env.from.len(), 1, "from should have one address");
assert!(
env.to.is_empty(),
"to should be empty (parsed from '()'), got {:?}",
env.to
);
assert_eq!(
env.message_id.as_deref(),
Some("<msg@example.com>"),
"message-id should be parsed correctly"
);
} else {
panic!("expected Fetch");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn regression_esearch_unknown_key_quoted_string_value() {
let input = b"* ESEARCH (TAG \"A001\") UID XFUTURE \"hello world\" COUNT 5\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(
esearch.count,
Some(5),
"COUNT must be parsed after skipping unknown quoted-string value"
);
assert!(esearch.uid);
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn regression_esearch_unknown_key_literal_value() {
let input = b"* ESEARCH (TAG \"A001\") UID XBLOB {5}\r\nhello COUNT 3\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Esearch(esearch) = &*u {
assert_eq!(
esearch.count,
Some(3),
"COUNT must be parsed after skipping unknown literal value"
);
assert!(esearch.uid);
} else {
panic!("expected Esearch, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn regression_status_unknown_attr_quoted_string_value() {
let input = b"* STATUS \"INBOX\" (MESSAGES 10 XFOO \"some value\" UNSEEN 3)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::MailboxStatus { mailbox, items } = &*u {
assert_eq!(mailbox.as_str(), "INBOX");
let messages = items.iter().find_map(|i| match i {
StatusItem::Messages(n) => Some(*n),
_ => None,
});
let unseen = items.iter().find_map(|i| match i {
StatusItem::Unseen(n) => Some(*n),
_ => None,
});
assert_eq!(
messages,
Some(10),
"MESSAGES must be parsed before unknown quoted-string attr"
);
assert_eq!(
unseen,
Some(3),
"UNSEEN must be parsed after skipping unknown quoted-string value"
);
} else {
panic!("expected MailboxStatus, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn regression_status_unknown_attr_literal_value() {
let input = b"* STATUS \"INBOX\" (MESSAGES 10 XBLOB {5}\r\na b ) UNSEEN 3)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::MailboxStatus { mailbox, items } = &*u {
assert_eq!(mailbox.as_str(), "INBOX");
let messages = items.iter().find_map(|i| match i {
StatusItem::Messages(n) => Some(*n),
_ => None,
});
let unseen = items.iter().find_map(|i| match i {
StatusItem::Unseen(n) => Some(*n),
_ => None,
});
assert_eq!(
messages,
Some(10),
"MESSAGES must be parsed before unknown literal attr"
);
assert_eq!(
unseen,
Some(3),
"UNSEEN must be parsed after skipping unknown literal value"
);
} else {
panic!("expected MailboxStatus, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn fetch_preview_quoted() {
let (_, resp) =
parse_response(b"* 1 FETCH (UID 42 PREVIEW \"Meeting tomorrow at 3pm\")\r\n").unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(42));
assert_eq!(fr.preview.as_deref(), Some("Meeting tomorrow at 3pm"));
return;
}
}
panic!("expected Fetch response");
}
#[test]
fn fetch_preview_nil() {
let (_, resp) = parse_response(b"* 5 FETCH (UID 100 PREVIEW NIL)\r\n").unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(100));
assert!(fr.preview.is_none());
return;
}
}
panic!("expected Fetch response");
}
#[test]
fn fetch_preview_empty_string() {
let (_, resp) = parse_response(b"* 1 FETCH (PREVIEW \"\")\r\n").unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.preview.as_deref(), Some(""));
return;
}
}
panic!("expected Fetch response");
}
#[test]
fn fetch_preview_with_other_items() {
let (_, resp) =
parse_response(b"* 3 FETCH (UID 7 FLAGS (\\Seen) PREVIEW \"Hello world\")\r\n").unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.uid, Some(7));
assert_eq!(fr.preview.as_deref(), Some("Hello world"));
assert!(fr.flags.is_some());
return;
}
}
panic!("expected Fetch response");
}
#[test]
fn capability_preview_parsed() {
let (_, resp) = parse_response(b"* OK [CAPABILITY IMAP4rev1 PREVIEW] Ready\r\n").unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Status {
code: Some(ResponseCode::Capability(caps)),
..
} = *boxed
{
assert!(
caps.contains(&Capability::Preview),
"PREVIEW capability must be parsed"
);
return;
}
}
panic!("expected OK with CAPABILITY");
}
#[test]
fn capability_within_parsed() {
let (_, resp) = parse_response(b"* OK [CAPABILITY IMAP4rev1 WITHIN] Ready\r\n").unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Status {
code: Some(ResponseCode::Capability(caps)),
..
} = *boxed
{
assert!(
caps.contains(&Capability::Within),
"WITHIN capability must be parsed"
);
return;
}
}
panic!("expected OK with CAPABILITY");
}
#[test]
fn skip_balanced_parens_escaped_chars_in_quoted_string() {
let input = b"(\"a\\\"b\\\\c\") tail";
let (rest, ()) = skip_parenthesized_block(input).unwrap();
assert_eq!(rest, b" tail");
}
#[test]
fn skip_balanced_parens_backslash_crlf_in_quoted_string() {
let input = b"(\"val\\\r\n\")";
let result = skip_parenthesized_block(input);
if let Ok((rest, ())) = result {
assert!(
!rest.is_empty(),
"skip_balanced_parens consumed past CRLF boundary"
);
}
}
#[test]
fn skip_balanced_parens_literal8_prefix() {
let input = b"(~{5}\r\nhello) tail";
let (rest, ()) = skip_parenthesized_block(input).unwrap();
assert_eq!(rest, b" tail");
}
#[test]
fn skip_balanced_parens_literal8_with_parens_in_data() {
let input = b"(~{3}\r\n(x)) tail";
let (rest, ()) = skip_parenthesized_block(input).unwrap();
assert_eq!(rest, b" tail");
}
#[test]
fn skip_balanced_parens_literal_plus() {
let input = b"({5+}\r\nhello) tail";
let (rest, ()) = skip_parenthesized_block(input).unwrap();
assert_eq!(rest, b" tail");
}
#[test]
fn skip_balanced_parens_literal_in_nested_parens() {
let input = b"((\"key\" {3}\r\nval)) rest";
let (rest, ()) = skip_parenthesized_block(input).unwrap();
assert_eq!(rest, b" rest");
}
#[test]
fn skip_balanced_parens_literal_bad_count() {
let input = b"({abc}) rest";
let (rest, ()) = skip_parenthesized_block(input).unwrap();
assert_eq!(rest, b" rest");
}
#[test]
fn number_rejects_alpha_string() {
assert!(number(b"abc").is_err());
}
#[test]
fn number_rejects_u32_overflow() {
assert!(number(b"4294967296").is_err());
}
#[test]
fn number64_rejects_above_i64_max_boundary() {
assert!(number64(b"9223372036854775808").is_err());
}
#[test]
fn number64_rejects_u64_overflow_parse_error() {
assert!(number64(b"18446744073709551616").is_err());
}
#[test]
fn nz_number_rejects_zero() {
assert!(nz_number(b"0").is_err());
}
#[test]
fn nz_number_rejects_leading_zero() {
assert!(nz_number(b"01").is_err());
}
#[test]
fn nz_number_accepts_valid() {
let (_, val) = nz_number(b"42").unwrap();
assert_eq!(val, 42);
}
#[test]
fn nz_number64_rejects_zero() {
assert!(nz_number64(b"0").is_err());
}
#[test]
fn nz_number64_rejects_leading_zero() {
assert!(nz_number64(b"01").is_err());
}
#[test]
fn nz_number64_accepts_valid() {
let (_, val) = nz_number64(b"12345678901234").unwrap();
assert_eq!(val, 12345678901234);
}
#[test]
fn literal_count_u64_overflow() {
let result = literal(b"{99999999999999999999999}\r\n");
assert!(result.is_err());
}
#[test]
fn literal8_tolerates_plus_suffix() {
let result = literal(b"~{5+}\r\nhello");
assert!(
result.is_ok(),
"literal8 ~{{n+}} must be tolerated per Postel's law"
);
let (rest, val) = result.unwrap();
assert_eq!(val, b"hello");
assert!(rest.is_empty());
}
#[test]
fn literal8_basic() {
let (rest, val) = literal(b"~{5}\r\nhello rest").unwrap();
assert_eq!(val, b"hello");
assert_eq!(rest, b" rest");
}
#[test]
fn capability_appendlimit_no_value() {
let (_, cap) = capability(b"APPENDLIMIT").unwrap();
assert_eq!(cap, Capability::AppendLimit(None));
}
#[test]
fn capability_appendlimit_with_value() {
let (_, cap) = capability(b"APPENDLIMIT=1048576").unwrap();
assert_eq!(cap, Capability::AppendLimit(Some(1_048_576)));
}
#[test]
fn capability_appendlimit_non_numeric_falls_to_other() {
let (_, cap) = capability(b"APPENDLIMIT=abc").unwrap();
assert_eq!(cap, Capability::Other("APPENDLIMIT=abc".into()));
}
#[test]
fn capability_appendlimit_prefix_without_separator_falls_to_other() {
let (_, cap) = capability(b"APPENDLIMITXYZ").unwrap();
assert_eq!(cap, Capability::Other("APPENDLIMITXYZ".into()));
}
#[test]
fn capability_appendlimit_leading_plus_falls_to_other() {
let (_, cap) = capability(b"APPENDLIMIT=+123").unwrap();
assert_eq!(cap, Capability::Other("APPENDLIMIT=+123".into()));
}
#[test]
fn capability_other_unknown_string() {
let (_, cap) = capability(b"X-CUSTOM-CAP").unwrap();
assert_eq!(cap, Capability::Other("X-CUSTOM-CAP".into()));
}
#[test]
fn capability_other_preserves_original_case() {
let (_, cap) = capability(b"XMixedCase").unwrap();
assert_eq!(cap, Capability::Other("XMixedCase".into()));
}
#[test]
fn continuation_bracket_resp_text_parse_failure_fallback() {
let input = b"+ [INVALID!@#$%\r\n";
let (_, cont) = parse_continuation(input).unwrap();
assert!(cont.code.is_none());
assert_eq!(cont.data, "[INVALID!@#$%");
}
#[test]
fn greeting_ok_plain_text_no_code() {
let input = b"* OK Dovecot ready.\r\n";
let (_, resp) = parse_greeting(input).unwrap();
match resp {
Response::Greeting(g) => {
assert_eq!(g.status, GreetingStatus::Ok);
assert!(g.code.is_none());
assert_eq!(g.text, "Dovecot ready.");
}
other => panic!("expected Greeting, got {other:?}"),
}
}
#[test]
fn list_extended_unknown_item_parenthesized_value_skipped() {
let input = b"* LIST (\\HasNoChildren) \"/\" \"INBOX\" (\"XFUTURE\" (\"some\" \"data\"))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::List(info) = *boxed {
assert_eq!(info.name.as_str(), "INBOX");
assert_eq!(info.delimiter, Some('/'));
assert!(info.attributes.contains(&MailboxAttribute::HasNoChildren));
assert!(info.old_name.is_none());
assert!(info.child_info.is_empty());
return;
}
}
panic!("expected List response");
}
#[test]
fn list_extended_unknown_item_simple_value_skipped() {
let input = b"* LIST () \"/\" \"INBOX\" (\"XTOKEN\" 12345)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::List(info) = *boxed {
assert_eq!(info.name.as_str(), "INBOX");
return;
}
}
panic!("expected List response");
}
#[test]
fn list_extended_multiple_unknown_items_skipped() {
let input = b"* LIST () \"/\" \"INBOX\" (\"XFOO\" (\"nested\") \"XBAR\" 42)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::List(info) = *boxed {
assert_eq!(info.name.as_str(), "INBOX");
return;
}
}
panic!("expected List response");
}
#[test]
fn status_deleted_storage() {
let (_, items) = status_items(b"DELETED-STORAGE 2048").unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0], StatusItem::DeletedStorage(2048));
}
#[test]
fn status_deleted_storage_with_other_items() {
let input = b"STATUS \"INBOX\" (MESSAGES 10 DELETED-STORAGE 512)\r\n";
let (_, resp) = parse_untagged_status_mailbox(input, false).unwrap();
if let UntaggedResponse::MailboxStatus { mailbox, items } = resp {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(items.len(), 2);
assert!(items.contains(&StatusItem::Messages(10)));
assert!(items.contains(&StatusItem::DeletedStorage(512)));
} else {
panic!("expected MailboxStatus, got {resp:?}");
}
}
#[test]
fn status_deleted_storage_large_value() {
let (_, items) = status_items(b"DELETED-STORAGE 5000000000").unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0], StatusItem::DeletedStorage(5_000_000_000u64));
}
#[test]
fn namespace_descriptor_nil_delimiter() {
let input = b"NAMESPACE ((\"\" NIL)) NIL NIL\r\n";
let (_, resp) = parse_untagged_namespace(input, false).unwrap();
if let UntaggedResponse::Namespace { personal, .. } = resp {
assert_eq!(personal.len(), 1);
assert_eq!(personal[0].prefix, "");
assert_eq!(personal[0].delimiter, None);
} else {
panic!("expected Namespace, got {resp:?}");
}
}
#[test]
fn namespace_descriptor_empty_string_delimiter() {
let input = b"NAMESPACE ((\"\" \"\")) NIL NIL\r\n";
let (_, resp) = parse_untagged_namespace(input, false).unwrap();
if let UntaggedResponse::Namespace { personal, .. } = resp {
assert_eq!(personal.len(), 1);
assert_eq!(personal[0].delimiter, None);
} else {
panic!("expected Namespace, got {resp:?}");
}
}
#[test]
fn envelope_nil_in_reply_to_and_message_id() {
let input = b"(\"Mon, 1 Jan 2024 00:00:00 +0000\" \"Test\" \
((\"A\" NIL \"a\" \"x.com\")) \
NIL NIL \
((\"B\" NIL \"b\" \"y.com\")) \
NIL NIL \
NIL NIL)";
let (_, env) = envelope(input, false).unwrap();
assert!(env.in_reply_to.is_none(), "in-reply-to NIL must be None");
assert!(env.message_id.is_none(), "message-id NIL must be None");
assert!(env.cc.is_empty(), "cc NIL must be empty Vec");
assert!(env.bcc.is_empty(), "bcc NIL must be empty Vec");
}
#[test]
fn envelope_all_address_lists_nil() {
let input = b"(NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)";
let (_, env) = envelope(input, false).unwrap();
assert!(env.date.is_none());
assert!(env.subject.is_none());
assert!(env.from.is_empty());
assert!(env.sender.is_empty());
assert!(env.reply_to.is_empty());
assert!(env.to.is_empty());
assert!(env.cc.is_empty());
assert!(env.bcc.is_empty());
assert!(env.in_reply_to.is_none());
assert!(env.message_id.is_none());
}
#[test]
fn esearch_trailing_space_tolerated() {
let input = b"ESEARCH (TAG \"A001\") UID COUNT 3 \r\n";
let (_, resp) = parse_untagged_esearch(input)
.expect("ESEARCH with trailing space must parse (Postel's law)");
if let UntaggedResponse::Esearch(esearch) = resp {
assert_eq!(esearch.tag.as_deref(), Some("A001"));
assert!(esearch.uid);
assert_eq!(esearch.count, Some(3));
} else {
panic!("expected Esearch, got {resp:?}");
}
}
#[test]
fn esearch_no_result_data_immediate_crlf() {
let input = b"* ESEARCH (TAG \"A001\") UID\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Esearch(esearch) = *boxed {
assert!(esearch.uid);
assert!(esearch.all.is_empty());
assert_eq!(esearch.min, None);
assert_eq!(esearch.max, None);
assert_eq!(esearch.count, None);
return;
}
}
panic!("expected Esearch response");
}
#[test]
fn quotaroot_trailing_space_tolerated_per_postels_law() {
let input = b"QUOTAROOT INBOX \"\" \r\n";
let result = parse_untagged_quotaroot(input, false);
assert!(
result.is_ok(),
"trailing space after last root should be tolerated (Postel's law); \
got: {result:?}"
);
}
#[test]
fn quotaroot_no_roots_loop_break() {
let input = b"* QUOTAROOT INBOX\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::QuotaRoot { mailbox, roots } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert!(roots.is_empty());
return;
}
}
panic!("expected QuotaRoot response");
}
#[test]
fn capability_appendlimit_in_full_response() {
let input = b"* CAPABILITY IMAP4rev1 APPENDLIMIT\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Capability(caps) = *boxed {
assert!(
caps.contains(&Capability::AppendLimit(None)),
"Bare APPENDLIMIT must parse as AppendLimit(None): {caps:?}"
);
return;
}
}
panic!("expected Capability response");
}
#[test]
fn capability_appendlimit_with_value_in_full_response() {
let input = b"* CAPABILITY IMAP4rev1 APPENDLIMIT=1048576\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Capability(caps) = *boxed {
assert!(
caps.contains(&Capability::AppendLimit(Some(1_048_576))),
"APPENDLIMIT=1048576 must parse as AppendLimit(Some(1048576)): {caps:?}"
);
return;
}
}
panic!("expected Capability response");
}
#[test]
fn status_appendlimit_with_value() {
let (_, items) = status_items(b"APPENDLIMIT 104857600").unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0], StatusItem::AppendLimit(Some(104_857_600)));
}
#[test]
fn status_appendlimit_nil() {
let (_, items) = status_items(b"APPENDLIMIT NIL").unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0], StatusItem::AppendLimit(None));
}
#[test]
fn status_messages_overflow_preserves_other_items() {
let input = b"* STATUS INBOX (UIDVALIDITY 1 MESSAGES 4294967296)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::MailboxStatus { mailbox, items } = &*u {
assert_eq!(mailbox.as_str(), "INBOX");
assert!(
items
.iter()
.any(|i| matches!(i, StatusItem::UidValidity(1))),
"UIDVALIDITY 1 missing: {items:?}"
);
assert!(
!items.iter().any(|i| matches!(i, StatusItem::Messages(_))),
"overflowed MESSAGES should have been skipped: {items:?}"
);
} else {
panic!("expected MailboxStatus, got {u:?}");
}
} else {
panic!("expected Untagged Status response, not Unknown");
}
}
#[test]
fn status_highestmodseq_overflow_preserves_other_items() {
let input = b"* STATUS INBOX (MESSAGES 10 HIGHESTMODSEQ 9223372036854775808)\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::MailboxStatus { mailbox, items } = &*u {
assert_eq!(mailbox.as_str(), "INBOX");
assert!(
items.iter().any(|i| matches!(i, StatusItem::Messages(10))),
"MESSAGES 10 missing: {items:?}"
);
assert!(
!items
.iter()
.any(|i| matches!(i, StatusItem::HighestModSeq(_))),
"overflowed HIGHESTMODSEQ should have been skipped: {items:?}"
);
} else {
panic!("expected MailboxStatus, got {u:?}");
}
} else {
panic!("expected Untagged Status response, not Unknown");
}
}
#[test]
fn search_zero_in_results_filtered() {
let input = b"* SEARCH 1 0 3\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Search { uids, mod_seq } = &*u {
assert_eq!(
*uids,
vec![1, 3],
"0 must be filtered; valid UIDs (1, 3) preserved"
);
assert_eq!(*mod_seq, None);
} else {
panic!("expected Search response, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn sort_zero_in_results_filtered() {
let input = b"* SORT 2 0 7\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Sort { nums, mod_seq } = &*u {
assert_eq!(
*nums,
vec![2, 7],
"0 must be filtered; valid UIDs (2, 7) preserved"
);
assert_eq!(*mod_seq, None);
} else {
panic!("expected Sort response, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn edge_envelope_all_nil_fields() {
let input = b"* 1 FETCH (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Fetch(f) = &*u {
let env = f.envelope.as_ref().expect("envelope should be present");
assert!(
env.subject.is_none(),
"NIL subject must be None, got {:?}",
env.subject
);
assert!(
env.from.is_empty(),
"NIL from must be empty, got {:?}",
env.from
);
assert!(env.to.is_empty(), "NIL to must be empty, got {:?}", env.to);
assert!(
env.message_id.is_none(),
"NIL message-id must be None, got {:?}",
env.message_id
);
} else {
panic!("expected Fetch response, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn edge_bodystructure_empty_parens_params() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" () NIL NIL \"7BIT\" 42 3))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Fetch(f) = &*u {
let bs = f
.body_structure
.as_ref()
.expect("body_structure should be present");
if let BodyStructure::Text { params, .. } = bs {
assert!(
params.is_empty(),
"empty () params should produce empty vec, got {params:?}"
);
} else {
panic!("expected Text body structure, got {bs:?}");
}
} else {
panic!("expected Fetch response, got {u:?}");
}
} else {
panic!("expected Untagged");
}
}
#[test]
fn edge_utf7_escaped_ampersand() {
use crate::codec::utf7::{decode_utf7, encode_utf7};
assert_eq!(
decode_utf7(b"&-"),
"&",
"RFC 3501 Section 5.1.3: &- must decode to literal &"
);
assert_eq!(
encode_utf7("&"),
"&-",
"RFC 3501 Section 5.1.3: & must encode as &-"
);
assert_eq!(
decode_utf7(b"&-&-"),
"&&",
"RFC 3501 Section 5.1.3: consecutive &- must each decode to &"
);
assert_eq!(
decode_utf7(b"AT&-T"),
"AT&T",
"RFC 3501 Section 5.1.3: &- in the middle of text"
);
}
#[test]
fn edge_envelope_nil_date_valid_subject() {
let input = b"(NIL \"Draft subject\" \
((\"Alice\" NIL \"alice\" \"example.com\")) \
NIL NIL \
((\"Bob\" NIL \"bob\" \"example.com\")) \
NIL NIL NIL NIL)";
let (_, env) = envelope(input, false).unwrap();
assert!(env.date.is_none(), "NIL date must be None");
assert_eq!(env.subject.as_deref(), Some("Draft subject"));
assert_eq!(env.from.len(), 1);
assert_eq!(env.from[0].mailbox.as_deref(), Some("alice"));
}
#[test]
fn edge_bodystructure_message_rfc822_nested() {
let input = b"(\"MESSAGE\" \"RFC822\" NIL NIL NIL \"7BIT\" 500 \
(NIL \"Inner subject\" ((\"Inner\" NIL \"inner\" \"ex.com\")) NIL NIL NIL NIL NIL NIL NIL) \
((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5)(\"IMAGE\" \"PNG\" NIL NIL NIL \"BASE64\" 200) \"MIXED\") \
20)";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Message {
media_subtype,
envelope: env,
body,
..
} = &bs
{
assert_eq!(media_subtype, "rfc822");
assert_eq!(env.subject.as_deref(), Some("Inner subject"));
if let BodyStructure::Multipart {
media_subtype: inner_sub,
bodies,
..
} = body.as_ref()
{
assert_eq!(inner_sub, "mixed");
assert_eq!(bodies.len(), 2);
} else {
panic!("expected inner Multipart, got {body:?}");
}
} else {
panic!("expected Message, got {bs:?}");
}
}
#[test]
fn edge_envelope_q_encoded_subject_underscore() {
let input = b"(NIL \"=?UTF-8?Q?hello_world?=\" \
((\"Test\" NIL \"test\" \"ex.com\")) \
NIL NIL NIL NIL NIL NIL NIL)";
let (_, env) = envelope(input, false).unwrap();
assert_eq!(
env.subject.as_deref(),
Some("hello world"),
"RFC 2047 Section 4.2: underscore in Q-encoding must decode to space \
in IMAP ENVELOPE subject"
);
}
#[test]
fn envelope_nil_sender_reply_to_defaults_to_from() {
let input = b"(\"Mon, 7 Feb 2022 21:52:25 -0800\" \"Test\" \
((\"Alice\" NIL \"alice\" \"example.com\")) \
NIL \
NIL \
((\"Bob\" NIL \"bob\" \"example.com\")) \
NIL NIL NIL \
\"<msg@example.com>\")";
let (_, env) = envelope(input, false).unwrap();
assert_eq!(env.from.len(), 1);
assert_eq!(
env.sender, env.from,
"sender must default to from when server sends NIL (RFC 3501 Section 7.4.2)"
);
assert_eq!(
env.reply_to, env.from,
"reply_to must default to from when server sends NIL (RFC 3501 Section 7.4.2)"
);
}
#[test]
fn envelope_nil_sender_reply_to_stays_empty_when_from_nil() {
let input = b"(NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)";
let (_, env) = envelope(input, false).unwrap();
assert!(env.from.is_empty());
assert!(
env.sender.is_empty(),
"sender must remain empty when from is also NIL"
);
assert!(
env.reply_to.is_empty(),
"reply_to must remain empty when from is also NIL"
);
}
#[test]
fn envelope_explicit_sender_reply_to_not_overridden() {
let input = b"(\"Mon, 7 Feb 2022 21:52:25 -0800\" \"Test\" \
((\"Alice\" NIL \"alice\" \"example.com\")) \
((\"Secretary\" NIL \"secretary\" \"example.com\")) \
((\"ReplyAddr\" NIL \"reply\" \"example.com\")) \
((\"Bob\" NIL \"bob\" \"example.com\")) \
NIL NIL NIL \
\"<msg@example.com>\")";
let (_, env) = envelope(input, false).unwrap();
assert_eq!(env.sender.len(), 1);
assert_eq!(env.sender[0].mailbox.as_deref(), Some("secretary"));
assert_eq!(env.reply_to.len(), 1);
assert_eq!(env.reply_to[0].mailbox.as_deref(), Some("reply"));
}
#[test]
fn literal8_tolerates_non_synchronizing_modifier() {
let mut input = b"~{100+}\r\n".to_vec();
input.extend(std::iter::repeat(b'x').take(100));
let result = literal(&input);
assert!(
result.is_ok(),
"literal8 with '+' modifier must be tolerated per Postel's law; got {result:?}"
);
let (rest, val) = result.unwrap();
assert_eq!(val.len(), 100);
assert!(rest.is_empty());
}
#[test]
fn literal8_accepts_valid_syntax() {
let mut input = b"~{5}\r\n".to_vec();
input.extend(b"HELLO");
let result = literal(&input);
assert!(
result.is_ok(),
"valid literal8 ~{{5}} must be accepted (RFC 9051 Section 9); got {result:?}"
);
let (remaining, data) = result.unwrap();
assert_eq!(data, b"HELLO");
assert!(remaining.is_empty());
}
#[test]
fn skip_balanced_parens_literal_overflow_must_not_process_body_bytes() {
let input = b"key {100}\r\n)garbage real_close)after";
let (rest, ()) = skip_balanced_parens(input).unwrap();
assert!(
rest.len() > b")garbage real_close)after".len(),
"skip_balanced_parens must return early on literal overflow, not \
consume body bytes as structure (RFC 3501 Section 9); \
rest = {:?}",
String::from_utf8_lossy(rest)
);
}
#[test]
fn skip_balanced_parens_literal_checked_add_overflow() {
let input = b"ext {18446744073709551615}\r\ndata) rest";
let result = skip_balanced_parens(input);
assert!(
result.is_ok(),
"skip_balanced_parens must not panic on overflow: {result:?}"
);
}
#[test]
fn trailing_whitespace_id() {
let input = b"ID (\"name\" \"test\") \r\n";
let (_, resp) = parse_untagged_id(input).unwrap();
assert!(matches!(resp, UntaggedResponse::Id(_)));
}
#[test]
fn trailing_whitespace_namespace() {
let input = b"NAMESPACE ((\"\" \"/\")) NIL NIL \r\n";
let (_, resp) = parse_untagged_namespace(input, false).unwrap();
assert!(matches!(resp, UntaggedResponse::Namespace { .. }));
}
#[test]
fn trailing_whitespace_quota() {
let input = b"QUOTA \"\" (STORAGE 10 512) \r\n";
let (_, resp) = parse_untagged_quota(input).unwrap();
assert!(matches!(resp, UntaggedResponse::Quota { .. }));
}
#[test]
fn trailing_whitespace_quotaroot() {
let input = b"QUOTAROOT INBOX root1 \r\n";
let (_, resp) = parse_untagged_quotaroot(input, false).unwrap();
assert!(matches!(resp, UntaggedResponse::QuotaRoot { .. }));
}
#[test]
fn trailing_whitespace_acl() {
let input = b"ACL INBOX alice lrs \r\n";
let (_, resp) = parse_untagged_acl(input, false).unwrap();
assert!(matches!(resp, UntaggedResponse::Acl { .. }));
}
#[test]
fn trailing_whitespace_myrights() {
let input = b"MYRIGHTS INBOX lrs \r\n";
let (_, resp) = parse_untagged_myrights(input, false).unwrap();
assert!(matches!(resp, UntaggedResponse::MyRights { .. }));
}
#[test]
fn regression_esearch_unknown_key_no_value() {
let input = b"* ESEARCH (TAG \"t1\") UID COUNT 5 XFUTURE\r\n";
let resp = parse_response(input);
let (_, resp) = resp.expect(
"ESEARCH with trailing unknown key (no value) must parse \
per Postel's law (RFC 1122 Section 1.2.2)",
);
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Esearch(esearch) = *boxed {
assert_eq!(
esearch.count,
Some(5),
"COUNT must be preserved when trailing unknown key has no value"
);
assert!(esearch.uid, "UID indicator must be preserved");
assert_eq!(esearch.tag.as_deref(), Some("t1"), "TAG must be preserved");
return;
}
}
panic!("expected ESEARCH response");
}
#[test]
fn trailing_whitespace_listrights() {
let input = b"LISTRIGHTS INBOX fred lr s w \r\n";
let (_, resp) = parse_untagged_listrights(input, false).unwrap();
assert!(matches!(resp, UntaggedResponse::ListRights { .. }));
}
#[test]
fn trailing_whitespace_metadata_form1() {
let input = b"METADATA \"INBOX\" (/private/comment \"value\") \r\n";
let (_, resp) = parse_untagged_metadata(input, false).unwrap();
assert!(matches!(resp, UntaggedResponse::Metadata { .. }));
}
#[test]
fn trailing_whitespace_metadata_form2() {
let input = b"METADATA \"INBOX\" /private/comment \r\n";
let (_, resp) = parse_untagged_metadata(input, false).unwrap();
assert!(matches!(resp, UntaggedResponse::Metadata { .. }));
}
#[test]
fn bodystructure_multipart_whitespace_after_open_paren() {
let input = b"* 1 FETCH (BODYSTRUCTURE ( (\"TEXT\" \"PLAIN\" (\"charset\" \"utf-8\") NIL NIL \"7BIT\" 100 5)(\"TEXT\" \"HTML\" (\"charset\" \"utf-8\") NIL NIL \"7BIT\" 200 10) \"ALTERNATIVE\"))\r\n";
let (_, resp) =
parse_response(input).expect("BODYSTRUCTURE with space after opening paren should parse");
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
let bs = fr.body_structure.expect("should have body_structure");
if let BodyStructure::Multipart {
media_subtype,
bodies,
..
} = &bs
{
assert_eq!(media_subtype, "alternative");
assert_eq!(bodies.len(), 2, "expected 2 children in multipart");
if let BodyStructure::Text {
media_subtype: sub, ..
} = &bodies[0]
{
assert_eq!(sub, "plain");
} else {
panic!("expected first child to be Text, got {:?}", bodies[0]);
}
if let BodyStructure::Text {
media_subtype: sub, ..
} = &bodies[1]
{
assert_eq!(sub, "html");
} else {
panic!("expected second child to be Text, got {:?}", bodies[1]);
}
} else {
panic!("expected Multipart, got {bs:?}");
}
} else {
panic!("expected Fetch response");
}
} else {
panic!("expected Untagged response");
}
}
#[test]
fn bodystructure_body_params_multiple_spaces_between_pairs() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" (\"charset\" \"utf-8\" \"name\" \"file.txt\") NIL NIL \"7BIT\" 100 5))\r\n";
let (_, resp) =
parse_response(input).expect("BODYSTRUCTURE with double space in params should parse");
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
let bs = fr.body_structure.expect("should have body_structure");
if let BodyStructure::Text { params, .. } = &bs {
assert_eq!(
params.len(),
2,
"expected 2 parameter pairs, got {params:?}"
);
assert_eq!(params[0], ("charset".to_string(), "utf-8".to_string()));
assert_eq!(params[1], ("name".to_string(), "file.txt".to_string()));
} else {
panic!("expected Text, got {bs:?}");
}
} else {
panic!("expected Fetch response");
}
} else {
panic!("expected Untagged response");
}
}
#[test]
fn edge_envelope_all_nil_full_response() {
let input = b"* 1 FETCH (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL))\r\n";
let (_, resp) = parse_response(input)
.expect("ENVELOPE with all-NIL fields must parse (RFC 3501 Section 7.4.2)");
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
let env = fr
.envelope
.expect("FETCH response should contain an envelope");
assert!(
env.date.is_none(),
"date must be None for NIL (RFC 3501 Section 7.4.2)"
);
assert!(
env.subject.is_none(),
"subject must be None for NIL (RFC 3501 Section 7.4.2)"
);
assert!(
env.from.is_empty(),
"from must be empty for NIL (RFC 3501 Section 7.4.2)"
);
assert!(
env.sender.is_empty(),
"sender must be empty when from is also NIL (RFC 3501 Section 7.4.2)"
);
assert!(
env.reply_to.is_empty(),
"reply_to must be empty when from is also NIL (RFC 3501 Section 7.4.2)"
);
assert!(
env.to.is_empty(),
"to must be empty for NIL (RFC 3501 Section 7.4.2)"
);
assert!(
env.cc.is_empty(),
"cc must be empty for NIL (RFC 3501 Section 7.4.2)"
);
assert!(
env.bcc.is_empty(),
"bcc must be empty for NIL (RFC 3501 Section 7.4.2)"
);
assert!(
env.in_reply_to.is_none(),
"in_reply_to must be None for NIL (RFC 3501 Section 7.4.2)"
);
assert!(
env.message_id.is_none(),
"message_id must be None for NIL (RFC 3501 Section 7.4.2)"
);
return;
}
}
panic!("expected Untagged Fetch with Envelope");
}
#[test]
fn edge_fetch_unknown_data_items_skipped() {
let input = b"* 1 FETCH (UID 42 XFUTURE somevalue FLAGS (\\Seen) XBLOB (nested stuff))\r\n";
let (_, resp) =
parse_response(input).expect("FETCH with unknown data items must parse (Postel's law)");
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(
fr.uid,
Some(42),
"UID must be preserved when unknown items are present"
);
let flags = fr.flags.expect("FLAGS must be preserved");
assert_eq!(flags.len(), 1);
assert_eq!(flags[0], Flag::Seen);
return;
}
}
panic!("expected Untagged Fetch response");
}
#[test]
fn edge_list_contradictory_attributes_preserved() {
let input = b"* LIST (\\HasChildren \\HasNoChildren) \"/\" \"INBOX\"\r\n";
let (_, resp) = parse_response(input)
.expect("LIST with contradictory attributes must parse (Postel's law)");
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::List(info) = *boxed {
assert_eq!(info.name.as_str(), "INBOX");
assert_eq!(info.delimiter, Some('/'));
assert_eq!(
info.attributes.len(),
2,
"both contradictory attributes must be preserved; got {:?}",
info.attributes
);
assert_eq!(
info.attributes[0],
MailboxAttribute::HasChildren,
"first attribute must be \\HasChildren"
);
assert_eq!(
info.attributes[1],
MailboxAttribute::HasNoChildren,
"second attribute must be \\HasNoChildren"
);
return;
}
}
panic!("expected Untagged List response");
}
#[test]
fn edge_esearch_empty_uid_list() {
let input = b"* ESEARCH (TAG \"A001\") UID\r\n";
let (_, resp) = parse_response(input)
.expect("ESEARCH with empty UID result must parse (RFC 4731 Section 3.1)");
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Esearch(esearch) = *boxed {
assert_eq!(
esearch.tag.as_deref(),
Some("A001"),
"TAG must be preserved"
);
assert!(esearch.uid, "UID indicator must be true");
assert!(
esearch.all.is_empty(),
"ALL must be empty for an empty ESEARCH result"
);
assert_eq!(
esearch.min, None,
"MIN must be None for an empty ESEARCH result"
);
assert_eq!(
esearch.max, None,
"MAX must be None for an empty ESEARCH result"
);
assert_eq!(
esearch.count, None,
"COUNT must be None for an empty ESEARCH result"
);
assert_eq!(
esearch.mod_seq, None,
"MODSEQ must be None for an empty ESEARCH result"
);
return;
}
}
panic!("expected Untagged Esearch response");
}
#[test]
fn edge_quoted_string_trailing_escaped_backslash() {
let (rest, val) = quoted_string(b"\"hello\\\\\" rest").unwrap();
assert_eq!(
val, b"hello\\",
"\\\\ at end of quoted string must parse as single backslash \
(RFC 3501 Section 9: quoted-specials)"
);
assert_eq!(rest, b" rest", "rest must follow the closing quote");
}
#[test]
fn edge_literal_containing_crlf() {
let input = b"{10}\r\nhello\r\nbye rest";
let (rest, val) = literal(input).unwrap();
assert_eq!(
val, b"hello\r\nbye",
"literal must preserve embedded CRLF (RFC 3501 Section 9)"
);
assert_eq!(rest, b" rest");
}
#[test]
fn edge_status_unknown_items_skipped() {
let input = b"* STATUS INBOX (MESSAGES 5 UNKNOWN 3)\r\n";
let (_, resp) =
parse_response(input).expect("STATUS with unknown items must parse (Postel's law)");
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::MailboxStatus { mailbox, items } = *boxed {
assert_eq!(mailbox.as_str(), "INBOX");
assert_eq!(
items.len(),
1,
"only recognized status items should be present; got {items:?}"
);
assert_eq!(
items[0],
StatusItem::Messages(5),
"MESSAGES value must be preserved"
);
return;
}
}
panic!("expected Untagged MailboxStatus response");
}
#[test]
fn edge_bodystructure_rfc2231_continuation_reassembly() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"APPLICATION\" \"PDF\" (\"FILENAME*0\" \"very_long_\" \"FILENAME*1\" \"document.pdf\") NIL NIL \"BASE64\" 99999))\r\n";
let (_, resp) =
parse_response(input).expect("BODYSTRUCTURE with RFC 2231 continuation params must parse");
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
let bs = fr
.body_structure
.expect("FETCH response should contain a body_structure");
if let BodyStructure::Basic { params, .. } = &bs {
assert_eq!(
params.len(),
1,
"RFC 2231 continuation segments must be reassembled into one param; \
got {params:?}"
);
assert_eq!(
params[0].0, "filename",
"reassembled param key must be the base name"
);
assert_eq!(
params[0].1, "very_long_document.pdf",
"reassembled param value must concatenate all segments \
(RFC 2231 Section 3)"
);
return;
}
panic!("expected Basic body structure, got {bs:?}");
}
}
panic!("expected Untagged Fetch response");
}
#[test]
fn edge_fetch_literal_with_crlf() {
let input = b"* 1 FETCH (BODY[] {12}\r\nline1\r\nline2)\r\n";
let (_, resp) = parse_response(input)
.expect("FETCH with literal containing CRLF must parse (RFC 3501 Section 9)");
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::Fetch(fr) = *boxed {
assert_eq!(fr.body_sections.len(), 1, "should have one body section");
let section = &fr.body_sections[0];
assert_eq!(
section.data.as_deref(),
Some(b"line1\r\nline2".as_slice()),
"literal must preserve embedded CRLF bytes (RFC 3501 Section 9)"
);
return;
}
}
panic!("expected Untagged Fetch response");
}
#[test]
fn flag_list_tolerates_multiple_spaces() {
let (_, flags) = flag_list(b"(\\Seen \\Flagged \\Answered)").unwrap();
assert_eq!(
flags.len(),
3,
"double spaces between flags must be tolerated"
);
assert_eq!(flags[0], Flag::Seen);
assert_eq!(flags[1], Flag::Flagged);
assert_eq!(flags[2], Flag::Answered);
}
#[test]
fn capability_list_tolerates_multiple_spaces() {
let (_, resp) = parse_response(b"* CAPABILITY IMAP4rev1 IDLE LITERAL+\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Capability(caps) = &*u {
assert!(
caps.contains(&Capability::Imap4Rev1),
"IMAP4rev1 missing after double spaces"
);
assert!(
caps.contains(&Capability::Idle),
"IDLE missing after double spaces"
);
assert!(
caps.contains(&Capability::LiteralPlus),
"LITERAL+ missing after double spaces"
);
return;
}
}
panic!("expected Untagged Capability response");
}
#[test]
fn capability_list_tolerates_tabs() {
let (_, resp) = parse_response(b"* CAPABILITY IMAP4rev1\tIDLE\tLITERAL+\r\n").unwrap();
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Capability(caps) = &*u {
assert!(caps.contains(&Capability::Imap4Rev1));
assert!(caps.contains(&Capability::Idle));
assert!(caps.contains(&Capability::LiteralPlus));
return;
}
}
panic!("expected Untagged Capability response");
}
#[test]
fn list_attributes_tolerate_multiple_spaces() {
let input = b"* LIST (\\NoSelect \\HasChildren) \"/\" \"Archive\"\r\n";
let (_, resp) = parse_response(input)
.expect("LIST with double-spaced attributes must parse (Postel's law)");
if let Response::Untagged(boxed) = resp {
if let UntaggedResponse::List(info) = *boxed {
assert_eq!(info.name.as_str(), "Archive");
assert_eq!(
info.attributes.len(),
2,
"both attributes must be parsed despite double spaces; got {:?}",
info.attributes
);
assert_eq!(info.attributes[0], MailboxAttribute::NoSelect);
assert_eq!(info.attributes[1], MailboxAttribute::HasChildren);
return;
}
}
panic!("expected Untagged List response");
}
#[test]
fn bodystructure_fields_normalized_to_lowercase() {
let text_input = b"(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"BASE64\" 100 5)";
let (_, bs) = body_structure(text_input, false, 0).unwrap();
if let BodyStructure::Text {
media_subtype,
encoding,
..
} = &bs
{
assert_eq!(
media_subtype, "plain",
"media_subtype must be lowercase per RFC 2045 Section 5.1"
);
assert_eq!(
encoding, "base64",
"encoding must be lowercase per RFC 2045 Section 6"
);
} else {
panic!("expected Text, got {bs:?}");
}
let basic_input = b"(\"IMAGE\" \"PNG\" NIL NIL NIL \"BASE64\" 2048)";
let (_, bs) = body_structure(basic_input, false, 0).unwrap();
if let BodyStructure::Basic {
media_type,
media_subtype,
encoding,
..
} = &bs
{
assert_eq!(
media_type, "image",
"media_type must be lowercase per RFC 2045 Section 5.1"
);
assert_eq!(
media_subtype, "png",
"media_subtype must be lowercase per RFC 2045 Section 5.1"
);
assert_eq!(
encoding, "base64",
"encoding must be lowercase per RFC 2045 Section 6"
);
} else {
panic!("expected Basic, got {bs:?}");
}
let mpart_input = b"((\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 50 3)(\"TEXT\" \"HTML\" NIL NIL NIL \"7BIT\" 80 4) \"MIXED\")";
let (_, bs) = body_structure(mpart_input, false, 0).unwrap();
if let BodyStructure::Multipart { media_subtype, .. } = &bs {
assert_eq!(
media_subtype, "mixed",
"multipart media_subtype must be lowercase per RFC 2045 Section 5.1"
);
} else {
panic!("expected Multipart, got {bs:?}");
}
let msg_input = b"(\"MESSAGE\" \"RFC822\" NIL NIL NIL \"7BIT\" 500 \
(NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL) \
(\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 100 5) 20)";
let (_, bs) = body_structure(msg_input, false, 0).unwrap();
if let BodyStructure::Message { media_subtype, .. } = &bs {
assert_eq!(
media_subtype, "rfc822",
"message media_subtype must be lowercase per RFC 2045 Section 5.1"
);
} else {
panic!("expected Message, got {bs:?}");
}
}
#[test]
fn badcharset_double_space_between_charsets() {
let (_, code) = response_code(b"[BADCHARSET (\"UTF-8\" \"US-ASCII\")]").unwrap();
if let ResponseCode::BadCharset(charsets) = code {
assert_eq!(
charsets,
vec!["UTF-8", "US-ASCII"],
"double space between BADCHARSET charsets must be tolerated"
);
} else {
panic!("expected BadCharset, got {code:?}");
}
}
#[test]
fn body_language_double_space_between_tags() {
let (_, lang) = body_language(b"(\"en\" \"fr\")").unwrap();
assert_eq!(
lang,
Some(vec!["en".to_owned(), "fr".to_owned()]),
"double space between body-fld-lang tags must be tolerated"
);
}
#[test]
fn list_childinfo_double_space_between_items() {
let input =
b"* LIST (\\HasChildren) \".\" \"Parent\" (\"CHILDINFO\" (\"SUBSCRIBED\" \"REMOTE\"))\r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(body) = resp {
if let UntaggedResponse::List(mailbox) = body.as_ref() {
assert_eq!(
mailbox.child_info,
vec!["SUBSCRIBED", "REMOTE"],
"double space between CHILDINFO items must be tolerated"
);
} else {
panic!("expected List, got {body:?}");
}
} else {
panic!("expected Untagged response, got {resp:?}");
}
}
#[test]
fn bodystructure_bare_atom_media_type() {
let input = b"(TEXT PLAIN (\"CHARSET\" \"UTF-8\") NIL NIL 7BIT 42 3)";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Text {
media_subtype,
encoding,
size,
lines,
..
} = &bs
{
assert_eq!(media_subtype, "plain");
assert_eq!(encoding, "7bit");
assert_eq!(*size, 42);
assert_eq!(*lines, 3);
} else {
panic!("expected Text, got {bs:?}");
}
}
#[test]
fn bodystructure_bare_atom_basic_type() {
let input = b"(IMAGE JPEG NIL NIL NIL BASE64 12345)";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Basic {
media_type,
media_subtype,
encoding,
size,
..
} = &bs
{
assert_eq!(media_type, "image");
assert_eq!(media_subtype, "jpeg");
assert_eq!(encoding, "base64");
assert_eq!(*size, 12345);
} else {
panic!("expected Basic, got {bs:?}");
}
}
#[test]
fn bodystructure_bare_atom_multipart_subtype() {
let input =
b"((\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 10 1)(\"TEXT\" \"HTML\" NIL NIL NIL \"7BIT\" 20 2) MIXED)";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Multipart {
media_subtype,
bodies,
..
} = &bs
{
assert_eq!(media_subtype, "mixed");
assert_eq!(bodies.len(), 2);
} else {
panic!("expected Multipart, got {bs:?}");
}
}
#[test]
fn body_params_double_space_within_pair() {
let input = b"(\"CHARSET\" \"UTF-8\")";
let (_, params) = body_params(input).unwrap();
assert_eq!(params.len(), 1);
assert_eq!(params[0].0, "charset");
assert_eq!(params[0].1, "UTF-8");
}
#[test]
fn bodystructure_params_double_space_within_pair() {
let input = b"(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5)";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Text { params, .. } = &bs {
assert_eq!(params.len(), 1);
assert_eq!(params[0].0, "charset");
assert_eq!(params[0].1, "UTF-8");
} else {
panic!("expected Text, got {bs:?}");
}
}
#[test]
fn bodystructure_mpart_multi_space_before_subtype() {
let input = b"((\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 10 1) \"MIXED\")";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Multipart {
media_subtype,
bodies,
..
} = &bs
{
assert_eq!(media_subtype, "mixed");
assert_eq!(bodies.len(), 1);
} else {
panic!("expected Multipart, got {bs:?}");
}
}
#[test]
fn bodystructure_mpart_tab_before_subtype() {
let input = b"((\"TEXT\" \"PLAIN\" NIL NIL NIL \"7BIT\" 10 1)\t\"MIXED\")";
let (_, bs) = body_structure(input, false, 0).unwrap();
if let BodyStructure::Multipart {
media_subtype,
bodies,
..
} = &bs
{
assert_eq!(media_subtype, "mixed");
assert_eq!(bodies.len(), 1);
} else {
panic!("expected Multipart, got {bs:?}");
}
}
#[test]
fn exists_trailing_whitespace_tolerated() {
let input = b"* 5 EXISTS \r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
assert_eq!(
*boxed,
UntaggedResponse::Exists(5),
"EXISTS with trailing space must parse as Exists(5), not Unknown"
);
} else {
panic!("expected Untagged Exists");
}
}
#[test]
fn recent_trailing_whitespace_tolerated() {
let input = b"* 0 RECENT \r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
assert_eq!(
*boxed,
UntaggedResponse::Recent(0),
"RECENT with trailing space must parse as Recent(0), not Unknown"
);
} else {
panic!("expected Untagged Recent");
}
}
#[test]
fn expunge_trailing_whitespace_tolerated() {
let input = b"* 3 EXPUNGE \r\n";
let (_, resp) = parse_response(input).unwrap();
if let Response::Untagged(boxed) = resp {
assert_eq!(
*boxed,
UntaggedResponse::Expunge(3),
"EXPUNGE with trailing space must parse as Expunge(3), not Unknown"
);
} else {
panic!("expected Untagged Expunge");
}
}
#[test]
fn fetch_bodystructure_extra_spaces_full_response() {
let input = b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5))\r\n";
let result = parse_response(input);
assert!(
result.is_ok(),
"BODYSTRUCTURE with extra spaces must parse as full FETCH response"
);
}
#[test]
fn fetch_bodystructure_bare_atom_full_response() {
let input = b"* 1 FETCH (BODYSTRUCTURE (TEXT PLAIN (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5))\r\n";
let result = parse_response(input);
assert!(
result.is_ok(),
"BODYSTRUCTURE with bare atom media type must parse as full FETCH response"
);
}
#[test]
fn fetch_bodystructure_multipart_extra_spaces_full_response() {
let input = b"* 1 FETCH (BODYSTRUCTURE ((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5)(\"TEXT\" \"HTML\" (\"CHARSET\" \"UTF-8\") NIL NIL \"QUOTED-PRINTABLE\" 200 10) \"ALTERNATIVE\"))\r\n";
let result = parse_response(input);
assert!(
result.is_ok(),
"multipart BODYSTRUCTURE with extra spaces must parse as full FETCH response"
);
}
mod prop_invariants {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn parse_response_never_panics(data in prop::collection::vec(any::<u8>(), 0..500)) {
let _ = parse_response_utf8(&data, false);
let _ = parse_response_utf8(&data, true);
}
#[test]
fn parse_greeting_never_panics(data in prop::collection::vec(any::<u8>(), 0..500)) {
let _ = parse_greeting(&data);
}
#[test]
fn parse_response_consumes_bytes(data in prop::collection::vec(any::<u8>(), 1..500)) {
if let Ok((remaining, _)) = parse_response_utf8(&data, false) {
prop_assert!(
remaining.len() < data.len(),
"parser must consume at least one byte on success"
);
}
}
#[test]
fn search_uids_nonzero(data in prop::collection::vec(any::<u8>(), 0..500)) {
if let Ok((_, Response::Untagged(boxed))) = parse_response_utf8(&data, false).as_ref() {
if let UntaggedResponse::Search { uids, .. } = boxed.as_ref() {
for uid in uids {
prop_assert!(*uid >= 1, "UID must be >= 1 (RFC 3501 Section 9), got {}", uid);
}
}
}
}
#[test]
fn parse_response_does_not_overconsume(data in prop::collection::vec(any::<u8>(), 0..200)) {
if let Ok((rest_alone, _)) = parse_response_utf8(&data, false) {
let sentinel = b"A001 OK done\r\n";
let mut combined = data.clone();
combined.extend_from_slice(sentinel);
if let Ok((remaining, _)) = parse_response_utf8(&combined, false) {
prop_assert!(
remaining.len() >= sentinel.len(),
"parser consumed into sentinel: remaining={} < sentinel={}, \
data.len()={}, rest_alone.len()={}",
remaining.len(), sentinel.len(),
data.len(), rest_alone.len()
);
}
}
}
}
fn valid_imap_responses() -> Vec<&'static [u8]> {
vec![
b"* 42 EXISTS\r\n",
b"* 5 RECENT\r\n",
b"* 3 EXPUNGE\r\n",
b"* FLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)\r\n",
b"* CAPABILITY IMAP4rev1 STARTTLS AUTH=PLAIN IDLE\r\n",
b"* LIST (\\HasNoChildren) \"/\" \"INBOX\"\r\n",
b"* SEARCH 1 2 3 10 20\r\n",
b"* STATUS INBOX (MESSAGES 10 UNSEEN 3)\r\n",
b"* OK [READ-WRITE] SELECT completed\r\n",
b"* NO [AUTHENTICATIONFAILED] Login failed\r\n",
b"* 1 FETCH (UID 42 FLAGS (\\Seen) RFC822.SIZE 1024)\r\n",
b"* 1 FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5))\r\n",
b"* 1 FETCH (ENVELOPE (\"Mon, 7 Feb 2022 21:52:25 -0800\" \"Subject\" ((\"Name\" NIL \"user\" \"host.com\")) NIL NIL ((\"To\" NIL \"rcpt\" \"host.com\")) NIL NIL NIL \"<msgid@host.com>\"))\r\n",
b"* ENABLED CONDSTORE\r\n",
b"* VANISHED 1:3,5\r\n",
b"* SORT 5 3 1 10\r\n",
]
}
fn inject_extra_whitespace(input: &[u8], insertions: &[(usize, u8)]) -> Vec<u8> {
let mut result = Vec::with_capacity(input.len() + insertions.len());
let data_start = 2;
let data_end = input.len().saturating_sub(2); let sp_positions: Vec<usize> = input
.iter()
.enumerate()
.filter(|&(i, &b)| b == b' ' && i >= data_start && i < data_end)
.map(|(i, _)| i)
.collect();
if sp_positions.is_empty() {
return input.to_vec();
}
let mut extra_at: std::collections::HashSet<usize> = std::collections::HashSet::new();
for &(idx, _) in insertions {
extra_at.insert(sp_positions[idx % sp_positions.len()]);
}
for (i, &b) in input.iter().enumerate() {
result.push(b);
if extra_at.contains(&i) {
result.push(b' ');
}
}
result
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn extra_whitespace_still_parses(
response_idx in 0..16usize,
insertions in prop::collection::vec((0..50usize, any::<u8>()), 1..5),
) {
let responses = valid_imap_responses();
let response = responses[response_idx % responses.len()];
let mutated = inject_extra_whitespace(response, &insertions);
let result = parse_response(&mutated);
prop_assert!(
result.is_ok(),
"valid response with extra whitespace must still parse.\n\
Original: {:?}\nMutated: {:?}",
String::from_utf8_lossy(response),
String::from_utf8_lossy(&mutated)
);
}
#[test]
fn bodystructure_quoted_and_unquoted_media_types(
media_type in prop_oneof![
Just("TEXT"), Just("IMAGE"), Just("APPLICATION"),
Just("AUDIO"), Just("VIDEO"), Just("MESSAGE"),
],
subtype in prop_oneof![
Just("PLAIN"), Just("HTML"), Just("MIXED"),
Just("ALTERNATIVE"), Just("OCTET-STREAM"), Just("PDF"),
Just("PNG"),
],
) {
let quoted = format!(
"* 1 FETCH (BODYSTRUCTURE (\
\"{media_type}\" \"{subtype}\" \
(\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5))\r\n"
);
let r1 = parse_response(quoted.as_bytes());
prop_assert!(r1.is_ok(), "quoted form must parse: {:?}", quoted);
let unquoted = format!(
"* 1 FETCH (BODYSTRUCTURE (\
{media_type} {subtype} \
(\"CHARSET\" \"UTF-8\") NIL NIL \"7BIT\" 100 5))\r\n"
);
let r2 = parse_response(unquoted.as_bytes());
prop_assert!(r2.is_ok(), "unquoted form must parse: {:?}", unquoted);
}
#[test]
fn bodystructure_fields_always_lowercase(
media in prop_oneof![
Just("TEXT"), Just("text"), Just("Text"), Just("tExT"),
],
subtype in prop_oneof![
Just("PLAIN"), Just("plain"), Just("Plain"), Just("pLaIn"),
],
encoding in prop_oneof![
Just("7BIT"), Just("7bit"), Just("BASE64"),
Just("base64"), Just("QUOTED-PRINTABLE"),
],
) {
let input = format!(
"* 1 FETCH (BODYSTRUCTURE (\
\"{media}\" \"{subtype}\" NIL NIL NIL \
\"{encoding}\" 100 5))\r\n"
);
if let Ok((_, Response::Untagged(u))) =
parse_response(input.as_bytes()).as_ref()
{
if let UntaggedResponse::Fetch(f) = u.as_ref() {
if let Some(BodyStructure::Text {
media_subtype,
encoding: enc,
..
}) = &f.body_structure
{
prop_assert_eq!(
media_subtype,
&subtype.to_ascii_lowercase(),
"subtype must be lowercase"
);
prop_assert_eq!(
enc,
&encoding.to_ascii_lowercase(),
"encoding must be lowercase"
);
}
}
}
}
}
}
mod stuck_tests {
use super::*;
use std::time::{Duration, Instant};
const MAX_PARSE_TIME: Duration = Duration::from_secs(5);
fn assert_terminates<F: FnOnce()>(name: &str, f: F) {
let start = Instant::now();
f();
let elapsed = start.elapsed();
assert!(
elapsed < MAX_PARSE_TIME,
"{name} took {elapsed:?}, exceeds {MAX_PARSE_TIME:?} — parser may be stuck"
);
}
#[test]
fn parse_response_repeated_bytes_100kb() {
let data = vec![0xFFu8; 100_000];
assert_terminates("100KB 0xFF", || {
let _ = parse_response_utf8(&data, false);
});
}
#[test]
fn parse_response_repeated_star_100kb() {
let mut data = b"* ".to_vec();
data.extend(vec![b'A'; 100_000]);
data.extend(b"\r\n");
assert_terminates("100KB untagged garbage", || {
let _ = parse_response_utf8(&data, false);
});
}
#[test]
fn parse_response_deeply_nested_parens() {
let mut data = b"* 1 FETCH (BODYSTRUCTURE ".to_vec();
for _ in 0..10_000 {
data.push(b'(');
}
data.extend(b")\r\n");
assert_terminates("10K nested parens", || {
let _ = parse_response_utf8(&data, false);
});
}
#[test]
fn parse_response_long_literal_count() {
let data = b"* 1 FETCH (BODY[] {999999999}\r\n";
assert_terminates("huge literal count", || {
let _ = parse_response_utf8(data, false);
});
}
#[test]
fn parse_greeting_repeated_bytes_100kb() {
let mut data = b"* OK ".to_vec();
data.extend(vec![b'X'; 100_000]);
data.extend(b"\r\n");
assert_terminates("100KB greeting", || {
let _ = parse_greeting(&data);
});
}
}
#[test]
fn capability_notify_parsed() {
let input = b"* CAPABILITY IMAP4rev1 NOTIFY\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Capability(caps) = *u {
assert!(
caps.contains(&Capability::Notify),
"should parse NOTIFY capability, got: {caps:?}"
);
} else {
panic!("expected Capability, got: {u:?}");
}
} else {
panic!("expected Untagged, got: {resp:?}");
}
}
#[test]
fn capability_notify_round_trip() {
let cap = Capability::from_imap_str("NOTIFY");
assert_eq!(cap, Capability::Notify);
assert_eq!(cap.as_imap_str(), "NOTIFY");
}
#[test]
fn capability_notify_case_insensitive() {
assert_eq!(Capability::from_imap_str("notify"), Capability::Notify);
assert_eq!(Capability::from_imap_str("Notify"), Capability::Notify);
}
#[test]
fn capability_notify_cross_representation() {
assert_eq!(Capability::Notify, Capability::Other("NOTIFY".into()));
assert_eq!(Capability::Notify, Capability::Other("notify".into()));
}
#[test]
fn list_noaccess_attribute_parsed() {
let input = b"* LIST (\\NoAccess) \"/\" \"SharedFolder\"\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = *u {
assert!(
info.attributes
.contains(&crate::types::MailboxAttribute::NoAccess),
"should parse \\NoAccess attribute, got: {:?}",
info.attributes
);
} else {
panic!("expected List, got: {u:?}");
}
} else {
panic!("expected Untagged, got: {resp:?}");
}
}
#[test]
fn list_noaccess_case_insensitive() {
let input = b"* LIST (\\NOACCESS) \"/\" \"folder\"\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = *u {
assert!(
info.attributes
.contains(&crate::types::MailboxAttribute::NoAccess),
"\\NOACCESS (all caps) should match NoAccess, got: {:?}",
info.attributes
);
} else {
panic!("expected List, got: {u:?}");
}
} else {
panic!("expected Untagged, got: {resp:?}");
}
}
#[test]
fn response_code_notification_overflow() {
let input = b"* OK [NOTIFICATIONOVERFLOW] server cannot keep up\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Untagged(u) = resp {
if let UntaggedResponse::Status { code, .. } = *u {
assert!(
matches!(code, Some(ResponseCode::NotificationOverflow(_))),
"should parse NOTIFICATIONOVERFLOW, got: {code:?}"
);
} else {
panic!("expected Status, got: {u:?}");
}
} else {
panic!("expected Untagged, got: {resp:?}");
}
}
#[test]
fn response_code_badevent() {
let input = b"A001 NO [BADEVENT (AnnotationChange)] unsupported events\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Tagged(tagged) = resp {
assert_eq!(tagged.status, StatusKind::No);
assert!(
matches!(tagged.code, Some(ResponseCode::BadEvent(_))),
"should parse BADEVENT, got: {:?}",
tagged.code
);
} else {
panic!("expected Tagged, got: {resp:?}");
}
}
#[test]
fn response_code_badevent_multiple_events() {
let input =
b"A001 NO [BADEVENT (AnnotationChange FlagChange ServerMetadataChange)] unsupported\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Tagged(tagged) = resp {
assert_eq!(tagged.status, StatusKind::No);
if let Some(ResponseCode::BadEvent(Some(ref events))) = tagged.code {
assert!(
events.contains("AnnotationChange"),
"should contain AnnotationChange, got: {events}"
);
assert!(
events.contains("FlagChange"),
"should contain FlagChange, got: {events}"
);
assert!(
events.contains("ServerMetadataChange"),
"should contain ServerMetadataChange, got: {events}"
);
} else {
panic!("expected BadEvent with event list, got: {:?}", tagged.code);
}
} else {
panic!("expected Tagged, got: {resp:?}");
}
}
#[test]
fn response_code_badevent_empty_parens() {
let input = b"A001 NO [BADEVENT ()] no events supported\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Tagged(tagged) = resp {
assert_eq!(tagged.status, StatusKind::No);
assert!(
matches!(tagged.code, Some(ResponseCode::BadEvent(_))),
"should parse BADEVENT with empty parens, got: {:?}",
tagged.code
);
} else {
panic!("expected Tagged, got: {resp:?}");
}
}
#[test]
fn status_response_for_notify_event() {
let input = b"* STATUS \"Lists/Lemonade\" (UIDVALIDITY 4 UIDNEXT 10000 MESSAGES 501)\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Untagged(u) = resp {
if let UntaggedResponse::MailboxStatus { mailbox, items } = *u {
assert_eq!(mailbox.as_str(), "Lists/Lemonade");
assert!(
items.len() >= 3,
"should have UIDVALIDITY, UIDNEXT, MESSAGES, got: {items:?}"
);
} else {
panic!("expected MailboxStatus, got: {u:?}");
}
} else {
panic!("expected Untagged, got: {resp:?}");
}
}
#[test]
fn list_with_oldname_for_notify_rename() {
let input = b"* LIST () \"/\" \"NewMailbox\" (\"OLDNAME\" (\"OldMailbox\"))\r\n";
let (rest, resp) = parse_response(input).unwrap();
assert!(rest.is_empty());
if let Response::Untagged(u) = resp {
if let UntaggedResponse::List(info) = *u {
assert_eq!(info.name.as_str(), "NewMailbox");
assert_eq!(
info.old_name.as_ref().map(MailboxName::as_str),
Some("OldMailbox"),
"OLDNAME must be captured for NOTIFY rename events"
);
} else {
panic!("expected List, got: {u:?}");
}
} else {
panic!("expected Untagged, got: {resp:?}");
}
}