use super::envelope_fetch::fetch_response_inner;
use super::extensions::{
parse_untagged_acl, parse_untagged_listrights, parse_untagged_metadata,
parse_untagged_myrights, parse_untagged_quota, parse_untagged_quotaroot, parse_untagged_thread,
parse_untagged_unknown,
};
use super::flags_caps::{capability_list, flag_list, resp_text, sequence_set, uid_set};
#[allow(clippy::wildcard_imports)]
use super::primitives::*;
#[allow(clippy::wildcard_imports)]
use super::*;
pub(super) 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 }))
}
pub(super) 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(),
},
))
}
}
pub(super) 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,
|i| parse_untagged_list(i, utf8_mode),
|i| parse_untagged_lsub(i, utf8_mode),
parse_untagged_esearch,
parse_untagged_search,
parse_untagged_sort,
|i| parse_untagged_status_mailbox(i, utf8_mode),
)),
alt((
parse_untagged_enabled,
parse_untagged_vanished,
parse_untagged_id,
|i| parse_untagged_namespace(i, utf8_mode),
|i| parse_untagged_quotaroot(i, utf8_mode),
parse_untagged_quota,
|i| parse_untagged_acl(i, utf8_mode),
|i| parse_untagged_myrights(i, utf8_mode),
|i| parse_untagged_listrights(i, utf8_mode),
|i| parse_untagged_metadata(i, utf8_mode),
)),
alt((parse_untagged_thread, parse_untagged_unknown)),
))(input)
}
pub(super) 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(),
},
))
}
}
pub(super) 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, _) = tag_no_case(b"EXISTS")(input)?;
let (input, _) = take_while(|b: u8| b == b' ')(input)?;
let (input, _) = crlf(input)?;
return Ok((input, UntaggedResponse::Exists(n)));
}
if input.len() >= 6 && input[..6].eq_ignore_ascii_case(b"RECENT") {
let (input, _) = tag_no_case(b"RECENT")(input)?;
let (input, _) = take_while(|b: u8| b == b' ')(input)?;
let (input, _) = 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, _) = tag_no_case(b"EXPUNGE")(input)?;
let (input, _) = take_while(|b: u8| b == b' ')(input)?;
let (input, _) = 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))))
}
pub(super) 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)))
}
pub(super) 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],
utf8_mode: bool,
) -> IResult<&[u8], (Option<MailboxName>, 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_bytes) = astring(rest2)?;
let (rest2, _) = char(')')(rest2)?;
old_name = Some(decode_mailbox_from_wire(&name_bytes, utf8_mode));
input = rest2;
}
"CHILDINFO" => {
let (rest2, _) = char('(')(rest)?;
let (rest2, items) =
separated_list0(take_while1(|b: u8| b == b' '), 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],
utf8_mode: bool,
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(
take_while1(|b: u8| b == b' '),
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_bytes) = astring(input)?;
let name = decode_mailbox_from_wire(&name_bytes, utf8_mode);
let (input, ext_data) = opt(preceded(sp, |i| parse_list_extended_data(i, utf8_mode)))(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,
}),
))
}
pub(super) fn parse_untagged_list(
input: &[u8],
utf8_mode: bool,
) -> IResult<&[u8], UntaggedResponse> {
parse_mailbox_list_response(input, b"LIST", utf8_mode, UntaggedResponse::List)
}
pub(super) fn parse_untagged_lsub(
input: &[u8],
utf8_mode: bool,
) -> IResult<&[u8], UntaggedResponse> {
parse_mailbox_list_response(input, b"LSUB", utf8_mode, UntaggedResponse::Lsub)
}
fn mailbox_delimiter(input: &[u8]) -> IResult<&[u8], Option<char>> {
let (rest, delimiter_bytes) = alt((value(None, nil_token), 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() {
tracing::warn!(
delimiter = %s,
"LIST delimiter has multiple characters (RFC 3501 Section 9 \
requires exactly one QUOTED-CHAR); taking only the first character"
);
}
Ok((rest, ch))
}
}
}
pub(super) fn parse_mailbox_attribute(s: &str) -> MailboxAttribute {
MailboxAttribute::from_imap_str(s)
}
pub(super) 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,
},
))
}
pub(super) 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, number))(input)?;
let nums: Vec<u32> = nums.into_iter().filter(|&n| n != 0).collect();
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))
}
pub(super) 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,
terminated(
tag_no_case(b"UID"),
peek(alt((
value((), verify(take(1u8), |b: &[u8]| !is_atom_char(b[0]))),
value((), eof),
))),
),
))(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') || rest.first() == Some(&b'\n') {
input = rest;
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" => {
if let Ok((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;
}
} else {
let (rest2, _) = take_while(|b: u8| b != b' ' && b != b'\r')(rest)?;
input = rest2;
}
}
_ => {
let Ok((rest2, _)) = sp(rest) else {
input = rest;
continue;
};
if rest2.first() == Some(&b'(') {
let (rest2, ()) = skip_parenthesized_block(rest2)?;
input = rest2;
} else {
let (rest2, ()) =
super::envelope_fetch::skip_tagged_ext_simple(|b| b == b' ' || b == b'\r')(
rest2,
)?;
input = rest2;
}
}
}
}
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::Esearch(esearch)))
}
pub(super) fn parse_untagged_status_mailbox(
input: &[u8],
utf8_mode: bool,
) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"STATUS")(input)?;
let (input, _) = sp(input)?;
let (input, mailbox_bytes) = astring(input)?;
let mailbox = decode_mailbox_from_wire(&mailbox_bytes, utf8_mode);
let (input, _) = sp(input)?;
let (input, items) = delimited(char('('), status_items, char(')'))(input)?;
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::MailboxStatus { mailbox, items }))
}
#[allow(clippy::too_many_lines)]
pub(super) 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_tolerant(rest)?;
if let Some(val) = val {
items.push(StatusItem::Messages(val));
}
input_loop = rest;
}
"RECENT" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number_tolerant(rest)?;
if let Some(val) = val {
items.push(StatusItem::Recent(val));
}
input_loop = rest;
}
"UNSEEN" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number_tolerant(rest)?;
if let Some(val) = val {
items.push(StatusItem::Unseen(val));
}
input_loop = rest;
}
"UIDNEXT" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number_tolerant(rest)?;
if let Some(val) = val {
items.push(StatusItem::UidNext(val));
}
input_loop = rest;
}
"UIDVALIDITY" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number_tolerant(rest)?;
if let Some(val) = val {
items.push(StatusItem::UidValidity(val));
}
input_loop = rest;
}
"DELETED" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number_tolerant(rest)?;
if let Some(val) = val {
items.push(StatusItem::Deleted(val));
}
input_loop = rest;
}
"HIGHESTMODSEQ" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number64_tolerant(rest)?;
if let Some(val) = val {
items.push(StatusItem::HighestModSeq(val));
}
input_loop = rest;
}
"SIZE" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number64_tolerant(rest)?;
if let Some(val) = val {
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(nil_token, |_| Some(None::<u64>)),
map(number64_tolerant, |opt| opt.map(Some)),
))(rest)?;
if let Some(val) = val {
items.push(StatusItem::AppendLimit(val));
}
input_loop = rest;
}
"DELETED-STORAGE" => {
let (rest, _) = sp(rest)?;
let (rest, val) = number64_tolerant(rest)?;
if let Some(val) = val {
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, ()) =
super::envelope_fetch::skip_tagged_ext_simple(|b| b == b' ' || b == b')')(
rest,
)?;
input_loop = rest;
}
}
}
}
Ok((input_loop, items))
}
pub(super) 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)))
}
pub(super) 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,
},
))
}
pub(super) 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![], nil_token),
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, _) = take_while(|b: u8| b == b' ')(input)?;
let (input, _) = crlf(input)?;
Ok((input, UntaggedResponse::Id(params)))
}
pub(super) fn parse_untagged_namespace(
input: &[u8],
utf8_mode: bool,
) -> IResult<&[u8], UntaggedResponse> {
let (input, _) = tag_no_case(b"NAMESPACE")(input)?;
let (input, _) = sp(input)?;
let (input, personal) = namespace_list(input, utf8_mode)?;
let (input, _) = sp(input)?;
let (input, other) = namespace_list(input, utf8_mode)?;
let (input, _) = sp(input)?;
let (input, shared) = namespace_list(input, utf8_mode)?;
let (input, _) = take_while(|b: u8| b == b' ')(input)?;
let (input, _) = crlf(input)?;
Ok((
input,
UntaggedResponse::Namespace {
personal,
other,
shared,
},
))
}
fn namespace_list(input: &[u8], utf8_mode: bool) -> IResult<&[u8], Vec<NamespaceDescriptor>> {
alt((
value(vec![], nil_token),
delimited(
char('('),
many0(|i| namespace_descriptor(i, utf8_mode)),
char(')'),
),
))(input)
}
fn namespace_descriptor(input: &[u8], utf8_mode: bool) -> IResult<&[u8], NamespaceDescriptor> {
let (input, _) = char('(')(input)?;
let (input, raw_prefix) = string_utf8(input)?;
let prefix = if utf8_mode {
raw_prefix
} else {
crate::codec::utf7::decode_utf7(raw_prefix.as_bytes())
};
let (input, _) = sp(input)?;
let (input, delim_bytes) = alt((value(None, nil_token), 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() {
tracing::warn!(
delimiter = %s,
"NAMESPACE delimiter has multiple characters (RFC 2342 Section 6 \
requires exactly one QUOTED-CHAR); taking only the first character"
);
}
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)))
}