#[cfg(feature = "ext_sasl_ir")]
use std::borrow::Cow;
#[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;
pub use imap_types::command::*;
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case},
combinator::{map, opt, value},
multi::{separated_list0, separated_list1},
sequence::{delimited, preceded, tuple},
};
#[cfg(feature = "ext_sasl_ir")]
use crate::core::base64;
#[cfg(feature = "ext_compress")]
use crate::extensions::compress::compress;
#[cfg(feature = "ext_enable")]
use crate::extensions::enable::enable;
#[cfg(feature = "ext_idle")]
use crate::extensions::idle::idle;
#[cfg(feature = "ext_quota")]
use crate::extensions::quota::{getquota, getquotaroot, setquota};
#[cfg(feature = "ext_move")]
use crate::extensions::r#move::r#move;
use crate::{
auth::{auth_type, AuthMechanism},
codec::IMAPResult,
core::{astring, literal, tag_imap, AString},
datetime::date_time,
fetch::{fetch_att, Macro, MacroOrMessageDataItemNames},
flag::{flag, flag_list, Flag, StoreResponse, StoreType},
mailbox::{list_mailbox, mailbox},
search::search,
secret::Secret,
sequence::sequence_set,
status::status_att,
};
pub(crate) fn command(input: &[u8]) -> IMAPResult<&[u8], Command> {
let mut parser = tuple((
tag_imap,
sp,
alt((command_any, command_auth, command_nonauth, command_select)),
crlf,
));
let (remaining, (tag, _, body, _)) = parser(input)?;
Ok((remaining, Command { tag, body }))
}
pub(crate) fn command_any(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
alt((
value(CommandBody::Capability, tag_no_case(b"CAPABILITY")),
value(CommandBody::Logout, tag_no_case(b"LOGOUT")),
value(CommandBody::Noop, tag_no_case(b"NOOP")),
))(input)
}
pub(crate) fn command_auth(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
alt((
append,
create,
delete,
examine,
list,
lsub,
rename,
select,
status,
subscribe,
unsubscribe,
#[cfg(feature = "ext_idle")]
idle,
#[cfg(feature = "ext_enable")]
enable,
#[cfg(feature = "ext_compress")]
compress,
#[cfg(feature = "ext_quota")]
getquota,
#[cfg(feature = "ext_quota")]
getquotaroot,
#[cfg(feature = "ext_quota")]
setquota,
))(input)
}
pub(crate) fn append(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((
tag_no_case(b"APPEND"),
sp,
mailbox,
opt(preceded(sp, flag_list)),
opt(preceded(sp, date_time)),
sp,
literal,
));
let (remaining, (_, _, mailbox, flags, date, _, message)) = parser(input)?;
Ok((
remaining,
CommandBody::Append {
mailbox,
flags: flags.unwrap_or_default(),
date,
message,
},
))
}
pub(crate) fn create(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((tag_no_case(b"CREATE"), sp, mailbox));
let (remaining, (_, _, mailbox)) = parser(input)?;
Ok((remaining, CommandBody::Create { mailbox }))
}
pub(crate) fn delete(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((tag_no_case(b"DELETE"), sp, mailbox));
let (remaining, (_, _, mailbox)) = parser(input)?;
Ok((remaining, CommandBody::Delete { mailbox }))
}
pub(crate) fn examine(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((tag_no_case(b"EXAMINE"), sp, mailbox));
let (remaining, (_, _, mailbox)) = parser(input)?;
Ok((remaining, CommandBody::Examine { mailbox }))
}
pub(crate) fn list(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((tag_no_case(b"LIST"), sp, mailbox, sp, list_mailbox));
let (remaining, (_, _, reference, _, mailbox_wildcard)) = parser(input)?;
Ok((
remaining,
CommandBody::List {
reference,
mailbox_wildcard,
},
))
}
pub(crate) fn lsub(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((tag_no_case(b"LSUB"), sp, mailbox, sp, list_mailbox));
let (remaining, (_, _, reference, _, mailbox_wildcard)) = parser(input)?;
Ok((
remaining,
CommandBody::Lsub {
reference,
mailbox_wildcard,
},
))
}
pub(crate) fn rename(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((tag_no_case(b"RENAME"), sp, mailbox, sp, mailbox));
let (remaining, (_, _, mailbox, _, new_mailbox)) = parser(input)?;
Ok((
remaining,
CommandBody::Rename {
from: mailbox,
to: new_mailbox,
},
))
}
pub(crate) fn select(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((tag_no_case(b"SELECT"), sp, mailbox));
let (remaining, (_, _, mailbox)) = parser(input)?;
Ok((remaining, CommandBody::Select { mailbox }))
}
pub(crate) fn status(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((
tag_no_case(b"STATUS"),
sp,
mailbox,
sp,
delimited(tag(b"("), separated_list0(sp, status_att), tag(b")")),
));
let (remaining, (_, _, mailbox, _, item_names)) = parser(input)?;
Ok((
remaining,
CommandBody::Status {
mailbox,
item_names: item_names.into(),
},
))
}
pub(crate) fn subscribe(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((tag_no_case(b"SUBSCRIBE"), sp, mailbox));
let (remaining, (_, _, mailbox)) = parser(input)?;
Ok((remaining, CommandBody::Subscribe { mailbox }))
}
pub(crate) fn unsubscribe(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((tag_no_case(b"UNSUBSCRIBE"), sp, mailbox));
let (remaining, (_, _, mailbox)) = parser(input)?;
Ok((remaining, CommandBody::Unsubscribe { mailbox }))
}
pub(crate) fn command_nonauth(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = alt((
login,
#[cfg(not(feature = "ext_sasl_ir"))]
map(authenticate, |mechanism| CommandBody::Authenticate {
mechanism,
}),
#[cfg(feature = "ext_sasl_ir")]
map(authenticate_sasl_ir, |(mechanism, initial_response)| {
CommandBody::Authenticate {
mechanism,
initial_response,
}
}),
#[cfg(feature = "starttls")]
value(CommandBody::StartTLS, tag_no_case(b"STARTTLS")),
));
let (remaining, parsed_command_nonauth) = parser(input)?;
Ok((remaining, parsed_command_nonauth))
}
pub(crate) fn login(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((tag_no_case(b"LOGIN"), sp, userid, sp, password));
let (remaining, (_, _, username, _, password)) = parser(input)?;
Ok((
remaining,
CommandBody::Login {
username,
password: Secret::new(password),
},
))
}
#[inline]
pub(crate) fn userid(input: &[u8]) -> IMAPResult<&[u8], AString> {
astring(input)
}
#[inline]
pub(crate) fn password(input: &[u8]) -> IMAPResult<&[u8], AString> {
astring(input)
}
#[cfg(not(feature = "ext_sasl_ir"))]
pub(crate) fn authenticate(input: &[u8]) -> IMAPResult<&[u8], AuthMechanism> {
let mut parser = preceded(tag_no_case(b"AUTHENTICATE "), auth_type);
let (remaining, auth_type) = parser(input)?;
Ok((remaining, auth_type))
}
#[cfg(feature = "ext_sasl_ir")]
#[cfg_attr(docsrs, doc(cfg(feature = "ext_sasl_ir")))]
#[allow(clippy::type_complexity)]
pub(crate) fn authenticate_sasl_ir(
input: &[u8],
) -> IMAPResult<&[u8], (AuthMechanism, Option<Secret<Cow<[u8]>>>)> {
let mut parser = tuple((
tag_no_case(b"AUTHENTICATE "),
auth_type,
opt(preceded(
sp,
alt((
map(base64, |data| Secret::new(Cow::Owned(data))),
value(Secret::new(Cow::Borrowed(&b""[..])), tag("=")),
)),
)),
));
let (remaining, (_, auth_type, raw_data)) = parser(input)?;
Ok((remaining, (auth_type, raw_data)))
}
pub(crate) fn command_select(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
alt((
value(CommandBody::Check, tag_no_case(b"CHECK")),
value(CommandBody::Close, tag_no_case(b"CLOSE")),
value(CommandBody::Expunge, tag_no_case(b"EXPUNGE")),
copy,
fetch,
store,
uid,
search,
#[cfg(feature = "ext_unselect")]
value(CommandBody::Unselect, tag_no_case(b"UNSELECT")),
#[cfg(feature = "ext_move")]
r#move,
))(input)
}
pub(crate) fn copy(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((tag_no_case(b"COPY"), sp, sequence_set, sp, mailbox));
let (remaining, (_, _, sequence_set, _, mailbox)) = parser(input)?;
Ok((
remaining,
CommandBody::Copy {
sequence_set,
mailbox,
uid: false,
},
))
}
pub(crate) fn fetch(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((
tag_no_case(b"FETCH"),
sp,
sequence_set,
sp,
alt((
value(
MacroOrMessageDataItemNames::Macro(Macro::All),
tag_no_case(b"ALL"),
),
value(
MacroOrMessageDataItemNames::Macro(Macro::Fast),
tag_no_case(b"FAST"),
),
value(
MacroOrMessageDataItemNames::Macro(Macro::Full),
tag_no_case(b"FULL"),
),
map(fetch_att, |fetch_att| {
MacroOrMessageDataItemNames::MessageDataItemNames(vec![fetch_att])
}),
map(
delimited(tag(b"("), separated_list0(sp, fetch_att), tag(b")")),
MacroOrMessageDataItemNames::MessageDataItemNames,
),
)),
));
let (remaining, (_, _, sequence_set, _, macro_or_item_names)) = parser(input)?;
Ok((
remaining,
CommandBody::Fetch {
sequence_set,
macro_or_item_names,
uid: false,
},
))
}
pub(crate) fn store(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((tag_no_case(b"STORE"), sp, sequence_set, sp, store_att_flags));
let (remaining, (_, _, sequence_set, _, (kind, response, flags))) = parser(input)?;
Ok((
remaining,
CommandBody::Store {
sequence_set,
kind,
response,
flags,
uid: false,
},
))
}
pub(crate) fn store_att_flags(
input: &[u8],
) -> IMAPResult<&[u8], (StoreType, StoreResponse, Vec<Flag>)> {
let mut parser = tuple((
tuple((
map(
opt(alt((
value(StoreType::Add, tag(b"+")),
value(StoreType::Remove, tag(b"-")),
))),
|type_| match type_ {
Some(type_) => type_,
None => StoreType::Replace,
},
),
tag_no_case(b"FLAGS"),
map(opt(tag_no_case(b".SILENT")), |x| match x {
Some(_) => StoreResponse::Silent,
None => StoreResponse::Answer,
}),
)),
sp,
alt((flag_list, separated_list1(sp, flag))),
));
let (remaining, ((store_type, _, store_response), _, flag_list)) = parser(input)?;
Ok((remaining, (store_type, store_response, flag_list)))
}
pub(crate) fn uid(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
let mut parser = tuple((
tag_no_case(b"UID"),
sp,
alt((
copy,
fetch,
search,
store,
#[cfg(feature = "ext_move")]
r#move,
)),
));
let (remaining, (_, _, mut cmd)) = parser(input)?;
match cmd {
CommandBody::Copy { ref mut uid, .. }
| CommandBody::Fetch { ref mut uid, .. }
| CommandBody::Search { ref mut uid, .. }
| CommandBody::Store { ref mut uid, .. } => *uid = true,
#[cfg(feature = "ext_move")]
CommandBody::Move { ref mut uid, .. } => *uid = true,
_ => unreachable!(),
}
Ok((remaining, cmd))
}
#[cfg(test)]
mod tests {
use std::num::NonZeroU32;
use super::*;
#[cfg(feature = "ext_sasl_ir")]
use crate::codec::Encode;
#[cfg(feature = "ext_sasl_ir")]
use crate::core::Tag;
use crate::{fetch::MessageDataItemName, section::Section};
#[test]
fn test_parse_fetch() {
println!("{:#?}", fetch(b"fetch 1:1 (flags)???"));
}
#[test]
fn test_parse_fetch_att() {
let tests = [
(MessageDataItemName::Envelope, "ENVELOPE???"),
(MessageDataItemName::Flags, "FLAGS???"),
(MessageDataItemName::InternalDate, "INTERNALDATE???"),
(MessageDataItemName::Rfc822, "RFC822???"),
(MessageDataItemName::Rfc822Header, "RFC822.HEADER???"),
(MessageDataItemName::Rfc822Size, "RFC822.SIZE???"),
(MessageDataItemName::Rfc822Text, "RFC822.TEXT???"),
(MessageDataItemName::Body, "BODY???"),
(MessageDataItemName::BodyStructure, "BODYSTRUCTURE???"),
(MessageDataItemName::Uid, "UID???"),
(
MessageDataItemName::BodyExt {
partial: None,
peek: false,
section: None,
},
"BODY[]???",
),
(
MessageDataItemName::BodyExt {
partial: None,
peek: true,
section: None,
},
"BODY.PEEK[]???",
),
(
MessageDataItemName::BodyExt {
partial: None,
peek: true,
section: Some(Section::Text(None)),
},
"BODY.PEEK[TEXT]???",
),
(
MessageDataItemName::BodyExt {
partial: Some((42, NonZeroU32::try_from(1337).unwrap())),
peek: true,
section: Some(Section::Text(None)),
},
"BODY.PEEK[TEXT]<42.1337>???",
),
];
let expected_remainder = "???".as_bytes();
for (expected, test) in tests {
let (got_remainder, got) = fetch_att(test.as_bytes()).unwrap();
assert_eq!(expected, got);
assert_eq!(expected_remainder, got_remainder);
}
}
#[cfg(feature = "ext_sasl_ir")]
#[test]
fn test_that_empty_ir_is_encoded_correctly() {
let command = Command::new(
Tag::try_from("A").unwrap(),
CommandBody::Authenticate {
mechanism: AuthMechanism::PLAIN,
initial_response: Some(Secret::new(Cow::Borrowed(&b""[..]))),
},
)
.unwrap();
let buffer = command.encode().dump();
assert_eq!(buffer, b"A AUTHENTICATE PLAIN =\r\n")
}
}