use std::borrow::Cow;
use std::str::from_utf8;
use nom::{
branch::alt,
bytes::streaming::{tag, tag_no_case, take_while, take_while1},
character::streaming::char,
combinator::{map, map_res, opt, recognize, value},
multi::{many0, many1},
sequence::{delimited, pair, preceded, terminated},
IResult, Parser,
};
use crate::{
core::*, rfc2087, rfc2971, rfc3501::body::*, rfc3501::body_structure::*, rfc4314, rfc4315,
rfc4315::UidSetMember, rfc4551, rfc5161, rfc5256, rfc5464, rfc5464::Metadata, rfc7162,
Response,
};
use super::gmail;
pub mod body;
pub mod body_structure;
fn is_tag_char(c: u8) -> bool {
c != b'+' && is_astring_char(c)
}
fn status_ok(i: &[u8]) -> IResult<&[u8], Status> {
map(tag_no_case("OK"), |_s| Status::Ok).parse(i)
}
fn status_no(i: &[u8]) -> IResult<&[u8], Status> {
map(tag_no_case("NO"), |_s| Status::No).parse(i)
}
fn status_bad(i: &[u8]) -> IResult<&[u8], Status> {
map(tag_no_case("BAD"), |_s| Status::Bad).parse(i)
}
fn status_preauth(i: &[u8]) -> IResult<&[u8], Status> {
map(tag_no_case("PREAUTH"), |_s| Status::PreAuth).parse(i)
}
fn status_bye(i: &[u8]) -> IResult<&[u8], Status> {
map(tag_no_case("BYE"), |_s| Status::Bye).parse(i)
}
#[derive(Debug, Eq, PartialEq)]
pub enum Status {
Ok,
No,
Bad,
PreAuth,
Bye,
}
impl Status {
fn parse(i: &[u8]) -> IResult<&[u8], Self> {
alt((status_ok, status_no, status_bad, status_preauth, status_bye)).parse(i)
}
}
pub(crate) fn mailbox(i: &[u8]) -> IResult<&[u8], Cow<'_, str>> {
map(astring_utf8, |s| {
if s.eq_ignore_ascii_case("INBOX") {
Cow::Borrowed("INBOX")
} else {
s
}
})
.parse(i)
}
fn flag_extension(i: &[u8]) -> IResult<&[u8], &str> {
map_res(
recognize(pair(tag("\\"), take_while(is_atom_char))),
from_utf8,
)
.parse(i)
}
pub(crate) fn flag(i: &[u8]) -> IResult<&[u8], &str> {
alt((
flag_extension,
map_res(take_while1(is_astring_char), from_utf8),
))
.parse(i)
}
fn flag_list(i: &[u8]) -> IResult<&[u8], Vec<Cow<'_, str>>> {
parenthesized_list(map(flag_perm, Cow::Borrowed)).parse(i)
}
fn flag_perm(i: &[u8]) -> IResult<&[u8], &str> {
alt((map_res(tag("\\*"), from_utf8), flag)).parse(i)
}
fn resp_text_code_alert(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
map(tag_no_case("ALERT"), |_| ResponseCode::Alert).parse(i)
}
fn resp_text_code_badcharset(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
map(
preceded(
tag_no_case("BADCHARSET"),
opt(preceded(
tag(" "),
parenthesized_nonempty_list(astring_utf8),
)),
),
ResponseCode::BadCharset,
)
.parse(i)
}
fn resp_text_code_capability(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
map(capability_data, ResponseCode::Capabilities).parse(i)
}
fn resp_text_code_parse(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
map(tag_no_case("PARSE"), |_| ResponseCode::Parse).parse(i)
}
fn resp_text_code_permanent_flags(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
map(
preceded(
tag_no_case("PERMANENTFLAGS "),
parenthesized_list(map(flag_perm, Cow::Borrowed)),
),
ResponseCode::PermanentFlags,
)
.parse(i)
}
fn resp_text_code_read_only(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
map(tag_no_case("READ-ONLY"), |_| ResponseCode::ReadOnly).parse(i)
}
fn resp_text_code_read_write(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
map(tag_no_case("READ-WRITE"), |_| ResponseCode::ReadWrite).parse(i)
}
fn resp_text_code_try_create(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
map(tag_no_case("TRYCREATE"), |_| ResponseCode::TryCreate).parse(i)
}
fn resp_text_code_uid_validity(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
map(
preceded(tag_no_case("UIDVALIDITY "), number),
ResponseCode::UidValidity,
)
.parse(i)
}
fn resp_text_code_uid_next(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
map(
preceded(tag_no_case("UIDNEXT "), number),
ResponseCode::UidNext,
)
.parse(i)
}
fn resp_text_code_unseen(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
map(
preceded(tag_no_case("UNSEEN "), number),
ResponseCode::Unseen,
)
.parse(i)
}
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ResponseCode<'a> {
Alert,
BadCharset(Option<Vec<Cow<'a, str>>>),
Capabilities(Vec<Capability<'a>>),
HighestModSeq(u64), Parse,
PermanentFlags(Vec<Cow<'a, str>>),
ReadOnly,
ReadWrite,
TryCreate,
UidNext(u32),
UidValidity(u32),
Unseen(u32),
AppendUid(u32, Vec<UidSetMember>),
CopyUid(u32, Vec<UidSetMember>, Vec<UidSetMember>),
UidNotSticky,
MetadataLongEntries(u64), MetadataMaxSize(u64), MetadataTooMany, MetadataNoPrivate, }
impl<'a> ResponseCode<'a> {
fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
delimited(
tag("["),
alt((
resp_text_code_alert,
resp_text_code_badcharset,
resp_text_code_capability,
resp_text_code_parse,
resp_text_code_permanent_flags,
resp_text_code_uid_validity,
resp_text_code_uid_next,
resp_text_code_unseen,
resp_text_code_read_only,
resp_text_code_read_write,
resp_text_code_try_create,
rfc4551::resp_text_code_highest_mod_seq,
rfc4315::resp_text_code_append_uid,
rfc4315::resp_text_code_copy_uid,
rfc4315::resp_text_code_uid_not_sticky,
rfc5464::resp_text_code_metadata_long_entries,
rfc5464::resp_text_code_metadata_max_size,
rfc5464::resp_text_code_metadata_too_many,
rfc5464::resp_text_code_metadata_no_private,
)),
tag("]"),
)
.parse(i)
}
pub fn into_owned(self) -> ResponseCode<'static> {
match self {
ResponseCode::Alert => ResponseCode::Alert,
ResponseCode::BadCharset(v) => {
ResponseCode::BadCharset(v.map(|vs| vs.into_iter().map(to_owned_cow).collect()))
}
ResponseCode::Capabilities(v) => {
ResponseCode::Capabilities(v.into_iter().map(Capability::into_owned).collect())
}
ResponseCode::HighestModSeq(v) => ResponseCode::HighestModSeq(v),
ResponseCode::Parse => ResponseCode::Parse,
ResponseCode::PermanentFlags(v) => {
ResponseCode::PermanentFlags(v.into_iter().map(to_owned_cow).collect())
}
ResponseCode::ReadOnly => ResponseCode::ReadOnly,
ResponseCode::ReadWrite => ResponseCode::ReadWrite,
ResponseCode::TryCreate => ResponseCode::TryCreate,
ResponseCode::UidNext(v) => ResponseCode::UidNext(v),
ResponseCode::UidValidity(v) => ResponseCode::UidValidity(v),
ResponseCode::Unseen(v) => ResponseCode::Unseen(v),
ResponseCode::AppendUid(a, b) => ResponseCode::AppendUid(a, b),
ResponseCode::CopyUid(a, b, c) => ResponseCode::CopyUid(a, b, c),
ResponseCode::UidNotSticky => ResponseCode::UidNotSticky,
ResponseCode::MetadataLongEntries(v) => ResponseCode::MetadataLongEntries(v),
ResponseCode::MetadataMaxSize(v) => ResponseCode::MetadataMaxSize(v),
ResponseCode::MetadataTooMany => ResponseCode::MetadataTooMany,
ResponseCode::MetadataNoPrivate => ResponseCode::MetadataNoPrivate,
}
}
}
#[derive(Debug, Eq, PartialEq, Hash)]
pub enum Capability<'a> {
Imap4rev1,
Auth(Cow<'a, str>),
Atom(Cow<'a, str>),
}
impl<'a> Capability<'a> {
fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
alt((
map(tag_no_case("IMAP4rev1"), |_| Capability::Imap4rev1),
map(
map(preceded(tag_no_case("AUTH="), atom), Cow::Borrowed),
Capability::Auth,
),
map(map(atom, Cow::Borrowed), Capability::Atom),
))
.parse(i)
}
pub fn into_owned(self) -> Capability<'static> {
match self {
Capability::Imap4rev1 => Capability::Imap4rev1,
Capability::Auth(v) => Capability::Auth(to_owned_cow(v)),
Capability::Atom(v) => Capability::Atom(to_owned_cow(v)),
}
}
}
fn ensure_capabilities_contains_imap4rev(
capabilities: Vec<Capability<'_>>,
) -> Result<Vec<Capability<'_>>, ()> {
if capabilities.contains(&Capability::Imap4rev1) {
Ok(capabilities)
} else {
Err(())
}
}
fn capability_data(i: &[u8]) -> IResult<&[u8], Vec<Capability<'_>>> {
map_res(
preceded(
tag_no_case("CAPABILITY"),
many0(preceded(char(' '), Capability::parse)),
),
ensure_capabilities_contains_imap4rev,
)
.parse(i)
}
fn mailbox_data_search(i: &[u8]) -> IResult<&[u8], MailboxDatum<'_>> {
map(
terminated(
preceded(tag_no_case("SEARCH"), many0(preceded(tag(" "), number))),
opt(tag(" ")),
),
MailboxDatum::Search,
)
.parse(i)
}
fn mailbox_data_flags(i: &[u8]) -> IResult<&[u8], MailboxDatum<'_>> {
map(
preceded(tag_no_case("FLAGS "), flag_list),
MailboxDatum::Flags,
)
.parse(i)
}
fn mailbox_data_exists(i: &[u8]) -> IResult<&[u8], MailboxDatum<'_>> {
map(
terminated(number, tag_no_case(" EXISTS")),
MailboxDatum::Exists,
)
.parse(i)
}
#[derive(Debug, Eq, PartialEq, Clone)]
#[non_exhaustive]
pub enum NameAttribute<'a> {
NoInferiors,
NoSelect,
Marked,
Unmarked,
All,
Archive,
Drafts,
Flagged,
Junk,
Sent,
Trash,
Extension(Cow<'a, str>),
}
impl<'a> NameAttribute<'a> {
fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
alt((
value(NameAttribute::NoInferiors, tag_no_case("\\Noinferiors")),
value(NameAttribute::NoSelect, tag_no_case("\\Noselect")),
value(NameAttribute::Marked, tag_no_case("\\Marked")),
value(NameAttribute::Unmarked, tag_no_case("\\Unmarked")),
value(NameAttribute::All, tag_no_case("\\All")),
value(NameAttribute::Archive, tag_no_case("\\Archive")),
value(NameAttribute::Drafts, tag_no_case("\\Drafts")),
value(NameAttribute::Flagged, tag_no_case("\\Flagged")),
value(NameAttribute::Junk, tag_no_case("\\Junk")),
value(NameAttribute::Sent, tag_no_case("\\Sent")),
value(NameAttribute::Trash, tag_no_case("\\Trash")),
map(
map_res(
recognize(pair(tag("\\"), take_while(is_atom_char))),
from_utf8,
),
|s| NameAttribute::Extension(Cow::Borrowed(s)),
),
))
.parse(i)
}
pub fn into_owned(self) -> NameAttribute<'static> {
match self {
NameAttribute::NoInferiors => NameAttribute::NoInferiors,
NameAttribute::NoSelect => NameAttribute::NoSelect,
NameAttribute::Marked => NameAttribute::Marked,
NameAttribute::Unmarked => NameAttribute::Unmarked,
NameAttribute::All => NameAttribute::All,
NameAttribute::Archive => NameAttribute::Archive,
NameAttribute::Drafts => NameAttribute::Drafts,
NameAttribute::Flagged => NameAttribute::Flagged,
NameAttribute::Junk => NameAttribute::Junk,
NameAttribute::Sent => NameAttribute::Sent,
NameAttribute::Trash => NameAttribute::Trash,
NameAttribute::Extension(s) => NameAttribute::Extension(to_owned_cow(s)),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MailboxListData<'a> {
pub name_attributes: Vec<NameAttribute<'a>>,
pub delimiter: Option<Cow<'a, str>>,
pub name: Cow<'a, str>,
}
impl<'a> MailboxListData<'a> {
fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
map(
(
parenthesized_list(NameAttribute::parse),
tag(" "),
alt((map(quoted_utf8, Some), map(nil, |_| None))),
tag(" "),
mailbox,
),
|(name_attributes, _, delimiter, _, name)| MailboxListData {
name_attributes,
delimiter,
name,
},
)
.parse(i)
}
pub fn into_owned(self) -> MailboxListData<'static> {
MailboxListData {
name_attributes: self
.name_attributes
.into_iter()
.map(|named_attribute| named_attribute.into_owned())
.collect(),
delimiter: self.delimiter.map(to_owned_cow),
name: to_owned_cow(self.name),
}
}
}
fn mailbox_data_list(i: &[u8]) -> IResult<&[u8], MailboxDatum<'_>> {
map(
preceded(tag_no_case("LIST "), MailboxListData::parse),
MailboxDatum::List,
)
.parse(i)
}
fn mailbox_data_lsub(i: &[u8]) -> IResult<&[u8], MailboxDatum<'_>> {
map(
preceded(tag_no_case("LSUB "), MailboxListData::parse),
MailboxDatum::List,
)
.parse(i)
}
#[derive(Debug, Eq, PartialEq, Clone)]
#[non_exhaustive]
pub enum StatusAttribute {
HighestModSeq(u64), Messages(u32),
Recent(u32),
UidNext(u32),
UidValidity(u32),
Unseen(u32),
}
impl StatusAttribute {
fn parse(i: &[u8]) -> IResult<&[u8], Self> {
alt((
rfc4551::status_att_val_highest_mod_seq,
map(
preceded(tag_no_case("MESSAGES "), number),
StatusAttribute::Messages,
),
map(
preceded(tag_no_case("RECENT "), number),
StatusAttribute::Recent,
),
map(
preceded(tag_no_case("UIDNEXT "), number),
StatusAttribute::UidNext,
),
map(
preceded(tag_no_case("UIDVALIDITY "), number),
StatusAttribute::UidValidity,
),
map(
preceded(tag_no_case("UNSEEN "), number),
StatusAttribute::Unseen,
),
))
.parse(i)
}
}
fn status_att_list(i: &[u8]) -> IResult<&[u8], Vec<StatusAttribute>> {
parenthesized_list(StatusAttribute::parse).parse(i)
}
fn mailbox_data_status(i: &[u8]) -> IResult<&[u8], MailboxDatum<'_>> {
map(
(tag_no_case("STATUS "), mailbox, tag(" "), status_att_list),
|(_, mailbox, _, status)| MailboxDatum::Status { mailbox, status },
)
.parse(i)
}
fn mailbox_data_recent(i: &[u8]) -> IResult<&[u8], MailboxDatum<'_>> {
map(
terminated(number, tag_no_case(" RECENT")),
MailboxDatum::Recent,
)
.parse(i)
}
#[derive(Debug, Eq, PartialEq, Clone)]
#[non_exhaustive]
pub enum MailboxDatum<'a> {
Exists(u32),
Flags(Vec<Cow<'a, str>>),
List(MailboxListData<'a>),
Search(Vec<u32>),
Sort(Vec<u32>),
Status {
mailbox: Cow<'a, str>,
status: Vec<StatusAttribute>,
},
Recent(u32),
MetadataSolicited {
mailbox: Cow<'a, str>,
values: Vec<Metadata>,
},
MetadataUnsolicited {
mailbox: Cow<'a, str>,
values: Vec<Cow<'a, str>>,
},
GmailLabels(Vec<Cow<'a, str>>),
GmailMsgId(u64),
GmailThrId(u64),
}
impl<'a> MailboxDatum<'a> {
fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
alt((
mailbox_data_flags,
mailbox_data_exists,
mailbox_data_list,
mailbox_data_lsub,
mailbox_data_status,
mailbox_data_recent,
mailbox_data_search,
gmail::mailbox_data_gmail_labels,
gmail::mailbox_data_gmail_msgid,
gmail::mailbox_data_gmail_thrid,
rfc5256::mailbox_data_sort,
))
.parse(i)
}
pub fn into_owned(self) -> MailboxDatum<'static> {
match self {
MailboxDatum::Exists(seq) => MailboxDatum::Exists(seq),
MailboxDatum::Flags(flags) => {
MailboxDatum::Flags(flags.into_iter().map(to_owned_cow).collect())
}
MailboxDatum::List(data) => MailboxDatum::List(data.into_owned()),
MailboxDatum::Search(seqs) => MailboxDatum::Search(seqs),
MailboxDatum::Sort(seqs) => MailboxDatum::Sort(seqs),
MailboxDatum::Status { mailbox, status } => MailboxDatum::Status {
mailbox: to_owned_cow(mailbox),
status,
},
MailboxDatum::Recent(seq) => MailboxDatum::Recent(seq),
MailboxDatum::MetadataSolicited { mailbox, values } => {
MailboxDatum::MetadataSolicited {
mailbox: to_owned_cow(mailbox),
values,
}
}
MailboxDatum::MetadataUnsolicited { mailbox, values } => {
MailboxDatum::MetadataUnsolicited {
mailbox: to_owned_cow(mailbox),
values: values.into_iter().map(to_owned_cow).collect(),
}
}
MailboxDatum::GmailLabels(labels) => {
MailboxDatum::GmailLabels(labels.into_iter().map(to_owned_cow).collect())
}
MailboxDatum::GmailMsgId(msgid) => MailboxDatum::GmailMsgId(msgid),
MailboxDatum::GmailThrId(thrid) => MailboxDatum::GmailThrId(thrid),
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct Address<'a> {
pub name: Option<Cow<'a, [u8]>>,
pub adl: Option<Cow<'a, [u8]>>,
pub mailbox: Option<Cow<'a, [u8]>>,
pub host: Option<Cow<'a, [u8]>>,
}
impl<'a> Address<'a> {
fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
paren_delimited(map(
(
nstring,
tag(" "),
nstring,
tag(" "),
nstring,
tag(" "),
nstring,
),
|(name, _, adl, _, mailbox, _, host)| Address {
name: name.map(Cow::Borrowed),
adl: adl.map(Cow::Borrowed),
mailbox: mailbox.map(Cow::Borrowed),
host: host.map(Cow::Borrowed),
},
))
.parse(i)
}
pub fn into_owned(self) -> Address<'static> {
Address {
name: self.name.map(to_owned_cow),
adl: self.adl.map(to_owned_cow),
mailbox: self.mailbox.map(to_owned_cow),
host: self.host.map(to_owned_cow),
}
}
}
fn opt_addresses(i: &[u8]) -> IResult<&[u8], Option<Vec<Address<'_>>>> {
alt((
map(nil, |_s| None),
map(
paren_delimited(many1(terminated(Address::parse, opt(char(' '))))),
Some,
),
))
.parse(i)
}
#[derive(Debug, Eq, PartialEq)]
pub struct Envelope<'a> {
pub date: Option<Cow<'a, [u8]>>,
pub subject: Option<Cow<'a, [u8]>>,
pub from: Option<Vec<Address<'a>>>,
pub sender: Option<Vec<Address<'a>>>,
pub reply_to: Option<Vec<Address<'a>>>,
pub to: Option<Vec<Address<'a>>>,
pub cc: Option<Vec<Address<'a>>>,
pub bcc: Option<Vec<Address<'a>>>,
pub in_reply_to: Option<Cow<'a, [u8]>>,
pub message_id: Option<Cow<'a, [u8]>>,
}
impl<'a> Envelope<'a> {
pub(crate) fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
paren_delimited(map(
(
nstring,
tag(" "),
nstring,
tag(" "),
opt_addresses,
tag(" "),
opt_addresses,
tag(" "),
opt_addresses,
tag(" "),
opt_addresses,
tag(" "),
opt_addresses,
tag(" "),
opt_addresses,
tag(" "),
nstring,
tag(" "),
nstring,
),
|(
date,
_,
subject,
_,
from,
_,
sender,
_,
reply_to,
_,
to,
_,
cc,
_,
bcc,
_,
in_reply_to,
_,
message_id,
)| Envelope {
date: date.map(Cow::Borrowed),
subject: subject.map(Cow::Borrowed),
from,
sender,
reply_to,
to,
cc,
bcc,
in_reply_to: in_reply_to.map(Cow::Borrowed),
message_id: message_id.map(Cow::Borrowed),
},
))
.parse(i)
}
pub fn into_owned(self) -> Envelope<'static> {
Envelope {
date: self.date.map(to_owned_cow),
subject: self.subject.map(to_owned_cow),
from: self
.from
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
sender: self
.sender
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
reply_to: self
.reply_to
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
to: self
.to
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
cc: self
.cc
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
bcc: self
.bcc
.map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
in_reply_to: self.in_reply_to.map(to_owned_cow),
message_id: self.message_id.map(to_owned_cow),
}
}
}
fn msg_att_envelope(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
map(
preceded(tag_no_case("ENVELOPE "), Envelope::parse),
|envelope| AttributeValue::Envelope(Box::new(envelope)),
)
.parse(i)
}
fn msg_att_internal_date(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
map(
preceded(tag_no_case("INTERNALDATE "), nstring_utf8),
|date| AttributeValue::InternalDate(date.unwrap()),
)
.parse(i)
}
fn msg_att_flags(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
map(
preceded(tag_no_case("FLAGS "), flag_list),
AttributeValue::Flags,
)
.parse(i)
}
fn msg_att_rfc822(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
map(preceded(tag_no_case("RFC822 "), nstring), |v| {
AttributeValue::Rfc822(v.map(Cow::Borrowed))
})
.parse(i)
}
fn msg_att_rfc822_header(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
map(
(tag_no_case("RFC822.HEADER "), opt(tag(" ")), nstring),
|(_, _, raw)| AttributeValue::Rfc822Header(raw.map(Cow::Borrowed)),
)
.parse(i)
}
fn msg_att_rfc822_size(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
map(
preceded(tag_no_case("RFC822.SIZE "), number),
AttributeValue::Rfc822Size,
)
.parse(i)
}
fn msg_att_rfc822_text(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
map(preceded(tag_no_case("RFC822.TEXT "), nstring), |v| {
AttributeValue::Rfc822Text(v.map(Cow::Borrowed))
})
.parse(i)
}
fn msg_att_uid(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
map(preceded(tag_no_case("UID "), number), AttributeValue::Uid).parse(i)
}
fn msg_att_emailid(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
map(
preceded(
tag_no_case("EMAILID "),
paren_delimited(map_res(take_while1(is_astring_char), from_utf8)),
),
|id| AttributeValue::EmailId(Cow::Borrowed(id)),
)
.parse(i)
}
fn msg_att_threadid(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
map(
preceded(
tag_no_case("THREADID "),
alt((
map(nil, |_| None),
map(
paren_delimited(map_res(take_while1(is_astring_char), from_utf8)),
|id| Some(Cow::Borrowed(id)),
),
)),
),
AttributeValue::ThreadId,
)
.parse(i)
}
fn msg_att_unknown(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
map(
pair(
map_res(take_while1(is_atom_char), from_utf8),
preceded(
tag(" "),
alt((
value((), paren_delimited(take_while(|c: u8| c != b')'))),
value((), nstring),
value((), take_while1(|c: u8| c != b' ' && c != b')')),
)),
),
),
|_| AttributeValue::Unknown,
)
.parse(i)
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum AttributeValue<'a> {
BodySection {
section: Option<SectionPath>,
index: Option<u32>,
data: Option<Cow<'a, [u8]>>,
},
BodyStructure(BodyStructure<'a>),
Envelope(Box<Envelope<'a>>),
Flags(Vec<Cow<'a, str>>),
InternalDate(Cow<'a, str>),
ModSeq(u64), Rfc822(Option<Cow<'a, [u8]>>),
Rfc822Header(Option<Cow<'a, [u8]>>),
Rfc822Size(u32),
Rfc822Text(Option<Cow<'a, [u8]>>),
Uid(u32),
GmailLabels(Vec<Cow<'a, str>>),
GmailMsgId(u64),
GmailThrId(u64),
EmailId(Cow<'a, str>),
ThreadId(Option<Cow<'a, str>>),
Unknown,
}
impl<'a> AttributeValue<'a> {
fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
alt((
msg_att_body_section,
msg_att_body_structure,
msg_att_envelope,
msg_att_internal_date,
msg_att_flags,
rfc4551::msg_att_mod_seq,
msg_att_rfc822,
msg_att_rfc822_header,
msg_att_rfc822_size,
msg_att_rfc822_text,
msg_att_uid,
gmail::msg_att_gmail_labels,
gmail::msg_att_gmail_msgid,
gmail::msg_att_gmail_thrid,
msg_att_emailid,
msg_att_threadid,
msg_att_unknown,
))
.parse(i)
}
pub fn into_owned(self) -> AttributeValue<'static> {
match self {
AttributeValue::BodySection {
section,
index,
data,
} => AttributeValue::BodySection {
section,
index,
data: data.map(to_owned_cow),
},
AttributeValue::BodyStructure(body) => AttributeValue::BodyStructure(body.into_owned()),
AttributeValue::Envelope(e) => AttributeValue::Envelope(Box::new(e.into_owned())),
AttributeValue::Flags(v) => {
AttributeValue::Flags(v.into_iter().map(to_owned_cow).collect())
}
AttributeValue::InternalDate(v) => AttributeValue::InternalDate(to_owned_cow(v)),
AttributeValue::ModSeq(v) => AttributeValue::ModSeq(v),
AttributeValue::Rfc822(v) => AttributeValue::Rfc822(v.map(to_owned_cow)),
AttributeValue::Rfc822Header(v) => AttributeValue::Rfc822Header(v.map(to_owned_cow)),
AttributeValue::Rfc822Size(v) => AttributeValue::Rfc822Size(v),
AttributeValue::Rfc822Text(v) => AttributeValue::Rfc822Text(v.map(to_owned_cow)),
AttributeValue::Uid(v) => AttributeValue::Uid(v),
AttributeValue::GmailLabels(v) => {
AttributeValue::GmailLabels(v.into_iter().map(to_owned_cow).collect())
}
AttributeValue::GmailMsgId(v) => AttributeValue::GmailMsgId(v),
AttributeValue::GmailThrId(v) => AttributeValue::GmailThrId(v),
AttributeValue::EmailId(v) => AttributeValue::EmailId(to_owned_cow(v)),
AttributeValue::ThreadId(v) => AttributeValue::ThreadId(v.map(to_owned_cow)),
AttributeValue::Unknown => AttributeValue::Unknown,
}
}
}
fn msg_att_list(i: &[u8]) -> IResult<&[u8], Vec<AttributeValue<'_>>> {
parenthesized_nonempty_list(AttributeValue::parse).parse(i)
}
fn message_data_fetch(i: &[u8]) -> IResult<&[u8], Response<'_>> {
map(
(number, tag_no_case(" FETCH "), msg_att_list),
|(num, _, attrs)| Response::Fetch(num, attrs),
)
.parse(i)
}
fn message_data_expunge(i: &[u8]) -> IResult<&[u8], u32> {
terminated(number, tag_no_case(" EXPUNGE")).parse(i)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RequestId(pub String);
impl RequestId {
fn parse(i: &[u8]) -> IResult<&[u8], Self> {
map(map_res(take_while1(is_tag_char), from_utf8), |s| {
RequestId(s.to_string())
})
.parse(i)
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_bytes()
}
}
#[derive(Debug, Default, Eq, PartialEq)]
pub struct Outcome<'a> {
pub code: Option<ResponseCode<'a>>,
pub information: Option<Cow<'a, str>>,
}
impl<'a> Outcome<'a> {
fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
map((opt(ResponseCode::parse), text), |(code, text)| {
let information = if text.is_empty() {
None
} else if code.is_some() {
Some(match text {
Cow::Borrowed(s) => Cow::Borrowed(&s[1..]),
Cow::Owned(s) => Cow::Owned(s[1..].to_string()),
})
} else {
Some(text)
};
Outcome { code, information }
})
.parse(i)
}
pub fn into_owned(self) -> Outcome<'static> {
Outcome {
code: self.code.map(ResponseCode::into_owned),
information: self.information.map(to_owned_cow),
}
}
}
fn trailing_resp_text(i: &[u8]) -> IResult<&[u8], Outcome<'_>> {
map(opt((tag(" "), Outcome::parse)), |resptext| {
resptext.map(|(_, tuple)| tuple).unwrap_or_default()
})
.parse(i)
}
pub(crate) fn continue_req(i: &[u8]) -> IResult<&[u8], Response<'_>> {
map(
(tag("+"), opt(tag(" ")), Outcome::parse, tag("\r\n")),
|(_, _, outcome, _)| Response::Continue(outcome),
)
.parse(i)
}
pub(crate) fn response_tagged(i: &[u8]) -> IResult<&[u8], Response<'_>> {
map(
(
RequestId::parse,
tag(" "),
Status::parse,
trailing_resp_text,
tag("\r\n"),
),
|(tag, _, status, outcome, _)| Response::Done {
tag,
status,
outcome,
},
)
.parse(i)
}
fn resp_cond(i: &[u8]) -> IResult<&[u8], Response<'_>> {
map((Status::parse, trailing_resp_text), |(status, outcome)| {
Response::Data { status, outcome }
})
.parse(i)
}
pub(crate) fn response_data(i: &[u8]) -> IResult<&[u8], Response<'_>> {
delimited(
tag("* "),
alt((
resp_cond,
map(MailboxDatum::parse, Response::MailboxData),
map(message_data_expunge, Response::Expunge),
message_data_fetch,
map(capability_data, Response::Capabilities),
rfc5161::resp_enabled,
rfc5464::metadata_solicited,
rfc5464::metadata_unsolicited,
rfc7162::resp_vanished,
rfc2087::quota,
rfc2087::quota_root,
rfc2971::resp_id,
rfc4314::acl,
rfc4314::list_rights,
rfc4314::my_rights,
)),
preceded(
many0(tag(" ")), tag("\r\n"),
),
)
.parse(i)
}
#[cfg(test)]
mod tests {
use super::{AttributeValue, Capability, NameAttribute};
use assert_matches::assert_matches;
use std::borrow::Cow;
#[test]
fn test_list() {
match super::mailbox(b"iNboX ") {
Ok((_, mb)) => {
assert_eq!(mb, "INBOX");
}
rsp => panic!("unexpected response {rsp:?}"),
}
}
#[test]
fn test_envelope() {
let env = br#"ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" "IMAP4rev1 WG mtg summary and minutes" (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) ((NIL NIL "imap" "cac.washington.edu")) ((NIL NIL "minutes" "CNRI.Reston.VA.US") ("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL "<B27397-0100000@cac.washington.edu>") "#;
match super::msg_att_envelope(env) {
Ok((_, AttributeValue::Envelope(_))) => {}
rsp => panic!("unexpected response {rsp:?}"),
}
}
#[test]
fn test_opt_addresses() {
let addr = b"((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\") (\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) ";
match super::opt_addresses(addr) {
Ok((_, _addresses)) => {}
rsp => panic!("unexpected response {rsp:?}"),
}
}
#[test]
fn test_opt_addresses_no_space() {
let addr =
br#"((NIL NIL "test" "example@example.com")(NIL NIL "test" "example@example.com"))"#;
match super::opt_addresses(addr) {
Ok((_, _addresses)) => {}
rsp => panic!("unexpected response {rsp:?}"),
}
}
#[test]
fn test_addresses() {
match super::Address::parse(b"(\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\") ") {
Ok((_, _address)) => {}
rsp => panic!("unexpected response {rsp:?}"),
}
match super::Address::parse(b"({12}\r\nJoh\xff Klensin NIL \"KLENSIN\" \"MIT.EDU\") ") {
Ok((_, _address)) => {}
rsp => panic!("unexpected response {rsp:?}"),
}
}
#[test]
fn test_capability_data() {
assert_matches!(
super::capability_data(b"CAPABILITY IMAP4rev1\r\n"),
Ok((_, capabilities)) => {
assert_eq!(capabilities, vec![Capability::Imap4rev1])
}
);
assert_matches!(
super::capability_data(b"CAPABILITY XPIG-LATIN IMAP4rev1 STARTTLS AUTH=GSSAPI\r\n"),
Ok((_, capabilities)) => {
assert_eq!(capabilities, vec![
Capability::Atom(Cow::Borrowed("XPIG-LATIN")),
Capability::Imap4rev1,
Capability::Atom(Cow::Borrowed("STARTTLS")),
Capability::Auth(Cow::Borrowed("GSSAPI")),
])
}
);
assert_matches!(
super::capability_data(b"CAPABILITY IMAP4rev1 AUTH=GSSAPI AUTH=PLAIN\r\n"),
Ok((_, capabilities)) => {
assert_eq!(capabilities, vec![
Capability::Imap4rev1,
Capability::Auth(Cow::Borrowed("GSSAPI")),
Capability::Auth(Cow::Borrowed("PLAIN")),
])
}
);
assert_matches!(
super::capability_data(b"CAPABILITY AUTH=GSSAPI AUTH=PLAIN\r\n"),
Err(_)
);
}
#[test]
fn test_surgemail_select_flags() {
assert_matches!(
super::flag_list(b"(\\Answered \\Flagged \\Deleted \\Draft \\Seen $Forwarded )"),
Ok(([], flags)) => {
assert_eq!(flags, vec![
"\\Answered",
"\\Flagged",
"\\Deleted",
"\\Draft",
"\\Seen",
"$Forwarded"
])
}
);
}
#[test]
fn test_name_attribute_into_owned() {
let name_attributes = [
NameAttribute::NoInferiors,
NameAttribute::NoSelect,
NameAttribute::Marked,
NameAttribute::Unmarked,
NameAttribute::All,
NameAttribute::Archive,
NameAttribute::Drafts,
NameAttribute::Flagged,
NameAttribute::Junk,
NameAttribute::Sent,
NameAttribute::Trash,
NameAttribute::Extension(Cow::Borrowed("Foobar")),
];
for name_attribute in name_attributes {
let owned_name_attribute = name_attribute.clone().into_owned();
assert_eq!(name_attribute, owned_name_attribute);
}
}
#[test]
fn test_attribute_value_unknown_into_owned() {
assert_eq!(
AttributeValue::Unknown.into_owned(),
AttributeValue::Unknown
);
}
}