use nom::branch::alt;
use nom::bytes::complete::{tag, tag_no_case, take, take_while, take_while1};
use nom::character::complete::{char, digit1};
use nom::combinator::{map, opt, peek, value};
use nom::multi::{many0, separated_list0, separated_list1};
use nom::sequence::{delimited, preceded, terminated, tuple};
use nom::IResult;
use crate::types::body::{BodyStructure, ContentDisposition};
use crate::types::envelope::{Envelope, EnvelopeAddress};
use crate::types::fetch::{BinarySection, BodySection, FetchResponse};
use crate::types::flag::Flag;
use crate::types::mailbox::{MailboxAttribute, MailboxInfo, StatusItem};
use crate::types::response::{
AclEntry, Capability, ContinuationRequest, EsearchResponse, GreetingResponse, GreetingStatus,
MetadataEntry, NamespaceDescriptor, QuotaResource, Response, ResponseCode, StatusKind,
TaggedResponse, ThreadNode, UidRange, UntaggedResponse, UntaggedStatus,
};
#[cfg(test)]
pub(crate) fn parse_response(input: &[u8]) -> IResult<&[u8], Response> {
parse_response_utf8(input, false)
}
pub(crate) fn parse_response_utf8(input: &[u8], utf8_mode: bool) -> IResult<&[u8], Response> {
alt((
map(parse_continuation, Response::Continuation),
map(
|i| parse_untagged(i, utf8_mode),
|u| Response::Untagged(Box::new(u)),
),
map(parse_tagged, Response::Tagged),
))(input)
}
pub(crate) fn parse_greeting(input: &[u8]) -> IResult<&[u8], Response> {
let (input, _) = tag(b"* ")(input)?;
let (input, status) = alt((
value(GreetingStatus::Ok, tag_no_case(b"OK")),
value(GreetingStatus::PreAuth, tag_no_case(b"PREAUTH")),
value(GreetingStatus::Bye, tag_no_case(b"BYE")),
))(input)?;
let (input, maybe_sp) = opt(sp)(input)?;
if maybe_sp.is_some() {
let (input, (code, text)) = resp_text(input)?;
let (input, _) = crlf(input)?;
Ok((
input,
Response::Greeting(GreetingResponse { status, code, text }),
))
} else {
let (input, _) = crlf(input)?;
Ok((
input,
Response::Greeting(GreetingResponse {
status,
code: None,
text: String::new(),
}),
))
}
}
fn crlf(input: &[u8]) -> IResult<&[u8], &[u8]> {
tag(b"\r\n")(input)
}
fn sp(input: &[u8]) -> IResult<&[u8], u8> {
nom::character::complete::char(' ')(input).map(|(i, _)| (i, b' '))
}
fn skip_parenthesized_block(input: &[u8]) -> IResult<&[u8], ()> {
let (input, _) = char('(')(input)?;
let (input, ()) = skip_balanced_parens(input)?;
let (input, _) = char(')')(input)?;
Ok((input, ()))
}
fn skip_balanced_parens(mut input: &[u8]) -> IResult<&[u8], ()> {
let mut depth: u32 = 0;
loop {
if input.is_empty() || (depth == 0 && input[0] == b')') {
return Ok((input, ()));
}
match input[0] {
b'(' => {
depth += 1;
input = &input[1..];
}
b')' if depth > 0 => {
depth -= 1;
input = &input[1..];
}
b'"' => {
input = &input[1..];
while !input.is_empty() && input[0] != b'"' {
if input[0] == b'\\' && input.len() > 1 {
input = &input[2..]; } else {
input = &input[1..];
}
}
if !input.is_empty() {
input = &input[1..]; }
}
b'~' if input.len() > 1 && input[1] == b'{' => {
input = &input[1..]; }
b'{' => {
input = &input[1..]; let start = 0;
let mut end = start;
while end < input.len() && input[end].is_ascii_digit() {
end += 1;
}
if end > start && end < input.len() {
let count_end = end;
if input[end] == b'+' {
end += 1;
}
if end < input.len() && input[end] == b'}' {
end += 1; if end + 1 < input.len() && input[end] == b'\r' && input[end + 1] == b'\n' {
end += 2;
if let Ok(s) = std::str::from_utf8(&input[start..count_end]) {
if let Ok(count) = s.parse::<usize>() {
if let Some(new_end) = end.checked_add(count) {
if new_end <= input.len() {
input = &input[new_end..];
}
}
}
}
}
}
}
}
_ => {
input = &input[1..];
}
}
}
}
fn atom(input: &[u8]) -> IResult<&[u8], &[u8]> {
take_while1(is_atom_char)(input)
}
fn is_objectid_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_' || b == b'-'
}
fn objectid(input: &[u8]) -> IResult<&[u8], &[u8]> {
let (rest, val) = atom(input)?;
let all_objectid = val.iter().all(|&b| is_objectid_char(b));
if all_objectid && val.len() <= 255 {
Ok((rest, val))
} else if all_objectid {
Ok((&input[255..], &input[..255]))
} else {
Ok((rest, val))
}
}
fn fetch_attr_atom(input: &[u8]) -> IResult<&[u8], &[u8]> {
take_while1(is_atom_char_no_bracket)(input)
}
fn tag_str(input: &[u8]) -> IResult<&[u8], &[u8]> {
take_while1(is_tag_char)(input)
}
fn is_tag_char(b: u8) -> bool {
(is_atom_char(b) || b == b']') && b != b'+'
}
fn is_atom_char(b: u8) -> bool {
b > 0x1F
&& b != 0x7F
&& b != b' '
&& b != b'('
&& b != b')'
&& b != b'{'
&& b != b'%'
&& b != b'*'
&& b != b'"'
&& b != b'\\'
&& b != b']'
}
fn is_atom_char_no_bracket(b: u8) -> bool {
is_atom_char(b) && b != b'['
}
fn quoted_string(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
let (input, _) = char('"')(input)?;
let mut result = Vec::new();
let mut i = input;
loop {
if i.is_empty() {
return Err(nom::Err::Error(nom::error::Error::new(
i,
nom::error::ErrorKind::Char,
)));
}
match i[0] {
b'"' => {
return Ok((&i[1..], result));
}
b'\\' => {
if i.len() < 2 {
return Err(nom::Err::Error(nom::error::Error::new(
i,
nom::error::ErrorKind::Char,
)));
}
let escaped = i[1];
if escaped != b'"' && escaped != b'\\' {
tracing::debug!(
escaped_byte = escaped,
"non-standard quoted-string escape: preserving backslash as literal data"
);
result.push(b'\\');
}
result.push(escaped);
i = &i[2..];
}
0 => {
return Err(nom::Err::Error(nom::error::Error::new(
i,
nom::error::ErrorKind::Char,
)));
}
b => {
if b == b'\r' || b == b'\n' {
return Err(nom::Err::Error(nom::error::Error::new(
i,
nom::error::ErrorKind::Char,
)));
}
result.push(b);
i = &i[1..];
}
}
}
}
fn literal(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
let (input, is_literal8) = opt(char('~'))(input)?;
let (input, _) = char('{')(input)?;
let (input, count_bytes) = digit1(input)?;
let count_str = std::str::from_utf8(count_bytes).map_err(|_| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
})?;
let count_u64: u64 = count_str.parse().map_err(|_| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
})?;
if count_u64 > i64::MAX as u64 {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)));
}
let count: usize = usize::try_from(count_u64).map_err(|_| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Verify))
})?;
let (input, has_plus) = opt(char('+'))(input)?;
if is_literal8.is_some() && has_plus.is_some() {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)));
}
let (input, _) = char('}')(input)?;
let (input, _) = crlf(input)?;
let (input, data) = take(count)(input)?;
Ok((input, data.to_vec()))
}
fn string(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
alt((quoted_string, literal))(input)
}
fn astring(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
alt((
map(astring_chars, |s: &[u8]| s.to_vec()),
string,
))(input)
}
fn astring_chars(input: &[u8]) -> IResult<&[u8], &[u8]> {
take_while1(|b: u8| is_atom_char(b) || b == b']')(input)
}
fn nstring(input: &[u8]) -> IResult<&[u8], Option<Vec<u8>>> {
alt((value(None, tag_no_case(b"NIL")), map(string, Some)))(input)
}
fn number(input: &[u8]) -> IResult<&[u8], u32> {
let (input, digits) = digit1(input)?;
let s = std::str::from_utf8(digits).map_err(|_| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
})?;
let n: u32 = s.parse().map_err(|_| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
})?;
Ok((input, n))
}
fn nz_number(input: &[u8]) -> IResult<&[u8], u32> {
if input.first() == Some(&b'0') {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)));
}
let (rest, n) = number(input)?;
if n == 0 {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)));
}
Ok((rest, n))
}
fn number64(input: &[u8]) -> IResult<&[u8], u64> {
let (input, digits) = digit1(input)?;
let s = std::str::from_utf8(digits).map_err(|_| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
})?;
let n: u64 = s.parse().map_err(|_| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
})?;
if n > i64::MAX as u64 {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)));
}
Ok((input, n))
}
fn nz_number64(input: &[u8]) -> IResult<&[u8], u64> {
if input.first() == Some(&b'0') {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)));
}
let (rest, n) = number64(input)?;
if n == 0 {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)));
}
Ok((rest, n))
}
fn nstring_utf8(input: &[u8]) -> IResult<&[u8], Option<String>> {
let (input, val) = nstring(input)?;
Ok((input, val.map(|v| String::from_utf8_lossy(&v).into_owned())))
}
fn string_utf8(input: &[u8]) -> IResult<&[u8], String> {
let (input, val) = string(input)?;
Ok((input, String::from_utf8_lossy(&val).into_owned()))
}
fn astring_utf8(input: &[u8]) -> IResult<&[u8], String> {
let (input, val) = astring(input)?;
Ok((input, String::from_utf8_lossy(&val).into_owned()))
}
fn flag_or_perm(input: &[u8]) -> IResult<&[u8], Flag> {
alt((
map(tag(b"\\*"), |_| Flag::Wildcard),
map(tuple((char('\\'), atom)), |(_, name)| {
let full = format!("\\{}", String::from_utf8_lossy(name));
Flag::from_imap_str(&full)
}),
map(atom, |a| Flag::from_imap_str(&String::from_utf8_lossy(a))),
))(input)
}
fn parse_flag_list(input: &[u8], allow_wildcard: bool) -> IResult<&[u8], Vec<Flag>> {
let (input, flags) = delimited(
char('('),
separated_list0(char(' '), flag_or_perm),
char(')'),
)(input)?;
if allow_wildcard {
Ok((input, flags))
} else {
Ok((
input,
flags.into_iter().filter(|f| *f != Flag::Wildcard).collect(),
))
}
}
fn flag_list(input: &[u8]) -> IResult<&[u8], Vec<Flag>> {
parse_flag_list(input, false)
}
fn flag_perm_list(input: &[u8]) -> IResult<&[u8], Vec<Flag>> {
parse_flag_list(input, true)
}
fn capability(input: &[u8]) -> IResult<&[u8], Capability> {
let (input, raw) = atom(input)?;
let s = String::from_utf8_lossy(raw);
let upper = s.to_ascii_uppercase();
let cap = match upper.as_str() {
"IMAP4REV1" => Capability::Imap4Rev1,
"IMAP4REV2" => Capability::Imap4Rev2,
"ACL" => Capability::Acl,
"BINARY" => Capability::Binary,
"CHILDREN" => Capability::Children,
"COMPRESS=DEFLATE" => Capability::CompressDeflate,
"CONDSTORE" => Capability::Condstore,
"CREATE-SPECIAL-USE" => Capability::CreateSpecialUse,
"ENABLE" => Capability::Enable,
"ESEARCH" => Capability::Esearch,
"ID" => Capability::Id,
"IDLE" => Capability::Idle,
"LIST-EXTENDED" => Capability::ListExtended,
"LIST-STATUS" => Capability::ListStatus,
"LITERAL+" => Capability::LiteralPlus,
"LITERAL-" => Capability::LiteralMinus,
"LOGINDISABLED" => Capability::LoginDisabled,
"METADATA" => Capability::Metadata,
"METADATA-SERVER" => Capability::MetadataServer,
"MOVE" => Capability::Move,
"MULTIAPPEND" => Capability::MultiAppend,
"NAMESPACE" => Capability::Namespace,
"OBJECTID" => Capability::ObjectId,
"PREVIEW" => Capability::Preview,
"QRESYNC" => Capability::QResync,
"QUOTA" => Capability::Quota,
"SASL-IR" => Capability::SaslIr,
"SAVEDATE" => Capability::SaveDate,
"SEARCHRES" => Capability::SearchRes,
"SORT" => Capability::Sort,
"SPECIAL-USE" => Capability::SpecialUse,
"STARTTLS" => Capability::StartTls,
"STATUS=SIZE" => Capability::StatusSize,
"UNAUTHENTICATE" => Capability::Unauthenticate,
"UIDPLUS" => Capability::UidPlus,
"UNSELECT" => Capability::Unselect,
"WITHIN" => Capability::Within,
"UTF8=ACCEPT" => Capability::Utf8Accept,
"UTF8=ONLY" => Capability::Utf8Only,
_ => {
if let Some(mechanism) = upper.strip_prefix("AUTH=") {
Capability::Auth(mechanism.to_owned())
} else if let Some(variant) = upper.strip_prefix("SORT=") {
Capability::SortDisplay(variant.to_owned())
} else if let Some(algo) = upper.strip_prefix("THREAD=") {
Capability::Thread(algo.to_owned())
} else if upper.starts_with("RIGHTS=") {
Capability::Rights(s["RIGHTS=".len()..].to_string())
} else if let Some(rest) = upper.strip_prefix("APPENDLIMIT") {
if let Some(val_str) = rest.strip_prefix('=') {
if let Ok(n) = val_str.parse::<u64>() {
Capability::AppendLimit(Some(n))
} else {
Capability::Other(s.into_owned())
}
} else {
Capability::AppendLimit(None)
}
} else {
Capability::Other(s.into_owned())
}
}
};
Ok((input, cap))
}
fn capability_list(input: &[u8]) -> IResult<&[u8], Vec<Capability>> {
separated_list0(char(' '), capability)(input)
}
fn uid_range(input: &[u8]) -> IResult<&[u8], UidRange> {
let (input, start) = nz_number(input)?;
let (input, end) = if input.first() == Some(&b':') {
let (input, _) = char(':')(input)?;
let (input, end) = nz_number(input)?;
(input, Some(end))
} else {
(input, None)
};
Ok((input, UidRange { start, end }))
}
fn uid_set(input: &[u8]) -> IResult<&[u8], Vec<UidRange>> {
separated_list1(char(','), uid_range)(input)
}
fn seq_number(input: &[u8]) -> IResult<&[u8], u32> {
if input.first() == Some(&b'*') {
Ok((&input[1..], u32::MAX))
} else {
nz_number(input)
}
}
fn seq_range(input: &[u8]) -> IResult<&[u8], UidRange> {
let (input, start) = seq_number(input)?;
let (input, end) = if input.first() == Some(&b':') {
let (input, _) = char(':')(input)?;
let (input, end) = seq_number(input)?;
(input, Some(end))
} else {
(input, None)
};
Ok((input, UidRange { start, end }))
}
fn sequence_set(input: &[u8]) -> IResult<&[u8], Vec<UidRange>> {
separated_list1(char(','), seq_range)(input)
}
fn response_code(input: &[u8]) -> IResult<&[u8], ResponseCode> {
let (input, _) = char('[')(input)?;
let (input, code_atom) = atom(input)?;
let code_str = String::from_utf8_lossy(code_atom);
let upper = code_str.to_ascii_uppercase();
let (input, code) = response_code_inner(input, &code_str, &upper)?;
let (input, _) = char(']')(input)?;
Ok((input, code))
}
#[allow(clippy::too_many_lines)]
fn response_code_inner<'a>(
input: &'a [u8],
code_str: &str,
upper: &str,
) -> IResult<&'a [u8], ResponseCode> {
match upper {
"ALERT" => Ok((input, ResponseCode::Alert)),
"PARSE" => Ok((input, ResponseCode::Parse)),
"READ-ONLY" => Ok((input, ResponseCode::ReadOnly)),
"READ-WRITE" => Ok((input, ResponseCode::ReadWrite)),
"TRYCREATE" => Ok((input, ResponseCode::TryCreate)),
"NOMODSEQ" => Ok((input, ResponseCode::NoModSeq)),
"CLOSED" => Ok((input, ResponseCode::Closed)),
"UNAVAILABLE" => Ok((input, ResponseCode::Unavailable)),
"AUTHENTICATIONFAILED" => Ok((input, ResponseCode::AuthenticationFailed)),
"AUTHORIZATIONFAILED" => Ok((input, ResponseCode::AuthorizationFailed)),
"EXPIRED" => Ok((input, ResponseCode::Expired)),
"PRIVACYREQUIRED" => Ok((input, ResponseCode::PrivacyRequired)),
"CONTACTADMIN" => Ok((input, ResponseCode::ContactAdmin)),
"NOPERM" => Ok((input, ResponseCode::NoPerm)),
"INUSE" => Ok((input, ResponseCode::InUse)),
"EXPUNGEISSUED" => Ok((input, ResponseCode::ExpungeIssued)),
"CORRUPTION" => Ok((input, ResponseCode::Corruption)),
"SERVERBUG" => Ok((input, ResponseCode::ServerBug)),
"CLIENTBUG" => Ok((input, ResponseCode::ClientBug)),
"CANNOT" => Ok((input, ResponseCode::Cannot)),
"LIMIT" => Ok((input, ResponseCode::Limit)),
"OVERQUOTA" => Ok((input, ResponseCode::OverQuota)),
"ALREADYEXISTS" => Ok((input, ResponseCode::AlreadyExists)),
"NONEXISTENT" => Ok((input, ResponseCode::NonExistent)),
"UIDNOTSTICKY" => Ok((input, ResponseCode::UidNotSticky)),
"NOTSAVED" => Ok((input, ResponseCode::NotSaved)),
"HASCHILDREN" => Ok((input, ResponseCode::HasChildren)),
"UNKNOWN-CTE" => Ok((input, ResponseCode::UnknownCte)),
"TOOBIG" => Ok((input, ResponseCode::TooBig)),
"COMPRESSIONACTIVE" => Ok((input, ResponseCode::CompressionActive)),
"USEATTR" => Ok((input, ResponseCode::UseAttr)),
"UIDNEXT" => {
let (input, _) = sp(input)?;
let (input, n) = nz_number(input)?;
Ok((input, ResponseCode::UidNext(n)))
}
"UIDVALIDITY" => {
let (input, _) = sp(input)?;
let (input, n) = nz_number(input)?;
Ok((input, ResponseCode::UidValidity(n)))
}
"UNSEEN" => {
let (input, _) = sp(input)?;
let (input, n) = nz_number(input)?;
Ok((input, ResponseCode::Unseen(n)))
}
"HIGHESTMODSEQ" => {
let (input, _) = sp(input)?;
let (input, n) = number64(input)?;
Ok((input, ResponseCode::HighestModSeq(n)))
}
"CAPABILITY" => {
let (input, _) = sp(input)?;
let (input, caps) = capability_list(input)?;
Ok((input, ResponseCode::Capability(caps)))
}
"PERMANENTFLAGS" => {
let (input, _) = sp(input)?;
let (input, flags) = flag_perm_list(input)?;
Ok((input, ResponseCode::PermanentFlags(flags)))
}
"BADCHARSET" => {
let (input, charsets) = opt(preceded(
sp,
delimited(char('('), separated_list0(sp, astring_utf8), char(')')),
))(input)?;
Ok((
input,
ResponseCode::BadCharset(charsets.unwrap_or_default()),
))
}
"APPENDUID" => {
let (input, _) = sp(input)?;
let (input, uid_validity) = nz_number(input)?;
let (input, _) = sp(input)?;
let (input, uids) = uid_set(input)?;
Ok((input, ResponseCode::AppendUid { uid_validity, uids }))
}
"COPYUID" => {
let (input, _) = sp(input)?;
let (input, uid_validity) = nz_number(input)?;
let (input, _) = sp(input)?;
let (input, source_uids) = uid_set(input)?;
let (input, _) = sp(input)?;
let (input, dest_uids) = uid_set(input)?;
Ok((
input,
ResponseCode::CopyUid {
uid_validity,
source_uids,
dest_uids,
},
))
}
"MODIFIED" => {
let (input, _) = sp(input)?;
let (input, uids) = sequence_set(input)?;
Ok((input, ResponseCode::Modified(uids)))
}
"MAILBOXID" => {
let (input, _) = sp(input)?;
let (input, _) = char('(')(input)?;
let (input, val) = map(objectid, |a| String::from_utf8_lossy(a).into_owned())(input)?;
let (input, _) = char(')')(input)?;
Ok((input, ResponseCode::MailboxId(val)))
}
"METADATA" => {
let (input, _) = sp(input)?;
let (input, sub_atom) = atom(input)?;
let sub = String::from_utf8_lossy(sub_atom).to_ascii_uppercase();
match sub.as_str() {
"LONGENTRIES" => {
let (input, _) = sp(input)?;
let (input, n) = number64(input)?;
Ok((input, ResponseCode::MetadataLongEntries(n)))
}
"MAXSIZE" => {
let (input, _) = sp(input)?;
let (input, n) = number64(input)?;
Ok((input, ResponseCode::MetadataMaxSize(n)))
}
"TOOMANY" => Ok((input, ResponseCode::MetadataTooMany)),
"NOPRIVATE" => Ok((input, ResponseCode::MetadataNoPrivate)),
_ => Ok((
input,
ResponseCode::Other {
name: format!("METADATA {sub}"),
value: None,
},
)),
}
}
_ => {
let (input, val) = opt(preceded(sp, take_while(|b: u8| b != b']')))(input)?;
Ok((
input,
ResponseCode::Other {
name: code_str.to_owned(),
value: val.map(|v| String::from_utf8_lossy(v).into_owned()),
},
))
}
}
}
fn resp_text(input: &[u8]) -> IResult<&[u8], (Option<ResponseCode>, String)> {
let (input, code) = opt(response_code)(input)?;
let (input, _) = opt(char(' '))(input)?;
let (input, text_bytes) = take_while(|b: u8| b != b'\r' && b != b'\n')(input)?;
let text = String::from_utf8_lossy(text_bytes).into_owned();
Ok((input, (code, text)))
}
fn parse_continuation(input: &[u8]) -> IResult<&[u8], ContinuationRequest> {
let (input, _) = tag(b"+")(input)?;
let (input, _) = opt(char(' '))(input)?;
let (input, code, data) = if input.first() == Some(&b'[') {
if let Ok((rest, (code, text))) = resp_text(input) {
(rest, code, text)
} else {
let (rest, data_bytes) = take_while(|b: u8| b != b'\r' && b != b'\n')(input)?;
(rest, None, String::from_utf8_lossy(data_bytes).into_owned())
}
} else {
let (rest, data_bytes) = take_while(|b: u8| b != b'\r' && b != b'\n')(input)?;
(rest, None, String::from_utf8_lossy(data_bytes).into_owned())
};
let (input, _) = crlf(input)?;
Ok((input, ContinuationRequest { code, data }))
}
fn parse_tagged(input: &[u8]) -> IResult<&[u8], TaggedResponse> {
let (input, tag_bytes) = tag_str(input)?;
let (input, _) = sp(input)?;
let (input, status) = alt((
value(StatusKind::Ok, tag_no_case(b"OK")),
value(StatusKind::No, tag_no_case(b"NO")),
value(StatusKind::Bad, tag_no_case(b"BAD")),
))(input)?;
let (input, maybe_sp) = opt(sp)(input)?;
if maybe_sp.is_some() {
let (input, (code, text)) = resp_text(input)?;
let (input, _) = crlf(input)?;
Ok((
input,
TaggedResponse {
tag: String::from_utf8_lossy(tag_bytes).into_owned(),
status,
code,
text,
},
))
} else {
let (input, _) = crlf(input)?;
Ok((
input,
TaggedResponse {
tag: String::from_utf8_lossy(tag_bytes).into_owned(),
status,
code: None,
text: String::new(),
},
))
}
}
fn parse_untagged(input: &[u8], utf8_mode: bool) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag(b"* ")(input)?;
alt((
alt((
parse_untagged_status,
|i| parse_untagged_numbered(i, utf8_mode),
parse_untagged_capability,
parse_untagged_flags,
parse_untagged_list,
parse_untagged_lsub,
parse_untagged_esearch,
parse_untagged_search,
parse_untagged_sort,
parse_untagged_status_mailbox,
)),
alt((
parse_untagged_enabled,
parse_untagged_vanished,
parse_untagged_id,
parse_untagged_namespace,
parse_untagged_quotaroot,
parse_untagged_quota,
parse_untagged_acl,
parse_untagged_myrights,
parse_untagged_listrights,
parse_untagged_metadata,
)),
alt((parse_untagged_thread, parse_untagged_unknown)),
))(input)
}
fn parse_untagged_status(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, status) = alt((
value(UntaggedStatus::Ok, tag_no_case(b"OK")),
value(UntaggedStatus::No, tag_no_case(b"NO")),
value(UntaggedStatus::Bad, tag_no_case(b"BAD")),
value(UntaggedStatus::Bye, tag_no_case(b"BYE")),
))(input)?;
let (input, maybe_sp) = opt(sp)(input)?;
if maybe_sp.is_some() {
let (input, (code, text)) = resp_text(input)?;
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::Status { status, code, text }))
} else {
let (input, _) = crlf(input)?;
Ok((
input,
UntaggedResponse::Status {
status,
code: None,
text: String::new(),
},
))
}
}
fn parse_untagged_numbered(input: &[u8], utf8_mode: bool) -> IResult<&[u8], UntaggedResponse> {
let num_start = input; let (input, n) = number(input)?;
let (input, _) = sp(input)?;
if input.len() >= 6 && input[..6].eq_ignore_ascii_case(b"EXISTS") {
let (input, _) = terminated(tag_no_case(b"EXISTS"), crlf)(input)?;
return Ok((input, UntaggedResponse::Exists(n)));
}
if input.len() >= 6 && input[..6].eq_ignore_ascii_case(b"RECENT") {
let (input, _) = terminated(tag_no_case(b"RECENT"), crlf)(input)?;
return Ok((input, UntaggedResponse::Recent(n)));
}
if n == 0 {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)));
}
if num_start.first() == Some(&b'0') {
return Err(nom::Err::Failure(nom::error::Error::new(
num_start,
nom::error::ErrorKind::Verify,
)));
}
if input.len() >= 7 && input[..7].eq_ignore_ascii_case(b"EXPUNGE") {
let (input, _) = terminated(tag_no_case(b"EXPUNGE"), crlf)(input)?;
return Ok((input, UntaggedResponse::Expunge(n)));
}
let (input, _) = tag_no_case(b"FETCH")(input)?;
let (input, _) = sp(input)?;
let (input, mut fr) = fetch_response_inner(input, utf8_mode)?;
let (input, _) = crlf(input)?;
fr.seq = n;
Ok((input, UntaggedResponse::Fetch(Box::new(fr))))
}
fn parse_untagged_capability(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"CAPABILITY")(input)?;
let (input, _) = sp(input)?;
let (input, caps) = capability_list(input)?;
let (input, _) = take_while(|b: u8| b == b' ')(input)?;
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::Capability(caps)))
}
fn parse_untagged_flags(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"FLAGS")(input)?;
let (input, _) = sp(input)?;
let (input, flags) = flag_list(input)?;
let (input, _) = take_while(|b: u8| b == b' ')(input)?;
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::Flags(flags)))
}
fn parse_list_extended_data(input: &[u8]) -> IResult<&[u8], (Option<String>, Vec<String>)> {
let (mut input, _) = char('(')(input)?;
let mut old_name = None;
let mut child_info = Vec::new();
loop {
let (rest, _) = take_while(|b: u8| b == b' ')(input)?;
input = rest;
if input.first() == Some(&b')') {
input = &input[1..];
break;
}
let (rest, tag_bytes) = astring_utf8(input)?;
let (rest, _) = sp(rest)?;
let tag_upper = tag_bytes.to_ascii_uppercase();
match tag_upper.as_str() {
"OLDNAME" => {
let (rest2, _) = char('(')(rest)?;
let (rest2, name) = astring_utf8(rest2)?;
let (rest2, _) = char(')')(rest2)?;
old_name = Some(name);
input = rest2;
}
"CHILDINFO" => {
let (rest2, _) = char('(')(rest)?;
let (rest2, items) = separated_list0(sp, astring_utf8)(rest2)?;
let (rest2, _) = char(')')(rest2)?;
child_info = items;
input = rest2;
}
_ => {
if rest.first() == Some(&b'(') {
let (rest2, ()) = skip_parenthesized_block(rest)?;
input = rest2;
} else {
let (rest2, _) = take_while1(|b: u8| b != b' ' && b != b')')(rest)?;
input = rest2;
}
}
}
}
Ok((input, (old_name, child_info)))
}
fn parse_mailbox_list_response<'a, F>(
input: &'a [u8],
keyword: &'static [u8],
wrap: F,
) -> IResult<&'a [u8], UntaggedResponse>
where
F: FnOnce(MailboxInfo) -> UntaggedResponse,
{
let (input, _) = tag_no_case(keyword)(input)?;
let (input, _) = sp(input)?;
let (input, attrs) = delimited(
char('('),
separated_list0(
char(' '),
map(
alt((
map(tuple((char('\\'), atom)), |(_, a)| {
format!("\\{}", String::from_utf8_lossy(a))
}),
map(atom, |a| String::from_utf8_lossy(a).into_owned()),
)),
|s| parse_mailbox_attribute(&s),
),
),
char(')'),
)(input)?;
let (input, _) = sp(input)?;
let (input, delimiter) = mailbox_delimiter(input)?;
let (input, _) = sp(input)?;
let (input, name) = astring_utf8(input)?;
let (input, ext_data) = opt(preceded(sp, parse_list_extended_data))(input)?;
let (old_name, child_info) = ext_data.unwrap_or_default();
let (input, _) = take_while(|b: u8| b == b' ')(input)?;
let (input, _) = crlf(input)?;
Ok((
input,
wrap(MailboxInfo {
name,
delimiter,
attributes: attrs,
old_name,
child_info,
}),
))
}
fn parse_untagged_list(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
parse_mailbox_list_response(input, b"LIST", UntaggedResponse::List)
}
fn parse_untagged_lsub(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
parse_mailbox_list_response(input, b"LSUB", UntaggedResponse::Lsub)
}
fn mailbox_delimiter(input: &[u8]) -> IResult<&[u8], Option<char>> {
let (rest, delimiter_bytes) =
alt((value(None, tag_no_case(b"NIL")), map(quoted_string, Some)))(input)?;
match delimiter_bytes {
None => Ok((rest, None)),
Some(v) if v.is_empty() => Ok((rest, None)),
Some(v) => {
let s = String::from_utf8_lossy(&v);
let mut chars = s.chars();
let ch = chars.next();
if chars.next().is_some() {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)));
}
Ok((rest, ch))
}
}
}
fn parse_mailbox_attribute(s: &str) -> MailboxAttribute {
let lower = s.to_ascii_lowercase();
match lower.as_str() {
"\\noselect" | "\\nonexistent" => {
if lower == "\\nonexistent" {
MailboxAttribute::NonExistent
} else {
MailboxAttribute::NoSelect
}
}
"\\noinferiors" => MailboxAttribute::NoInferiors,
"\\haschildren" => MailboxAttribute::HasChildren,
"\\hasnochildren" => MailboxAttribute::HasNoChildren,
"\\marked" => MailboxAttribute::Marked,
"\\unmarked" => MailboxAttribute::Unmarked,
"\\subscribed" => MailboxAttribute::Subscribed,
"\\remote" => MailboxAttribute::Remote,
"\\all" => MailboxAttribute::All,
"\\archive" => MailboxAttribute::Archive,
"\\drafts" => MailboxAttribute::Drafts,
"\\flagged" => MailboxAttribute::Flagged,
"\\junk" => MailboxAttribute::Junk,
"\\sent" => MailboxAttribute::Sent,
"\\trash" => MailboxAttribute::Trash,
"\\important" => MailboxAttribute::Important,
"\\memos" => MailboxAttribute::Memos,
"\\scheduled" => MailboxAttribute::Scheduled,
"\\snoozed" => MailboxAttribute::Snoozed,
_ => MailboxAttribute::Custom(s.to_owned()),
}
}
fn parse_untagged_search(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, (nums, mod_seq)) = parse_number_list_with_modseq(input, b"SEARCH")?;
Ok((
input,
UntaggedResponse::Search {
uids: nums,
mod_seq,
},
))
}
fn parse_untagged_sort(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, (nums, mod_seq)) = parse_number_list_with_modseq(input, b"SORT")?;
Ok((input, UntaggedResponse::Sort { nums, mod_seq }))
}
fn parse_number_list_with_modseq<'a>(
input: &'a [u8],
keyword: &'static [u8],
) -> IResult<&'a [u8], (Vec<u32>, Option<u64>)> {
let (input, _) = tag_no_case(keyword)(input)?;
let (input, nums) = many0(preceded(sp, nz_number))(input)?;
let (input, mod_seq) = parse_optional_modseq_and_crlf(input)?;
Ok((input, (nums, mod_seq)))
}
fn parse_optional_modseq_and_crlf(input: &[u8]) -> IResult<&[u8], Option<u64>> {
let (input, mod_seq) = opt(preceded(
sp,
delimited(
tuple((char('('), tag_no_case(b"MODSEQ"), sp)),
nz_number64,
char(')'),
),
))(input)?;
let (input, _) = take_while(|b: u8| b != b'\r' && b != b'\n')(input)?;
let (input, _) = crlf(input)?;
Ok((input, mod_seq))
}
fn parse_untagged_esearch(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"ESEARCH")(input)?;
let (input, tag_val) = opt(preceded(
sp,
delimited(
char('('),
preceded(tuple((tag_no_case(b"TAG"), sp)), astring),
char(')'),
),
))(input)?;
let (input, uid_indicator) = opt(preceded(sp, tag_no_case(b"UID")))(input)?;
let mut esearch = EsearchResponse {
tag: tag_val.map(|v| String::from_utf8_lossy(&v).into_owned()),
uid: uid_indicator.is_some(),
..EsearchResponse::default()
};
let mut input = input;
while let Ok((rest, _)) = sp(input) {
if rest.first() == Some(&b'\r') {
break;
}
let (rest, key) = atom(rest)?;
let key_upper = String::from_utf8_lossy(key).to_ascii_uppercase();
match key_upper.as_str() {
"ALL" => {
let (rest2, _) = sp(rest)?;
let (rest2, set) = sequence_set(rest2)?;
esearch.all = set;
input = rest2;
}
"MIN" => {
let (rest2, _) = sp(rest)?;
let (rest2, val) = nz_number(rest2)?;
esearch.min = Some(val);
input = rest2;
}
"MAX" => {
let (rest2, _) = sp(rest)?;
let (rest2, val) = nz_number(rest2)?;
esearch.max = Some(val);
input = rest2;
}
"COUNT" => {
let (rest2, _) = sp(rest)?;
let (rest2, val) = number(rest2)?;
esearch.count = Some(val);
input = rest2;
}
"MODSEQ" => {
let (rest2, _) = sp(rest)?;
if let Ok((rest2, val)) = nz_number64(rest2) {
esearch.mod_seq = Some(val);
input = rest2;
} else {
let (rest2, _) = take_while(|b: u8| b != b' ' && b != b'\r')(rest2)?;
input = rest2;
}
}
_ => {
let (rest2, _) = sp(rest)?;
if rest2.first() == Some(&b'(') {
let (rest2, ()) = skip_parenthesized_block(rest2)?;
input = rest2;
} else {
let (rest2, ()) = skip_tagged_ext_simple(|b| b == b' ' || b == b'\r')(rest2)?;
input = rest2;
}
}
}
}
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::Esearch(esearch)))
}
fn parse_untagged_status_mailbox(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"STATUS")(input)?;
let (input, _) = sp(input)?;
let (input, mailbox) = astring_utf8(input)?;
let (input, _) = sp(input)?;
let (input, items) = delimited(char('('), status_items, char(')'))(input)?;
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::MailboxStatus { mailbox, items }))
}
fn status_items(input: &[u8]) -> IResult<&[u8], Vec<StatusItem>> {
let mut items = Vec::new();
let mut input_loop = input;
loop {
let (rest, _) = take_while(|b: u8| b == b' ')(input_loop)?;
input_loop = rest;
if input_loop.first() == Some(&b')') || input_loop.is_empty() {
break;
}
let (rest, name) = atom(input_loop)?;
let upper = String::from_utf8_lossy(name).to_ascii_uppercase();
match upper.as_str() {
"MESSAGES" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number(rest)?;
items.push(StatusItem::Messages(val));
input_loop = rest;
}
"RECENT" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number(rest)?;
items.push(StatusItem::Recent(val));
input_loop = rest;
}
"UNSEEN" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number(rest)?;
items.push(StatusItem::Unseen(val));
input_loop = rest;
}
"UIDNEXT" => {
let (rest, _) = sp(rest)?;
let (rest, val) = nz_number(rest)?;
items.push(StatusItem::UidNext(val));
input_loop = rest;
}
"UIDVALIDITY" => {
let (rest, _) = sp(rest)?;
let (rest, val) = nz_number(rest)?;
items.push(StatusItem::UidValidity(val));
input_loop = rest;
}
"DELETED" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number(rest)?;
items.push(StatusItem::Deleted(val));
input_loop = rest;
}
"HIGHESTMODSEQ" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number64(rest)?;
items.push(StatusItem::HighestModSeq(val));
input_loop = rest;
}
"SIZE" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number64(rest)?;
items.push(StatusItem::Size(val));
input_loop = rest;
}
"MAILBOXID" => {
let (rest, _) = sp(rest)?;
let (rest, _) = char('(')(rest)?;
let (rest, val) = map(objectid, |a| String::from_utf8_lossy(a).into_owned())(rest)?;
let (rest, _) = char(')')(rest)?;
items.push(StatusItem::MailboxId(val));
input_loop = rest;
}
"APPENDLIMIT" => {
let (rest, _) = sp(rest)?;
let (rest, val) =
alt((map(tag_no_case(b"NIL"), |_| None), map(number64, Some)))(rest)?;
items.push(StatusItem::AppendLimit(val));
input_loop = rest;
}
"DELETED-STORAGE" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number64(rest)?;
items.push(StatusItem::DeletedStorage(val));
input_loop = rest;
}
_ => {
let (rest, _) = sp(rest)?;
if rest.first() == Some(&b'(') {
let (rest, ()) = skip_parenthesized_block(rest)?;
input_loop = rest;
} else {
let (rest, ()) = skip_tagged_ext_simple(|b| b == b' ' || b == b')')(rest)?;
input_loop = rest;
}
}
}
}
Ok((input_loop, items))
}
fn parse_untagged_enabled(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"ENABLED")(input)?;
let (input, caps) = many0(preceded(
sp,
map(atom, |a| String::from_utf8_lossy(a).into_owned()),
))(input)?;
let (input, _) = take_while(|b: u8| b == b' ')(input)?;
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::Enabled(caps)))
}
fn parse_untagged_vanished(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"VANISHED")(input)?;
let (input, _) = sp(input)?;
let (input, earlier) = opt(delimited(char('('), tag_no_case(b"EARLIER"), char(')')))(input)?;
let (input, _) = if earlier.is_some() {
sp(input)?
} else {
(input, b' ')
};
let (input, uids) = uid_set(input)?;
let (input, _) = take_while(|b: u8| b == b' ')(input)?;
let (input, _) = crlf(input)?;
Ok((
input,
UntaggedResponse::Vanished {
earlier: earlier.is_some(),
uids,
},
))
}
fn parse_untagged_id(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"ID")(input)?;
let (input, _) = sp(input)?;
let (input, params) = alt((
value(vec![], tag_no_case(b"NIL")),
delimited(
char('('),
many0(map(
tuple((
preceded(take_while(|b: u8| b == b' '), string_utf8),
preceded(sp, nstring_utf8),
)),
|(k, v)| (k, v),
)),
char(')'),
),
))(input)?;
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::Id(params)))
}
fn parse_untagged_namespace(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"NAMESPACE")(input)?;
let (input, _) = sp(input)?;
let (input, personal) = namespace_list(input)?;
let (input, _) = sp(input)?;
let (input, other) = namespace_list(input)?;
let (input, _) = sp(input)?;
let (input, shared) = namespace_list(input)?;
let (input, _) = crlf(input)?;
Ok((
input,
UntaggedResponse::Namespace {
personal,
other,
shared,
},
))
}
fn namespace_list(input: &[u8]) -> IResult<&[u8], Vec<NamespaceDescriptor>> {
alt((
value(vec![], tag_no_case(b"NIL")),
delimited(char('('), many0(namespace_descriptor), char(')')),
))(input)
}
fn namespace_descriptor(input: &[u8]) -> IResult<&[u8], NamespaceDescriptor> {
let (input, _) = char('(')(input)?;
let (input, prefix) = string_utf8(input)?;
let (input, _) = sp(input)?;
let (input, delim_bytes) =
alt((value(None, tag_no_case(b"NIL")), map(quoted_string, Some)))(input)?;
let delim = match delim_bytes {
None => None,
Some(v) if v.is_empty() => None,
Some(v) => {
let s = String::from_utf8_lossy(&v);
let mut chars = s.chars();
let ch = chars.next();
if chars.next().is_some() {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)));
}
ch
}
};
let (input, extensions) = many0(namespace_response_extension)(input)?;
let (input, _) = char(')')(input)?;
Ok((
input,
NamespaceDescriptor {
prefix,
delimiter: delim,
extensions,
},
))
}
fn namespace_response_extension(input: &[u8]) -> IResult<&[u8], (String, Vec<String>)> {
let (input, _) = sp(input)?;
let (input, key) = string_utf8(input)?;
let (input, _) = sp(input)?;
let (input, _) = char('(')(input)?;
let (input, first_val) = string_utf8(input)?;
let (input, rest_vals) = many0(preceded(sp, string_utf8))(input)?;
let (input, _) = char(')')(input)?;
let mut values = Vec::with_capacity(1 + rest_vals.len());
values.push(first_val);
values.extend(rest_vals);
Ok((input, (key, values)))
}
fn parse_untagged_quota(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"QUOTA ")(input)?;
let (input, root) = astring_utf8(input)?;
let (input, _) = sp(input)?;
let (input, resources) = parse_quota_resource_list(input)?;
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::Quota { root, resources }))
}
fn parse_quota_resource_list(input: &[u8]) -> IResult<&[u8], Vec<QuotaResource>> {
let (input, _) = char('(')(input)?;
let mut resources = Vec::new();
let mut input = input;
loop {
let (rest, _) = take_while(|b: u8| b == b' ')(input)?;
input = rest;
if input.first() == Some(&b')') {
input = &input[1..];
break;
}
let (rest, name_bytes) = atom(input)?;
let name = String::from_utf8_lossy(name_bytes).into_owned();
let (rest, _) = sp(rest)?;
let (rest, usage) = number64(rest)?;
let (rest, _) = sp(rest)?;
let (rest, limit) = number64(rest)?;
resources.push(QuotaResource { name, usage, limit });
input = rest;
}
Ok((input, resources))
}
fn parse_untagged_quotaroot(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"QUOTAROOT ")(input)?;
let (input, mailbox) = astring_utf8(input)?;
let mut roots = Vec::new();
let mut input = input;
loop {
let Ok((rest, _)) = sp(input) else {
break;
};
if rest.starts_with(b"\r\n") {
break;
}
let Ok((rest, root)) = astring_utf8(rest) else {
break;
};
roots.push(root);
input = rest;
}
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::QuotaRoot { mailbox, roots }))
}
fn parse_untagged_acl(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"ACL ")(input)?;
let (input, mailbox) = astring_utf8(input)?;
let mut entries = Vec::new();
let mut input = input;
loop {
let Ok((rest, _)) = sp(input) else {
break;
};
if rest.starts_with(b"\r\n") {
break;
}
let Ok((rest, identifier)) = astring_utf8(rest) else {
break;
};
let Ok((rest, _)) = sp(rest) else {
break;
};
let Ok((rest, rights)) = astring_utf8(rest) else {
break;
};
entries.push(AclEntry { identifier, rights });
input = rest;
}
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::Acl { mailbox, entries }))
}
fn parse_untagged_myrights(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"MYRIGHTS ")(input)?;
let (input, mailbox) = astring_utf8(input)?;
let (input, _) = sp(input)?;
let (input, rights) = astring_utf8(input)?;
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::MyRights { mailbox, rights }))
}
fn parse_untagged_listrights(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"LISTRIGHTS ")(input)?;
let (input, mailbox) = astring_utf8(input)?;
let (input, _) = sp(input)?;
let (input, identifier) = astring_utf8(input)?;
let (input, _) = sp(input)?;
let (input, required) = astring_utf8(input)?;
let mut optional = Vec::new();
let mut input = input;
loop {
let Ok((rest, _)) = sp(input) else {
break;
};
if rest.starts_with(b"\r\n") {
break;
}
let Ok((rest, rights_group)) = astring_utf8(rest) else {
break;
};
optional.push(rights_group);
input = rest;
}
let (input, _) = crlf(input)?;
Ok((
input,
UntaggedResponse::ListRights {
mailbox,
identifier,
required,
optional,
},
))
}
fn parse_untagged_metadata(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"METADATA ")(input)?;
let (input, mailbox) = astring_utf8(input)?;
let (input, _) = sp(input)?;
let (input, entries) = if input.first() == Some(&b'(') {
parse_metadata_entry_list(input)?
} else {
let mut entries = Vec::new();
let (rest, first_name) = astring_utf8(input)?;
entries.push(MetadataEntry {
name: first_name,
value: None,
});
let mut input = rest;
loop {
if input.starts_with(b"\r\n") || input.is_empty() {
break;
}
let Ok((rest, _)) = sp(input) else {
break;
};
if rest.starts_with(b"\r\n") {
break;
}
let Ok((rest, name)) = astring_utf8(rest) else {
break;
};
entries.push(MetadataEntry { name, value: None });
input = rest;
}
(input, entries)
};
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::Metadata { mailbox, entries }))
}
fn parse_metadata_entry_list(input: &[u8]) -> IResult<&[u8], Vec<MetadataEntry>> {
let (input, _) = char('(')(input)?;
let mut entries = Vec::new();
let mut input = input;
loop {
let (rest, _) = take_while(|b: u8| b == b' ')(input)?;
input = rest;
if input.first() == Some(&b')') {
input = &input[1..];
break;
}
let (rest, name) = astring_utf8(input)?;
let (rest, _) = sp(rest)?;
let (rest, value) = nstring(rest)?;
entries.push(MetadataEntry { name, value });
input = rest;
}
Ok((input, entries))
}
const MAX_THREAD_NESTING_DEPTH: u32 = 64;
fn parse_untagged_thread(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"THREAD")(input)?;
let mut threads = Vec::new();
let mut input = input;
loop {
let (rest, _) = take_while(|b: u8| b == b' ')(input)?;
input = rest;
if input.starts_with(b"\r\n") || input.is_empty() {
break;
}
if input.first() != Some(&b'(') {
break;
}
let (rest, node) = parse_thread_node(input, 0)?;
threads.push(node);
input = rest;
}
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::Thread(threads)))
}
fn parse_untagged_unknown(input: &[u8]) -> IResult<&[u8], UntaggedResponse> {
let (input, raw) = scan_unknown_response(input)?;
let (input, _) = crlf(input)?;
Ok((
input,
UntaggedResponse::Unknown(String::from_utf8_lossy(raw).into_owned()),
))
}
fn scan_unknown_response(input: &[u8]) -> IResult<&[u8], &[u8]> {
let start = input;
let mut pos = 0;
while pos < input.len() {
let b = input[pos];
if b == b'\r' || b == b'\n' {
return Ok((&input[pos..], &start[..pos]));
}
if b == b'"' {
pos += 1; while pos < input.len() {
match input[pos] {
b'\\' => {
pos += 2;
}
b'"' => {
pos += 1; break;
}
b'\r' | b'\n' => break,
_ => {
pos += 1;
}
}
}
continue;
}
if b == b'~' && pos + 1 < input.len() && input[pos + 1] == b'{' {
if let Some(advance) = try_skip_literal(&input[pos + 1..]) {
pos += 1 + advance; continue;
}
}
if b == b'{' {
if let Some(advance) = try_skip_literal(&input[pos..]) {
pos += advance;
continue;
}
}
pos += 1;
}
Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::CrLf,
)))
}
fn try_skip_literal(input: &[u8]) -> Option<usize> {
if input.is_empty() || input[0] != b'{' {
return None;
}
let mut pos = 1;
let digit_start = pos;
while pos < input.len() && input[pos].is_ascii_digit() {
pos += 1;
}
let digit_end = pos;
if digit_end == digit_start {
return None;
}
if pos < input.len() && input[pos] == b'+' {
pos += 1;
}
if pos + 2 >= input.len()
|| input[pos] != b'}'
|| input[pos + 1] != b'\r'
|| input[pos + 2] != b'\n'
{
return None;
}
pos += 3;
let count_str = std::str::from_utf8(&input[digit_start..digit_end]).ok()?;
let count: usize = count_str.parse().ok()?;
let total = pos.checked_add(count)?;
if total > input.len() {
return None;
}
Some(total)
}
fn parse_thread_node(input: &[u8], depth: u32) -> IResult<&[u8], ThreadNode> {
if depth > MAX_THREAD_NESTING_DEPTH {
return Err(nom::Err::Failure(nom::error::Error::new(
input,
nom::error::ErrorKind::TooLarge,
)));
}
let (input, _) = char('(')(input)?;
let mut input = input;
let root_id: Option<u32> = if input.first().map_or(true, |b| *b == b'(' || *b == b')') {
None
} else {
let (rest, uid) = nz_number(input)?;
input = rest;
Some(uid)
};
let mut chain_uids: Vec<Option<u32>> = Vec::new();
let mut branch_groups: Vec<Vec<ThreadNode>> = Vec::new();
loop {
let (rest, _) = take_while(|b: u8| b == b' ')(input)?;
input = rest;
if input.first() == Some(&b')') {
input = &input[1..];
break;
}
if input.first() == Some(&b'(') {
let (rest, child) = parse_thread_node(input, depth + 1)?;
if let Some(last) = branch_groups.last_mut() {
last.push(child);
} else {
branch_groups.push(vec![child]);
}
input = rest;
} else {
let (rest, uid) = nz_number(input)?;
chain_uids.push(Some(uid));
branch_groups.push(Vec::new());
input = rest;
}
}
let children = build_thread_tree(&chain_uids, &branch_groups);
let mut node = ThreadNode {
id: root_id,
children,
};
if node.id.is_none() && node.children.len() == 1 {
if let Some(child) = node.children.pop() {
node = child;
}
}
Ok((input, node))
}
fn build_thread_tree(
chain_uids: &[Option<u32>],
branch_groups: &[Vec<ThreadNode>],
) -> Vec<ThreadNode> {
if chain_uids.is_empty() {
return branch_groups.iter().flatten().cloned().collect();
}
let last = chain_uids.len() - 1;
let last_children: Vec<ThreadNode> = if last < branch_groups.len() {
branch_groups[last].clone()
} else {
vec![]
};
let mut result = ThreadNode {
id: chain_uids[last],
children: last_children,
};
for i in (0..last).rev() {
let mut children: Vec<ThreadNode> = if i < branch_groups.len() {
branch_groups[i].clone()
} else {
vec![]
};
children.push(result);
result = ThreadNode {
id: chain_uids[i],
children,
};
}
vec![result]
}
pub(crate) fn decode_rfc2047(input: &[u8]) -> String {
let s = String::from_utf8_lossy(input);
decode_rfc2047_str(&s)
}
fn decode_rfc2047_str(input: &str) -> String {
let mut result = String::new();
let mut remaining = input;
let mut last_was_encoded = false;
while let Some(start) = remaining.find("=?") {
let before = &remaining[..start];
if !last_was_encoded || !before.chars().all(|c| c == ' ' || c == '\t') {
result.push_str(before);
}
remaining = &remaining[start + 2..];
if let Some(decoded) = parse_encoded_word(&mut remaining) {
result.push_str(&decoded);
last_was_encoded = true;
} else {
result.push_str("=?");
last_was_encoded = false;
}
}
result.push_str(remaining);
result
}
fn parse_encoded_word(remaining: &mut &str) -> Option<String> {
let saved = *remaining;
let result = parse_encoded_word_inner(remaining);
if result.is_none() {
*remaining = saved;
}
result
}
fn parse_encoded_word_inner(remaining: &mut &str) -> Option<String> {
let q1 = remaining.find('?')?;
let charset_raw = &remaining[..q1];
let charset = match charset_raw.find('*') {
Some(pos) => &charset_raw[..pos],
None => charset_raw,
};
*remaining = &remaining[q1 + 1..];
let q2 = remaining.find('?')?;
let encoding = &remaining[..q2];
*remaining = &remaining[q2 + 1..];
let end = remaining.find("?=")?;
let encoded_text = &remaining[..end];
*remaining = &remaining[end + 2..];
let raw_bytes = match encoding.to_ascii_uppercase().as_str() {
"B" => {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(encoded_text)
.ok()?
}
"Q" => decode_q_encoding(encoded_text),
_ => return None,
};
let charset_upper = charset.to_ascii_uppercase();
if charset_upper == "UTF-8" || charset_upper == "US-ASCII" || charset_upper == "ASCII" {
Some(String::from_utf8_lossy(&raw_bytes).into_owned())
} else {
let encoding = encoding_rs::Encoding::for_label(charset.as_bytes())?;
let (cow, _) = encoding.decode_without_bom_handling(&raw_bytes);
Some(cow.into_owned())
}
}
fn decode_q_encoding(input: &str) -> Vec<u8> {
let mut result = Vec::new();
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'=' if i + 1 < bytes.len() => {
if bytes[i + 1] == b'\r' && i + 2 < bytes.len() && bytes[i + 2] == b'\n' {
i += 3;
} else if bytes[i + 1] == b'\n' {
i += 2;
} else if i + 2 < bytes.len() {
if let (Some(hi), Some(lo)) = (hex_digit(bytes[i + 1]), hex_digit(bytes[i + 2]))
{
result.push(hi << 4 | lo);
i += 3;
} else {
result.push(b'=');
i += 1;
}
} else {
result.push(b'=');
i += 1;
}
}
b'_' => {
result.push(b' ');
i += 1;
}
b => {
result.push(b);
i += 1;
}
}
}
result
}
fn hex_digit(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'A'..=b'F' => Some(b - b'A' + 10),
b'a'..=b'f' => Some(b - b'a' + 10),
_ => None,
}
}
fn address(input: &[u8], utf8_mode: bool) -> IResult<&[u8], EnvelopeAddress> {
let (input, _) = char('(')(input)?;
let (input, name_raw) = nstring(input)?;
let (input, _) = sp(input)?;
let (input, adl) = nstring_utf8(input)?;
let (input, _) = sp(input)?;
let (input, mailbox) = nstring_utf8(input)?;
let (input, _) = sp(input)?;
let (input, host) = nstring_utf8(input)?;
let (input, _) = char(')')(input)?;
let name = name_raw.map(|v| {
if utf8_mode {
String::from_utf8_lossy(&v).into_owned()
} else {
decode_rfc2047(&v)
}
});
Ok((
input,
EnvelopeAddress {
name,
adl,
mailbox,
host,
},
))
}
fn address_list(input: &[u8], utf8_mode: bool) -> IResult<&[u8], Vec<EnvelopeAddress>> {
alt((
value(vec![], tag_no_case(b"NIL")),
delimited(char('('), many0(|i| address(i, utf8_mode)), char(')')),
))(input)
}
fn envelope(input: &[u8], utf8_mode: bool) -> IResult<&[u8], Envelope> {
let (input, _) = char('(')(input)?;
let (input, date) = nstring_utf8(input)?;
let (input, _) = sp(input)?;
let (input, subject_raw) = nstring(input)?;
let subject = subject_raw.map(|v| {
if utf8_mode {
String::from_utf8_lossy(&v).into_owned()
} else {
decode_rfc2047(&v)
}
});
let (input, _) = sp(input)?;
let (input, from) = address_list(input, utf8_mode)?;
let (input, _) = sp(input)?;
let (input, sender) = address_list(input, utf8_mode)?;
let (input, _) = sp(input)?;
let (input, reply_to) = address_list(input, utf8_mode)?;
let (input, _) = sp(input)?;
let (input, to) = address_list(input, utf8_mode)?;
let (input, _) = sp(input)?;
let (input, cc) = address_list(input, utf8_mode)?;
let (input, _) = sp(input)?;
let (input, bcc) = address_list(input, utf8_mode)?;
let (input, _) = sp(input)?;
let (input, in_reply_to) = nstring_utf8(input)?;
let (input, _) = sp(input)?;
let (input, message_id) = nstring_utf8(input)?;
let (input, _) = char(')')(input)?;
Ok((
input,
Envelope {
date,
subject,
from,
sender,
reply_to,
to,
cc,
bcc,
in_reply_to,
message_id,
},
))
}
#[allow(clippy::too_many_lines)]
fn fetch_response_inner(input: &[u8], utf8_mode: bool) -> IResult<&[u8], FetchResponse> {
let (input, _) = char('(')(input)?;
let mut fr = FetchResponse::default();
let mut input = input;
loop {
let (rest, _) = take_while(|b: u8| b == b' ')(input)?;
input = rest;
if input.first() == Some(&b')') {
input = &input[1..];
break;
}
let (rest, attr_name) = fetch_attr_atom(input)?;
let upper = String::from_utf8_lossy(attr_name).to_ascii_uppercase();
input = rest;
match upper.as_str() {
"UID" => {
let (rest, _) = sp(input)?;
let (rest, n) = nz_number(rest)?;
fr.uid = Some(n);
input = rest;
}
"FLAGS" => {
let (rest, _) = sp(input)?;
let (rest, flags) = flag_list(rest)?;
fr.flags = Some(flags);
input = rest;
}
"ENVELOPE" => {
let (rest, _) = sp(input)?;
let (rest, env) = envelope(rest, utf8_mode)?;
fr.envelope = Some(env);
input = rest;
}
"BODYSTRUCTURE" => {
let (rest, _) = sp(input)?;
let (rest, bs) = body_structure(rest, utf8_mode, 0)?;
fr.body_structure = Some(bs);
input = rest;
}
"RFC822.SIZE" => {
let (rest, _) = sp(input)?;
let (rest, n) = number64(rest)?;
fr.rfc822_size = Some(n);
input = rest;
}
"RFC822" => {
let (rest, _) = sp(input)?;
let (rest, data) = nstring(rest)?;
fr.body_sections.push(BodySection {
section: String::new(),
origin: None,
data,
});
input = rest;
}
"RFC822.HEADER" => {
let (rest, _) = sp(input)?;
let (rest, data) = nstring(rest)?;
fr.body_sections.push(BodySection {
section: "HEADER".to_owned(),
origin: None,
data,
});
input = rest;
}
"RFC822.TEXT" => {
let (rest, _) = sp(input)?;
let (rest, data) = nstring(rest)?;
fr.body_sections.push(BodySection {
section: "TEXT".to_owned(),
origin: None,
data,
});
input = rest;
}
"INTERNALDATE" => {
let (rest, _) = sp(input)?;
let (rest, date_val) = nstring_utf8(rest)?;
fr.internal_date = date_val;
input = rest;
}
"MODSEQ" => {
let (rest, _) = sp(input)?;
let (rest, _) = char('(')(rest)?;
if let Ok((rest, n)) = nz_number64(rest) {
let (rest, _) = char(')')(rest)?;
fr.mod_seq = Some(n);
input = rest;
} else {
let (rest, _) = take_while(|b: u8| b != b')')(rest)?;
let (rest, _) = char(')')(rest)?;
input = rest;
}
}
"SAVEDATE" => {
let (rest, _) = sp(input)?;
let (rest, val) = nstring_utf8(rest)?;
fr.save_date = val;
input = rest;
}
"PREVIEW" => {
let (rest, _) = sp(input)?;
let (rest, val) = nstring_utf8(rest)?;
fr.preview = val;
input = rest;
}
"EMAILID" => {
let (rest, _) = sp(input)?;
let (rest, _) = char('(')(rest)?;
let (rest, val) = map(objectid, |a| String::from_utf8_lossy(a).into_owned())(rest)?;
let (rest, _) = char(')')(rest)?;
fr.email_id = Some(val);
input = rest;
}
"THREADID" => {
let (rest, _) = sp(input)?;
let (rest, nil_match) = opt(terminated(
tag_no_case(b"NIL"),
peek(alt((
value((), char(' ')),
value((), char(')')),
value((), char('\r')),
))),
))(rest)?;
if nil_match.is_some() {
fr.thread_id = None;
input = rest;
} else {
let (rest, _) = char('(')(rest)?;
let (rest, val) =
map(objectid, |a| String::from_utf8_lossy(a).into_owned())(rest)?;
let (rest, _) = char(')')(rest)?;
fr.thread_id = Some(val);
input = rest;
}
}
_ if upper == "BINARY.SIZE" && input.first() == Some(&b'[') => {
let (rest, section_parts) = binary_section_spec(input)?;
let (rest, _) = sp(rest)?;
let (rest, size) = number64(rest)?;
fr.binary_sizes.push((section_parts, size));
input = rest;
}
_ if upper == "BINARY" && input.first() == Some(&b'[') => {
let (rest, (section_parts, origin)) = binary_section_with_origin(input)?;
let (rest, _) = sp(rest)?;
let (rest, data) = nstring(rest)?;
fr.binary_sections.push(BinarySection {
section: section_parts,
origin,
data,
});
input = rest;
}
_ if upper == "BODY" && input.first() == Some(&b'[') => {
let (rest, section) = body_section_spec(input)?;
let (rest, _) = sp(rest)?;
let (rest, data) = nstring(rest)?;
fr.body_sections.push(BodySection {
section: section.0,
origin: section.1,
data,
});
input = rest;
}
_ if upper == "BODY" => {
let (rest, _) = sp(input)?;
let (rest, bs) = body_structure(rest, utf8_mode, 0)?;
fr.body_structure = Some(bs);
input = rest;
}
_ => {
let (rest, _) = sp(input)?;
let (rest, ()) = skip_fetch_value(rest)?;
input = rest;
}
}
}
Ok((input, fr))
}
fn body_section_spec(input: &[u8]) -> IResult<&[u8], (String, Option<u64>)> {
let (input, _) = char('[')(input)?;
let (input, section_bytes) = scan_section_spec(input)?;
let section = String::from_utf8_lossy(section_bytes).into_owned();
let (input, _) = char(']')(input)?;
let (input, origin) = opt(delimited(char('<'), number64, char('>')))(input)?;
Ok((input, (section, origin)))
}
fn scan_section_spec(input: &[u8]) -> IResult<&[u8], &[u8]> {
let start = input;
let mut pos = 0;
let mut paren_depth: u32 = 0;
while pos < input.len() {
match input[pos] {
b']' if paren_depth == 0 => {
return Ok((&input[pos..], &start[..pos]));
}
b'(' => {
paren_depth += 1;
pos += 1;
}
b')' if paren_depth > 0 => {
paren_depth -= 1;
pos += 1;
}
b'"' => {
pos += 1; while pos < input.len() && input[pos] != b'"' {
if input[pos] == b'\\' && pos + 1 < input.len() {
pos += 2; } else {
pos += 1;
}
}
if pos < input.len() {
pos += 1; }
}
b'~' if pos + 1 < input.len() && input[pos + 1] == b'{' => {
pos += 1; }
b'{' => {
pos += 1; let digit_start = pos;
while pos < input.len() && input[pos].is_ascii_digit() {
pos += 1;
}
if pos > digit_start && pos < input.len() {
let count_end = pos;
if input[pos] == b'+' {
pos += 1;
}
if pos < input.len() && input[pos] == b'}' {
pos += 1; if pos + 1 < input.len() && input[pos] == b'\r' && input[pos + 1] == b'\n' {
pos += 2;
if let Ok(s) = std::str::from_utf8(&input[digit_start..count_end]) {
if let Ok(count) = s.parse::<usize>() {
if let Some(new_pos) = pos.checked_add(count) {
if new_pos <= input.len() {
pos = new_pos;
}
}
}
}
}
}
}
}
_ => {
pos += 1;
}
}
}
Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Char,
)))
}
fn binary_section_spec(input: &[u8]) -> IResult<&[u8], Vec<u32>> {
let (input, _) = char('[')(input)?;
let (input, parts) = separated_list0(char('.'), nz_number)(input)?;
let (input, _) = char(']')(input)?;
Ok((input, parts))
}
fn binary_section_with_origin(input: &[u8]) -> IResult<&[u8], (Vec<u32>, Option<u64>)> {
let (input, parts) = binary_section_spec(input)?;
let (input, origin) = opt(delimited(char('<'), number64, char('>')))(input)?;
Ok((input, (parts, origin)))
}
fn skip_tagged_ext_simple<F>(terminator: F) -> impl FnMut(&[u8]) -> IResult<&[u8], ()>
where
F: Fn(u8) -> bool + Copy,
{
move |input: &[u8]| {
alt((
map(tag_no_case(b"NIL"), |_| ()),
map(literal, |_| ()),
map(quoted_string, |_| ()),
map(take_while1(move |b: u8| !terminator(b)), |_| ()),
))(input)
}
}
fn skip_fetch_value(input: &[u8]) -> IResult<&[u8], ()> {
alt((
map(skip_paren_group, |_| ()),
map(tag_no_case(b"NIL"), |_| ()),
map(literal, |_| ()),
map(quoted_string, |_| ()),
map(
take_while1(|b: u8| b != b' ' && b != b')' && b != b'\r'),
|_| (),
),
))(input)
}
fn skip_paren_group(input: &[u8]) -> IResult<&[u8], &[u8]> {
let (input, _) = char('(')(input)?;
let mut depth: u32 = 1;
let mut i = 0;
while i < input.len() && depth > 0 {
match input[i] {
b'(' => depth += 1,
b')' => depth -= 1,
b'"' => {
i += 1;
while i < input.len() && input[i] != b'"' {
if input[i] == b'\\' {
i += 1; if i >= input.len() {
break;
}
}
i += 1;
}
if i >= input.len() {
break;
}
}
b'~' if i + 1 < input.len() && input[i + 1] == b'{' => {
i += 1; continue;
}
b'{' => {
let start = i + 1;
let mut end = start;
while end < input.len() && input[end].is_ascii_digit() {
end += 1;
}
if end > start && end < input.len() {
let count_end = end;
if input[end] == b'+' {
end += 1;
}
if end < input.len() && input[end] == b'}' {
end += 1; if end + 1 < input.len() && input[end] == b'\r' && input[end + 1] == b'\n' {
end += 2;
if let Ok(s) = std::str::from_utf8(&input[start..count_end]) {
if let Ok(count) = s.parse::<usize>() {
if let Some(new_end) = end.checked_add(count) {
if new_end <= input.len() {
i = new_end;
continue;
}
}
}
}
}
}
}
}
_ => {}
}
i += 1;
}
if depth != 0 {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Char,
)));
}
Ok((&input[i..], &input[..i - 1]))
}
const MAX_BODY_NESTING_DEPTH: u32 = 64;
fn body_structure(input: &[u8], utf8_mode: bool, depth: u32) -> IResult<&[u8], BodyStructure> {
if depth > MAX_BODY_NESTING_DEPTH {
return Err(nom::Err::Failure(nom::error::Error::new(
input,
nom::error::ErrorKind::TooLarge,
)));
}
let (input, _) = char('(')(input)?;
if input.first() == Some(&b'(') {
body_type_mpart(input, utf8_mode, depth)
} else {
body_type_single(input, utf8_mode, depth)
}
}
fn body_type_single(input: &[u8], utf8_mode: bool, depth: u32) -> IResult<&[u8], BodyStructure> {
let (input, media_type_raw) = string(input)?;
let media_type = String::from_utf8_lossy(&media_type_raw).to_ascii_uppercase();
let (input, _) = sp(input)?;
let (input, media_subtype_raw) = string(input)?;
let media_subtype = String::from_utf8_lossy(&media_subtype_raw).to_ascii_uppercase();
let (input, _) = sp(input)?;
let (input, params) = body_params(input)?;
let (input, _) = sp(input)?;
let (input, id) = nstring_utf8(input)?;
let (input, _) = sp(input)?;
let (input, description_raw) = nstring(input)?;
let description = description_raw.map(|v| {
if utf8_mode {
String::from_utf8_lossy(&v).into_owned()
} else {
decode_rfc2047(&v)
}
});
let (input, _) = sp(input)?;
let (input, encoding_raw) = string(input)?;
let encoding = String::from_utf8_lossy(&encoding_raw).to_ascii_uppercase();
let (input, _) = sp(input)?;
let (input, size) = number64(input)?;
if media_type == "TEXT" {
let (input, _) = sp(input)?;
let (input, lines) = number64(input)?;
let (input, ext) = body_ext_1part(input)?;
let (input, _) = char(')')(input)?;
Ok((
input,
BodyStructure::Text {
media_subtype,
params,
id,
description,
encoding,
size,
lines,
md5: ext.0,
disposition: ext.1,
language: ext.2,
location: ext.3,
},
))
} else if media_type == "MESSAGE" && (media_subtype == "RFC822" || media_subtype == "GLOBAL") {
let (input, _) = sp(input)?;
let (input, env) = envelope(input, utf8_mode)?;
let (input, _) = sp(input)?;
let (input, body) = body_structure(input, utf8_mode, depth + 1)?;
let (input, _) = sp(input)?;
let (input, lines) = number64(input)?;
let (input, ext) = body_ext_1part(input)?;
let (input, _) = char(')')(input)?;
Ok((
input,
BodyStructure::Message {
media_subtype,
params,
id,
description,
encoding,
size,
envelope: Box::new(env),
body: Box::new(body),
lines,
md5: ext.0,
disposition: ext.1,
language: ext.2,
location: ext.3,
},
))
} else {
let (input, ext) = body_ext_1part(input)?;
let (input, _) = char(')')(input)?;
Ok((
input,
BodyStructure::Basic {
media_type,
media_subtype,
params,
id,
description,
encoding,
size,
md5: ext.0,
disposition: ext.1,
language: ext.2,
location: ext.3,
},
))
}
}
fn body_type_mpart(input: &[u8], utf8_mode: bool, depth: u32) -> IResult<&[u8], BodyStructure> {
let mut bodies = Vec::new();
let mut input = input;
while input.first() == Some(&b'(') {
let (rest, bs) = body_structure(input, utf8_mode, depth + 1)?;
bodies.push(bs);
input = rest;
}
if bodies.is_empty() {
return Err(nom::Err::Failure(nom::error::Error::new(
input,
nom::error::ErrorKind::Many1,
)));
}
let (input, _) = sp(input)?;
let (input, subtype_raw) = string(input)?;
let media_subtype = String::from_utf8_lossy(&subtype_raw).to_ascii_uppercase();
let (input, ext) = body_ext_mpart(input)?;
let (input, _) = char(')')(input)?;
Ok((
input,
BodyStructure::Multipart {
media_subtype,
bodies,
params: ext.0,
disposition: ext.1,
language: ext.2,
location: ext.3,
},
))
}
fn body_params(input: &[u8]) -> IResult<&[u8], Vec<(String, String)>> {
alt((
value(vec![], tag_no_case(b"NIL")),
delimited(
char('('),
separated_list0(
sp,
map(tuple((string_utf8, preceded(sp, string_utf8))), |(k, v)| {
(k.to_ascii_lowercase(), v)
}),
),
char(')'),
),
))(input)
}
fn body_disposition(input: &[u8]) -> IResult<&[u8], Option<ContentDisposition>> {
alt((
value(None, tag_no_case(b"NIL")),
map(
delimited(
char('('),
tuple((string_utf8, preceded(sp, body_params))),
char(')'),
),
|(disposition_type, params)| {
Some(ContentDisposition {
disposition_type: disposition_type.to_ascii_uppercase(),
params,
})
},
),
))(input)
}
fn body_language(input: &[u8]) -> IResult<&[u8], Option<Vec<String>>> {
alt((
value(None, tag_no_case(b"NIL")),
map(
delimited(
char('('),
separated_list0(sp, string_utf8),
char(')'),
),
Some,
),
map(string_utf8, |s| Some(vec![s])),
))(input)
}
type BodyExtData = (
Option<String>,
Option<ContentDisposition>,
Option<Vec<String>>,
Option<String>,
);
type MpartExtData = (
Vec<(String, String)>,
Option<ContentDisposition>,
Option<Vec<String>>,
Option<String>,
);
fn at_body_ext_end(input: &[u8]) -> IResult<&[u8], bool> {
let (trimmed, _) = take_while(|b: u8| b == b' ')(input)?;
if trimmed.first() == Some(&b')') {
Ok((trimmed, true))
} else {
Ok((input, false))
}
}
fn body_ext_1part(input: &[u8]) -> IResult<&[u8], BodyExtData> {
let (input, at_end) = at_body_ext_end(input)?;
if at_end {
return Ok((input, (None, None, None, None)));
}
let (input, _) = sp(input)?;
let (input, md5) = nstring_utf8(input)?;
let (input, at_end) = at_body_ext_end(input)?;
if at_end {
return Ok((input, (md5, None, None, None)));
}
let (input, _) = sp(input)?;
let (input, disposition) = body_disposition(input)?;
let (input, at_end) = at_body_ext_end(input)?;
if at_end {
return Ok((input, (md5, disposition, None, None)));
}
let (input, _) = sp(input)?;
let (input, language) = body_language(input)?;
let (input, at_end) = at_body_ext_end(input)?;
if at_end {
return Ok((input, (md5, disposition, language, None)));
}
let (input, _) = sp(input)?;
let (input, location) = nstring_utf8(input)?;
let (input, ()) = skip_balanced_parens(input)?;
Ok((input, (md5, disposition, language, location)))
}
fn body_ext_mpart(input: &[u8]) -> IResult<&[u8], MpartExtData> {
let (input, at_end) = at_body_ext_end(input)?;
if at_end {
return Ok((input, (vec![], None, None, None)));
}
let (input, _) = sp(input)?;
let (input, params) = body_params(input)?;
let (input, at_end) = at_body_ext_end(input)?;
if at_end {
return Ok((input, (params, None, None, None)));
}
let (input, _) = sp(input)?;
let (input, disposition) = body_disposition(input)?;
let (input, at_end) = at_body_ext_end(input)?;
if at_end {
return Ok((input, (params, disposition, None, None)));
}
let (input, _) = sp(input)?;
let (input, language) = body_language(input)?;
let (input, at_end) = at_body_ext_end(input)?;
if at_end {
return Ok((input, (params, disposition, language, None)));
}
let (input, _) = sp(input)?;
let (input, location) = nstring_utf8(input)?;
let (input, ()) = skip_balanced_parens(input)?;
Ok((input, (params, disposition, language, location)))
}
#[cfg(test)]
#[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
)]
mod tests {
use super::*;
#[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 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_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_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, "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, "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_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, "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, "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 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!(
env.sender.is_empty(),
"sender should be empty (NIL), not defaulted to from"
);
assert!(
env.reply_to.is_empty(),
"reply_to should be empty (NIL), not defaulted to from"
);
}
#[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!(
env.sender.is_empty(),
"sender should be empty (NIL from server), not defaulted to from; got {:?}",
env.sender
);
assert!(
env.reply_to.is_empty(),
"reply_to should be empty (NIL from server), not defaulted to from; 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(), 2);
assert_eq!(params[0].0, "filename*0*");
assert_eq!(params[1].0, "filename*1");
let decoded = crate::types::rfc2231::decode_rfc2231_params(params);
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].0, "filename");
assert_eq!(decoded[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_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, "UTF-8''t%C3%A9st%20file.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(), 2);
assert_eq!(params[0], ("filename*0".into(), "very-long-".into()));
assert_eq!(params[1], ("filename*1".into(), "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(), 2);
assert_eq!(params[0].0, "filename*0*");
assert_eq!(params[0].1, "UTF-8''%C3%A9l%C3%A8ve-");
assert_eq!(params[1], ("filename*1".into(), "rapport.pdf".into()));
}
#[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, "UTF-8''r%C3%A9sum%C3%A9.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, "ISO-8859-1''caf%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, "UTF-8''t%C3%A9st.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).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).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).unwrap();
if let UntaggedResponse::MailboxStatus { mailbox, items } = resp {
assert_eq!(mailbox, "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_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], UidRange::range(1, u32::MAX));
} else {
panic!("expected Modified, got {code:?}");
}
}
#[test]
fn response_code_modified_star_mixed() {
let (_, code) = response_code(b"[MODIFIED 1,5:*,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, u32::MAX));
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 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());
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);
assert_eq!(fr.binary_sizes[0], (vec![], 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 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_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).unwrap();
if let UntaggedResponse::List(info) = resp {
assert_eq!(info.name, "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).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, "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, "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, "NewName");
assert_eq!(
info.old_name.as_deref(),
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, "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).unwrap();
if let UntaggedResponse::MailboxStatus { mailbox, items } = resp {
assert_eq!(mailbox, "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).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).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).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 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.to_uppercase(), "MIXED");
assert_eq!(bodies.len(), 2);
if let BodyStructure::Multipart {
media_subtype: inner_sub,
bodies: inner_bodies,
..
} = &bodies[0]
{
assert_eq!(inner_sub.to_uppercase(), "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_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_capped() {
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(), 255);
assert_eq!(rest[0], b'A'); }
#[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, "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, "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, "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, "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, "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, "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, "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, "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, "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 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, "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, "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, "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, "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, "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, "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, "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, "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).unwrap();
if let UntaggedResponse::QuotaRoot { mailbox, roots } = resp {
assert_eq!(mailbox, "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).unwrap();
if let UntaggedResponse::QuotaRoot { mailbox, roots } = resp {
assert_eq!(mailbox, "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).unwrap();
if let UntaggedResponse::QuotaRoot { mailbox, roots } = resp {
assert_eq!(mailbox, "Sent Items");
assert!(roots.is_empty());
} else {
panic!("expected QuotaRoot, got {resp:?}");
}
}
#[test]
fn quotaroot_truncated_input() {
assert!(parse_untagged_quotaroot(b"QUOTAROOT INBOX").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).unwrap();
if let UntaggedResponse::Acl { mailbox, entries } = resp {
assert_eq!(mailbox, "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).unwrap();
if let UntaggedResponse::Acl { mailbox, entries } = resp {
assert_eq!(mailbox, "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).unwrap();
if let UntaggedResponse::Acl { mailbox, entries } = resp {
assert_eq!(mailbox, "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").is_err());
}
#[test]
fn acl_garbage_data() {
assert!(parse_untagged_acl(b"GARBAGE data\r\n").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).unwrap();
if let UntaggedResponse::ListRights {
mailbox,
identifier,
required,
optional,
} = resp
{
assert_eq!(mailbox, "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).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").is_err());
assert!(parse_untagged_listrights(b"LISTRIGHTS INBOX fred lr").is_err());
}
#[test]
fn listrights_garbage_data() {
assert!(parse_untagged_listrights(b"LISTRIGHTS !!!GARBAGE\r\n").is_err());
}
#[test]
fn myrights_atom_mailbox() {
let input = b"MYRIGHTS INBOX lr\r\n";
let (_, resp) = parse_untagged_myrights(input).unwrap();
if let UntaggedResponse::MyRights { mailbox, rights } = resp {
assert_eq!(mailbox, "INBOX");
assert_eq!(rights, "lr");
} else {
panic!("expected MyRights, got {resp:?}");
}
}
#[test]
fn myrights_truncated_input() {
assert!(parse_untagged_myrights(b"MYRIGHTS INBOX").is_err());
assert!(parse_untagged_myrights(b"MYRIGHTS INBOX lrs").is_err());
}
#[test]
fn myrights_garbage_data() {
assert!(parse_untagged_myrights(b"MYRIGHTS !!!GARBAGE\r\n").is_err());
}
#[test]
fn metadata_single_entry() {
let input = b"METADATA \"INBOX\" (\"/private/comment\" \"My folder\")\r\n";
let (_, resp) = parse_untagged_metadata(input).unwrap();
if let UntaggedResponse::Metadata { mailbox, entries } = resp {
assert_eq!(mailbox, "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).unwrap();
if let UntaggedResponse::Metadata { mailbox, entries } = resp {
assert_eq!(mailbox, "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).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).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).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).unwrap();
if let UntaggedResponse::Metadata { mailbox, entries } = resp {
assert_eq!(mailbox, "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")
.is_err()
);
assert!(
parse_untagged_metadata(b"METADATA \"INBOX\" (\"/private/comment\" \"val\")").is_err()
);
assert!(parse_untagged_metadata(b"METADATA \"INBOX\" (\"/private/comment\"").is_err());
}
#[test]
fn metadata_garbage_data() {
assert!(parse_untagged_metadata(b"METADATA !!!GARBAGE\r\n").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, "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(), 3);
assert_eq!(params[0].0, "filename*0");
assert_eq!(params[0].1, "very-");
assert_eq!(params[1].0, "filename*1");
assert_eq!(params[1].1, "long-");
assert_eq!(params[2].0, "filename*2");
assert_eq!(params[2].1, "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_rejected() {
assert!(
literal(b"~{3+}\r\nabc").is_err(),
"literal8 with + must be rejected per RFC 9051 Section 9"
);
}
#[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_rejects_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_err(),
"literal8 with + must be rejected per RFC 9051 Section 9"
);
}
#[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).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).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).unwrap();
if let UntaggedResponse::List(info) = resp {
assert_eq!(info.delimiter, None);
} else {
panic!("expected List response");
}
}
#[test]
fn list_delimiter_multi_char_rejected() {
let input = b"LIST () \"//\" INBOX\r\n";
let result = parse_untagged_list(input);
assert!(
result.is_err(),
"multi-character delimiter must be rejected per RFC 3501 Section 9: \
mailbox-list requires exactly one QUOTED-CHAR, got: {result:?}"
);
}
#[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);
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, "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);
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 namespace_nested_extension_data() {
let input = b"NAMESPACE ((\"\" \"/\" \"X-PARAM\" (\"FLAG1\" \"FLAG2\"))) NIL NIL\r\n";
let result = parse_untagged_namespace(input);
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);
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).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).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).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_rejects_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], "valid UIDs before zero 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 response_code_uidvalidity_zero_rejected() {
assert!(response_code(b"[UIDVALIDITY 0]").is_err());
}
#[test]
fn response_code_uidnext_zero_rejected() {
assert!(response_code(b"[UIDNEXT 0]").is_err());
}
#[test]
fn response_code_unseen_zero_rejected() {
assert!(response_code(b"[UNSEEN 0]").is_err());
}
#[test]
fn response_code_appenduid_zero_uidvalidity_rejected() {
assert!(response_code(b"[APPENDUID 0 5678]").is_err());
}
#[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_rejected() {
assert!(response_code(b"[COPYUID 0 1:5 10:14]").is_err());
}
#[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_rejects_zero() {
let input = b"* STATUS INBOX (UIDNEXT 0)\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => {
assert!(
matches!(*boxed, UntaggedResponse::Unknown(_)),
"STATUS UIDNEXT 0 should fall through to Unknown, got {boxed:?}"
);
}
other => panic!("Expected Untagged(Unknown), got {other:?}"),
}
}
#[test]
fn status_uidvalidity_rejects_zero() {
let input = b"* STATUS INBOX (UIDVALIDITY 0)\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => {
assert!(
matches!(*boxed, UntaggedResponse::Unknown(_)),
"STATUS UIDVALIDITY 0 should fall through to Unknown, got {boxed:?}"
);
}
other => panic!("Expected Untagged(Unknown), got {other:?}"),
}
}
#[test]
fn special_use_all_attribute() {
use crate::types::mailbox::{MailboxAttribute, MailboxInfo, SpecialUse};
let info = MailboxInfo {
name: "All Mail".into(),
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, "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, "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, "NewName");
assert_eq!(info.delimiter, Some('/'));
assert_eq!(
info.old_name.as_deref(),
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, "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, "folder");
assert_eq!(info.old_name.as_deref(), 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, "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, "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, "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, "folder");
assert_eq!(
info.old_name.as_deref(),
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_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, "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, "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 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_rejects_zero() {
let input = b"(UID 1 MODSEQ (0))";
let result = fetch_response_inner(input, false);
let (_, fr) = result.expect(
"FETCH with malformed MODSEQ (0) must still parse — other attributes must be preserved",
);
assert_eq!(
fr.mod_seq, None,
"MODSEQ 0 must be rejected per RFC 7162 (should be None, not Some(0))"
);
assert_eq!(
fr.uid,
Some(1),
"UID must be preserved when MODSEQ is malformed"
);
}
#[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 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 preserved");
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_rejects_multichar_delimiter() {
let input = b"* NAMESPACE ((\"\" \"ab\")) NIL NIL\r\n";
let (_, resp) = parse_response(input).unwrap();
match resp {
Response::Untagged(boxed) => {
assert!(
matches!(*boxed, UntaggedResponse::Unknown(_)),
"NAMESPACE with multi-char delimiter should fall through to Unknown, got {boxed:?}"
);
}
other => panic!("Expected Untagged(Unknown), 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.to_ascii_lowercase(), "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 uppercased"
);
}
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 uppercased"
);
}
other => panic!("expected Text variant, got {other:?}"),
}
}
other => panic!("expected Fetch, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
#[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_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_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_eq!(
rest, b")",
"skip_balanced_parens must leave closing ')' unconsumed after overflow"
);
}
#[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_ok(),
"skip_paren_group must handle overflow gracefully"
);
}
#[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 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 malformed MODSEQ must still parse — other attributes must be preserved",
);
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, None,
"malformed MODSEQ should be None, not Some(0)"
);
} else {
panic!("expected Fetch, got {u:?} — malformed MODSEQ 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_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 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 bodystructure_description_utf8_mode_no_rfc2047() {
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("=?UTF-8?B?SGVsbG8=?="),
"RFC 2047 decoding must be skipped 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, "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, "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_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_rejects_plus_suffix() {
let result = literal(b"~{5+}\r\nhello");
assert!(
result.is_err(),
"literal8 ~{{n+}} must be rejected per RFC 9051 Section 9"
);
}
#[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_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, "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, "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, "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).unwrap();
if let UntaggedResponse::MailboxStatus { mailbox, items } = resp {
assert_eq!(mailbox, "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).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).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_triggers_crlf_break() {
let input = b"ESEARCH (TAG \"A001\") UID COUNT 3 \r\n";
let result = parse_untagged_esearch(input);
assert!(
result.is_err(),
"trailing space after last ESEARCH result data should cause crlf failure"
);
}
#[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_triggers_crlf_break() {
let input = b"QUOTAROOT INBOX \"\" \r\n";
let result = parse_untagged_quotaroot(input);
assert!(
result.is_err(),
"trailing space after last root should cause crlf failure"
);
}
#[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, "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));
}
}