#[cfg(not(feature = "quirk_crlf_relaxed"))]
use abnf_core::streaming::crlf;
#[cfg(feature = "quirk_crlf_relaxed")]
use abnf_core::streaming::crlf_relaxed as crlf;
use abnf_core::streaming::sp;
use base64::{Engine, engine::general_purpose::STANDARD as _base64};
use imap_types::{
core::{Text, Vec1},
fetch::MessageDataItem,
response::{
Bye, Capability, Code, CodeOther, CommandContinuationRequest, Data, Greeting, GreetingKind,
Response, Status, StatusBody, StatusKind, Tagged,
},
};
#[cfg(feature = "quirk_missing_text")]
use nom::combinator::peek;
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case, take_until, take_while},
combinator::{map, map_res, opt, value},
multi::separated_list1,
sequence::{delimited, preceded, terminated, tuple},
};
#[cfg(feature = "ext_id")]
use crate::extensions::id::id_response;
#[cfg(feature = "ext_metadata")]
use crate::extensions::metadata::metadata_code;
use crate::{
core::{atom, charset, nz_number, tag_imap, text},
decode::IMAPResult,
extensions::{
enable::enable_data,
uidplus::{resp_code_apnd, resp_code_copy},
},
fetch::msg_att,
flag::flag_perm,
mailbox::mailbox_data,
};
#[cfg(feature = "ext_condstore_qresync")]
use crate::{extensions::condstore_qresync::mod_sequence_value, sequence::sequence_set};
pub(crate) fn greeting(input: &[u8]) -> IMAPResult<&[u8], Greeting> {
let mut parser = delimited(
tag(b"* "),
alt((
resp_cond_auth,
map(resp_cond_bye, |resp_text| (GreetingKind::Bye, resp_text)),
)),
crlf,
);
let (remaining, (kind, (code, text))) = parser(input)?;
Ok((remaining, Greeting { kind, code, text }))
}
#[allow(clippy::type_complexity)]
pub(crate) fn resp_cond_auth(
input: &[u8],
) -> IMAPResult<&[u8], (GreetingKind, (Option<Code>, Text))> {
let mut parser = tuple((
alt((
value(GreetingKind::Ok, tag_no_case(b"OK ")),
value(GreetingKind::PreAuth, tag_no_case(b"PREAUTH ")),
)),
resp_text,
));
let (remaining, (kind, resp_text)) = parser(input)?;
Ok((remaining, (kind, resp_text)))
}
pub(crate) fn resp_text(input: &[u8]) -> IMAPResult<&[u8], (Option<Code>, Text)> {
let (_, start) = opt(tag(b"["))(input)?;
if start.is_some() {
tuple((
preceded(
tag(b"["),
map(
alt((
terminated(resp_text_code, tag(b"]")),
map(
terminated(
take_while(|b: u8| b != b']' && b != b'\r' && b != b'\n'),
tag(b"]"),
),
|bytes: &[u8]| Code::Other(CodeOther::unvalidated(bytes)),
),
)),
Some,
),
),
#[cfg(not(feature = "quirk_missing_text"))]
preceded(sp, text),
#[cfg(feature = "quirk_missing_text")]
alt((
preceded(sp, text),
map(peek(crlf), |_| {
log::warn!("Rectified missing `text` to \"...\"");
Text::unvalidated("...")
}),
)),
))(input)
} else {
map(text, |text| (None, text))(input)
}
}
pub(crate) fn resp_text_code(input: &[u8]) -> IMAPResult<&[u8], Code> {
alt((
value(Code::Alert, tag_no_case(b"ALERT")),
map(
preceded(
tag_no_case(b"BADCHARSET"),
opt(delimited(
tag(b" ("),
separated_list1(sp, charset),
tag(b")"),
)),
),
|maybe_charsets| Code::BadCharset {
allowed: maybe_charsets.unwrap_or_default(),
},
),
map(capability_data, Code::Capability),
value(Code::Parse, tag_no_case(b"PARSE")),
map(
preceded(
tag_no_case(b"PERMANENTFLAGS "),
delimited(
tag(b"("),
map(opt(separated_list1(sp, flag_perm)), |maybe_flags| {
maybe_flags.unwrap_or_default()
}),
tag(b")"),
),
),
Code::PermanentFlags,
),
value(Code::ReadOnly, tag_no_case(b"READ-ONLY")),
value(Code::ReadWrite, tag_no_case(b"READ-WRITE")),
value(Code::TryCreate, tag_no_case(b"TRYCREATE")),
map(preceded(tag_no_case(b"UIDNEXT "), nz_number), Code::UidNext),
map(
preceded(tag_no_case(b"UIDVALIDITY "), nz_number),
Code::UidValidity,
),
map(preceded(tag_no_case(b"UNSEEN "), nz_number), Code::Unseen),
value(Code::CompressionActive, tag_no_case(b"COMPRESSIONACTIVE")),
value(Code::OverQuota, tag_no_case(b"OVERQUOTA")),
value(Code::TooBig, tag_no_case(b"TOOBIG")),
#[cfg(feature = "ext_metadata")]
map(
preceded(tag_no_case("METADATA "), metadata_code),
Code::Metadata,
),
value(Code::UnknownCte, tag_no_case(b"UNKNOWN-CTE")),
resp_code_apnd,
resp_code_copy,
value(Code::UidNotSticky, tag_no_case(b"UIDNOTSTICKY")),
#[cfg(feature = "ext_condstore_qresync")]
alt((
map(
preceded(tag_no_case(b"HIGHESTMODSEQ "), mod_sequence_value),
Code::HighestModSeq,
),
value(Code::NoModSeq, tag_no_case(b"NOMODSEQ")),
map(
preceded(tag_no_case(b"MODIFIED "), sequence_set),
Code::Modified,
),
value(Code::Closed, tag_no_case(b"CLOSED")),
)),
))(input)
}
pub(crate) fn capability_data(input: &[u8]) -> IMAPResult<&[u8], Vec1<Capability>> {
map(
#[cfg(not(feature = "quirk_trailing_space_capability"))]
preceded(tag_no_case("CAPABILITY "), separated_list1(sp, capability)),
#[cfg(feature = "quirk_trailing_space_capability")]
terminated(
preceded(tag_no_case("CAPABILITY "), separated_list1(sp, capability)),
opt(sp),
),
Vec1::unvalidated,
)(input)
}
pub(crate) fn capability(input: &[u8]) -> IMAPResult<&[u8], Capability> {
map(atom, Capability::from)(input)
}
pub(crate) fn resp_cond_bye(input: &[u8]) -> IMAPResult<&[u8], (Option<Code>, Text)> {
preceded(tag_no_case(b"BYE "), resp_text)(input)
}
pub(crate) fn response(input: &[u8]) -> IMAPResult<&[u8], Response> {
alt((
#[cfg(feature = "quirk_empty_continue_req")]
map(empty_continue_req, Response::CommandContinuationRequest),
map(continue_req, Response::CommandContinuationRequest),
response_data,
map(response_done, Response::Status),
))(input)
}
#[cfg(feature = "quirk_empty_continue_req")]
pub(crate) fn empty_continue_req(input: &[u8]) -> IMAPResult<&[u8], CommandContinuationRequest> {
let mut parser = tuple((tag(b"+"), crlf));
let (remaining, _) = parser(input)?;
log::warn!("Rectified faulty continuation request `+\r\n` to `+ ...\r\n`");
let req = CommandContinuationRequest::basic(None, "...").unwrap();
Ok((remaining, req))
}
pub(crate) fn continue_req(input: &[u8]) -> IMAPResult<&[u8], CommandContinuationRequest> {
enum Either<A, B> {
Base64(A),
Basic(B),
}
let mut parser = tuple((
tag(b"+ "),
alt((
#[cfg(not(feature = "quirk_crlf_relaxed"))]
map(
map_res(take_until("\r\n"), |input| _base64.decode(input)),
Either::Base64,
),
#[cfg(feature = "quirk_crlf_relaxed")]
map(
map_res(take_until("\n"), |input: &[u8]| {
if !input.is_empty() && input[input.len().saturating_sub(1)] == b'\r' {
_base64.decode(&input[..input.len().saturating_sub(1)])
} else {
_base64.decode(input)
}
}),
Either::Base64,
),
map(resp_text, Either::Basic),
)),
crlf,
));
let (remaining, (_, either, _)) = parser(input)?;
let continue_request = match either {
Either::Base64(data) => CommandContinuationRequest::base64(data),
Either::Basic((code, text)) => CommandContinuationRequest::basic(code, text).unwrap(),
};
Ok((remaining, continue_request))
}
pub(crate) fn response_data(input: &[u8]) -> IMAPResult<&[u8], Response> {
delimited(
tag(b"* "),
alt((
map(resp_cond_state, |(kind, code, text)| {
Response::Status(Status::Untagged(StatusBody { kind, code, text }))
}),
map(resp_cond_bye, |(code, text)| {
Response::Status(Status::Bye(Bye { code, text }))
}),
map(mailbox_data, Response::Data),
map(message_data, Response::Data),
map(capability_data, |caps| {
Response::Data(Data::Capability(caps))
}),
map(enable_data, Response::Data),
#[cfg(feature = "ext_id")]
map(id_response, |parameters| {
Response::Data(Data::Id { parameters })
}),
)),
crlf,
)(input)
}
pub(crate) fn resp_cond_state(input: &[u8]) -> IMAPResult<&[u8], (StatusKind, Option<Code>, Text)> {
let mut parser = tuple((
alt((
value(StatusKind::Ok, tag_no_case("OK ")),
value(StatusKind::No, tag_no_case("NO ")),
value(StatusKind::Bad, tag_no_case("BAD ")),
)),
resp_text,
));
let (remaining, (kind, (maybe_code, text))) = parser(input)?;
Ok((remaining, (kind, maybe_code, text)))
}
pub(crate) fn response_done(input: &[u8]) -> IMAPResult<&[u8], Status> {
alt((response_tagged, response_fatal))(input)
}
pub(crate) fn response_tagged(input: &[u8]) -> IMAPResult<&[u8], Status> {
let mut parser = tuple((tag_imap, sp, resp_cond_state, crlf));
let (remaining, (tag, _, (kind, code, text), _)) = parser(input)?;
Ok((
remaining,
Status::Tagged(Tagged {
tag,
body: StatusBody { kind, code, text },
}),
))
}
pub(crate) fn response_fatal(input: &[u8]) -> IMAPResult<&[u8], Status> {
let mut parser = delimited(tag(b"* "), resp_cond_bye, crlf);
let (remaining, (code, text)) = parser(input)?;
Ok((remaining, Status::Bye(Bye { code, text })))
}
pub(crate) fn message_data(input: &[u8]) -> IMAPResult<&[u8], Data> {
#[derive(Clone)]
enum TmpData<'a> {
Expunge,
Fetch(Vec1<MessageDataItem<'a>>),
}
alt((
map(
tuple((
terminated(nz_number, sp),
alt((
value(TmpData::Expunge, tag_no_case(b"EXPUNGE")),
map(preceded(tag_no_case(b"FETCH "), msg_att), TmpData::Fetch),
)),
)),
|(seq, tmp)| match tmp {
TmpData::Expunge => Data::Expunge(seq),
TmpData::Fetch(items) => Data::Fetch { seq, items },
},
),
#[cfg(feature = "ext_condstore_qresync")]
map(
tuple((
tag_no_case(b"VANISHED"),
opt(tag_no_case(" (EARLIER)")),
preceded(sp, sequence_set),
)),
|(_, earlier, known_uids)| Data::Vanished {
earlier: earlier.is_some(),
known_uids,
},
),
))(input)
}
#[cfg(test)]
mod tests {
use std::num::NonZeroU32;
use imap_types::{
body::{
BasicFields, Body, BodyExtension, BodyStructure, Disposition, Language, Location,
SinglePartExtensionData, SpecificFields,
},
core::{IString, NString, QuotedChar, Tag},
flag::FlagNameAttribute,
};
use super::*;
use crate::testing::{kat_inverse_greeting, kat_inverse_response, known_answer_test_encode};
#[test]
fn test_kat_inverse_greeting() {
kat_inverse_greeting(&[
(
b"* OK [badcharset] ...\r\n".as_slice(),
b"".as_slice(),
Greeting::ok(Some(Code::BadCharset { allowed: vec![] }), "...").unwrap(),
),
(
b"* OK [UnSEEN 12345] ...\r\naaa".as_slice(),
b"aaa".as_slice(),
Greeting::ok(
Some(Code::Unseen(NonZeroU32::try_from(12345).unwrap())),
"...",
)
.unwrap(),
),
(
b"* OK [unseen 12345] \r\n ".as_slice(),
b" ".as_slice(),
Greeting::ok(
Some(Code::Unseen(NonZeroU32::try_from(12345).unwrap())),
" ",
)
.unwrap(),
),
(
b"* PREAUTH [ALERT] hello\r\n".as_ref(),
b"".as_ref(),
Greeting::new(GreetingKind::PreAuth, Some(Code::Alert), "hello").unwrap(),
),
]);
}
#[test]
fn test_kat_inverse_response_data() {
kat_inverse_response(&[
(
b"* CAPABILITY IMAP4REV1\r\n".as_ref(),
b"".as_ref(),
Response::Data(Data::Capability(Vec1::from(Capability::Imap4Rev1))),
),
(
b"* LIST (\\Noselect) \"/\" bbb\r\n",
b"",
Response::Data(Data::List {
items: vec![FlagNameAttribute::Noselect],
delimiter: Some(QuotedChar::try_from('/').unwrap()),
mailbox: "bbb".try_into().unwrap(),
}),
),
(
b"* SEARCH 1 2 3 42\r\n",
b"",
Response::Data(Data::Search(
vec![
1.try_into().unwrap(),
2.try_into().unwrap(),
3.try_into().unwrap(),
42.try_into().unwrap(),
],
#[cfg(feature = "ext_condstore_qresync")]
None,
)),
),
(b"* 42 EXISTS\r\n", b"", Response::Data(Data::Exists(42))),
(
b"* 12345 RECENT\r\n",
b"",
Response::Data(Data::Recent(12345)),
),
(
b"* 123 EXPUNGE\r\n",
b"",
Response::Data(Data::Expunge(123.try_into().unwrap())),
),
]);
}
#[cfg(feature = "ext_condstore_qresync")]
#[test]
fn test_kat_inverse_response_vanished() {
kat_inverse_response(&[
(
b"* VANISHED 1\r\n".as_ref(),
b"".as_ref(),
Response::Data(Data::Vanished {
earlier: false,
known_uids: "1".try_into().unwrap(),
}),
),
(
b"* VANISHED 1:5,7\r\n".as_ref(),
b"".as_ref(),
Response::Data(Data::Vanished {
earlier: false,
known_uids: "1:5,7".try_into().unwrap(),
}),
),
(
b"* VANISHED (EARLIER) 1:5,7,9:11\r\n".as_ref(),
b"".as_ref(),
Response::Data(Data::Vanished {
earlier: true,
known_uids: "1:5,7,9:11".try_into().unwrap(),
}),
),
]);
}
#[cfg(feature = "ext_condstore_qresync")]
#[test]
fn test_parse_response_vanished_with_leading_seq_rejected() {
assert!(response_data(b"* 5 VANISHED 1:3\r\n").is_err());
assert!(response_data(b"* 5 VANISHED (EARLIER) 1:3\r\n").is_err());
}
#[test]
fn test_kat_inverse_response_status() {
kat_inverse_response(&[
(
b"A1 OK [ALERT] hello\r\n".as_ref(),
b"".as_ref(),
Response::Status(
Status::ok(
Some(Tag::try_from("A1").unwrap()),
Some(Code::Alert),
"hello",
)
.unwrap(),
),
),
(
b"A1 NO [ALERT] hello\r\n",
b"".as_ref(),
Response::Status(
Status::no(
Some(Tag::try_from("A1").unwrap()),
Some(Code::Alert),
"hello",
)
.unwrap(),
),
),
(
b"A1 BAD [ALERT] hello\r\n",
b"".as_ref(),
Response::Status(
Status::bad(
Some(Tag::try_from("A1").unwrap()),
Some(Code::Alert),
"hello",
)
.unwrap(),
),
),
(
b"A1 OK hello\r\n",
b"".as_ref(),
Response::Status(
Status::ok(Some(Tag::try_from("A1").unwrap()), None, "hello").unwrap(),
),
),
(
b"A1 NO hello\r\n",
b"".as_ref(),
Response::Status(
Status::no(Some(Tag::try_from("A1").unwrap()), None, "hello").unwrap(),
),
),
(
b"A1 BAD hello\r\n",
b"".as_ref(),
Response::Status(
Status::bad(Some(Tag::try_from("A1").unwrap()), None, "hello").unwrap(),
),
),
(
b"* OK [ALERT] hello\r\n",
b"".as_ref(),
Response::Status(Status::ok(None, Some(Code::Alert), "hello").unwrap()),
),
(
b"* NO [ALERT] hello\r\n",
b"".as_ref(),
Response::Status(Status::no(None, Some(Code::Alert), "hello").unwrap()),
),
(
b"* BAD [ALERT] hello\r\n",
b"".as_ref(),
Response::Status(Status::bad(None, Some(Code::Alert), "hello").unwrap()),
),
(
b"* OK hello\r\n",
b"".as_ref(),
Response::Status(Status::ok(None, None, "hello").unwrap()),
),
(
b"* NO hello\r\n",
b"".as_ref(),
Response::Status(Status::no(None, None, "hello").unwrap()),
),
(
b"* BAD hello\r\n",
b"".as_ref(),
Response::Status(Status::bad(None, None, "hello").unwrap()),
),
(
b"* BYE [ALERT] hello\r\n",
b"".as_ref(),
Response::Status(Status::bye(Some(Code::Alert), "hello").unwrap()),
),
]);
}
#[test]
fn test_encode_body_structure() {
let tests = [
(
BodyStructure::Single {
body: Body {
basic: BasicFields {
parameter_list: vec![],
id: NString(None),
description: NString::try_from("description").unwrap(),
content_transfer_encoding: IString::try_from("cte").unwrap(),
size: 123,
},
specific: SpecificFields::Basic {
r#type: IString::try_from("application").unwrap(),
subtype: IString::try_from("voodoo").unwrap(),
},
},
extension_data: None,
},
b"(\"application\" \"voodoo\" NIL NIL \"description\" \"cte\" 123)".as_ref(),
),
(
BodyStructure::Single {
body: Body {
basic: BasicFields {
parameter_list: vec![],
id: NString(None),
description: NString::try_from("description").unwrap(),
content_transfer_encoding: IString::try_from("cte").unwrap(),
size: 123,
},
specific: SpecificFields::Text {
subtype: IString::try_from("plain").unwrap(),
number_of_lines: 14,
},
},
extension_data: None,
},
b"(\"TEXT\" \"plain\" NIL NIL \"description\" \"cte\" 123 14)",
),
(
BodyStructure::Single {
body: Body {
basic: BasicFields {
parameter_list: vec![],
id: NString(None),
description: NString::try_from("description").unwrap(),
content_transfer_encoding: IString::try_from("cte").unwrap(),
size: 123,
},
specific: SpecificFields::Text {
subtype: IString::try_from("plain").unwrap(),
number_of_lines: 14,
},
},
extension_data: Some(SinglePartExtensionData {
md5: NString::try_from("AABB").unwrap(),
tail: Some(Disposition {
disposition: None,
tail: Some(Language {
language: vec![],
tail: Some(Location {
location: NString(None),
extensions: vec![BodyExtension::List(Vec1::from(BodyExtension::Number(1337)))],
}),
}),
}),
}),
},
b"(\"TEXT\" \"plain\" NIL NIL \"description\" \"cte\" 123 14 \"AABB\" NIL NIL NIL (1337))",
),
];
for test in tests {
known_answer_test_encode(test);
}
}
#[test]
fn test_parse_response_negative() {
let tests = [
];
for test in tests {
assert!(response(test).is_err());
}
}
#[test]
fn test_parse_resp_text_quirk() {
#[cfg(not(feature = "quirk_missing_text"))]
{
assert!(resp_text(b"[IMAP4rev1]\r\n").is_err());
assert!(resp_text(b"[IMAP4rev1]\r\n").is_err());
assert!(resp_text(b"[IMAP4rev1] \r\n").is_err());
assert!(resp_text(b"[IMAP4rev1] \r\n").is_ok());
}
#[cfg(feature = "quirk_missing_text")]
{
assert!(resp_text(b"[IMAP4rev1]\r\n").is_ok());
assert!(resp_text(b"[IMAP4rev1] \r\n").is_err());
assert!(resp_text(b"[IMAP4rev1] \r\n").is_ok());
}
}
#[test]
fn test_parse_resp_space_quirk() {
assert!(response_data(b"* STATUS INBOX (MESSAGES 100 UNSEEN 0)\r\n").is_ok());
assert!(response_data(b"* STATUS INBOX (MESSAGES 100 UNSEEN 0) \r\n").is_err());
#[cfg(not(feature = "quirk_trailing_space_status"))]
assert!(response_data(b"* STATUS INBOX (MESSAGES 100 UNSEEN 0) \r\n").is_err());
#[cfg(feature = "quirk_trailing_space_status")]
assert!(response_data(b"* STATUS INBOX (MESSAGES 100 UNSEEN 0) \r\n").is_ok());
}
#[test]
fn test_quirk_trailing_space_capability() {
assert!(response_data(b"* CAPABILITY IMAP4REV1\r\n").is_ok());
assert!(response_data(b"* CAPABILITY IMAP4REV1 \r\n").is_err());
#[cfg(not(feature = "quirk_trailing_space_capability"))]
assert!(response_data(b"* CAPABILITY IMAP4REV1 \r\n").is_err());
#[cfg(feature = "quirk_trailing_space_capability")]
assert!(response_data(b"* CAPABILITY IMAP4REV1 \r\n").is_ok());
}
}