forked from meli/meli
1
Fork 0
meli/melib/src/backends/imap/protocol_parser.rs

1699 lines
65 KiB
Rust

/*
* meli - melib crate.
*
* Copyright 2017-2020 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::{convert::TryFrom, str::FromStr};
use imap_codec::{
codec::Decode,
response::{data::FetchAttributeValue, Data, Response},
};
use nom::{
branch::{alt, permutation},
bytes::complete::{is_a, is_not, tag, take, take_until, take_while},
character::{complete::digit1, is_digit},
combinator::{map, map_res, opt},
multi::{fold_many1, length_data, many0, many1, separated_list1},
sequence::{delimited, preceded},
};
use super::*;
use crate::{
email::{
address::{Address, MailboxAddress},
parser::{
generic::{byte_in_range, byte_in_slice},
BytesExt, IResult,
},
},
error::ResultIntoError,
};
bitflags! {
#[derive(Default, Serialize, Deserialize)]
pub struct RequiredResponses: u64 {
const CAPABILITY = 0b0000_0000_0000_0001;
const BYE = 0b0000_0000_0000_0010;
const FLAGS = 0b0000_0000_0000_0100;
const EXISTS = 0b0000_0000_0000_1000;
const RECENT = 0b0000_0000_0001_0000;
const UNSEEN = 0b0000_0000_0010_0000;
const PERMANENTFLAGS = 0b0000_0000_0100_0000;
const UIDNEXT = 0b0000_0000_1000_0000;
const UIDVALIDITY = 0b0000_0001_0000_0000;
const LIST = 0b0000_0010_0000_0000;
const LSUB = 0b0000_0100_0000_0000;
const STATUS = 0b0000_1000_0000_0000;
const EXPUNGE = 0b0001_0000_0000_0000;
const SEARCH = 0b0010_0000_0000_0000;
const FETCH = 0b0100_0000_0000_0000;
const NO_REQUIRED = 0b1000_0000_0000_0000;
const CAPABILITY_REQUIRED = Self::CAPABILITY.bits;
const LOGOUT_REQUIRED = Self::BYE.bits;
const SELECT_REQUIRED = Self::FLAGS.bits | Self::EXISTS.bits | Self::RECENT.bits | Self::UNSEEN.bits | Self::PERMANENTFLAGS.bits | Self::UIDNEXT.bits | Self::UIDVALIDITY.bits;
const EXAMINE_REQUIRED = Self::FLAGS.bits | Self::EXISTS.bits | Self::RECENT.bits | Self::UNSEEN.bits | Self::PERMANENTFLAGS.bits | Self::UIDNEXT.bits | Self::UIDVALIDITY.bits;
const LIST_REQUIRED = Self::LIST.bits;
const LSUB_REQUIRED = Self::LSUB.bits;
const FETCH_REQUIRED = Self::FETCH.bits;
}
}
impl RequiredResponses {
pub fn check(&self, line: &[u8]) -> bool {
if !line.starts_with(b"* ") {
return false;
}
let line = &line[b"* ".len()..];
let mut ret = false;
if self.intersects(RequiredResponses::CAPABILITY) {
ret |= line.starts_with(b"CAPABILITY");
}
if self.intersects(RequiredResponses::BYE) {
ret |= line.starts_with(b"BYE");
}
if self.intersects(RequiredResponses::FLAGS) {
ret |= line.starts_with(b"FLAGS");
}
if self.intersects(RequiredResponses::EXISTS) {
ret |= line.ends_with(b"EXISTS\r\n");
}
if self.intersects(RequiredResponses::RECENT) {
ret |= line.ends_with(b"RECENT\r\n");
}
if self.intersects(RequiredResponses::UNSEEN) {
ret |= line.starts_with(b"UNSEEN");
}
if self.intersects(RequiredResponses::PERMANENTFLAGS) {
ret |= line.starts_with(b"PERMANENTFLAGS");
}
if self.intersects(RequiredResponses::UIDNEXT) {
ret |= line.starts_with(b"UIDNEXT");
}
if self.intersects(RequiredResponses::UIDVALIDITY) {
ret |= line.starts_with(b"UIDVALIDITY");
}
if self.intersects(RequiredResponses::LIST) {
ret |= line.starts_with(b"LIST");
}
if self.intersects(RequiredResponses::LSUB) {
ret |= line.starts_with(b"LSUB");
}
if self.intersects(RequiredResponses::STATUS) {
ret |= line.starts_with(b"STATUS");
}
if self.intersects(RequiredResponses::EXPUNGE) {
ret |= line.ends_with(b"EXPUNGE\r\n");
}
if self.intersects(RequiredResponses::SEARCH) {
ret |= line.starts_with(b"SEARCH");
}
if self.intersects(RequiredResponses::FETCH) {
let mut ptr = 0;
for (i, l) in line.iter().enumerate() {
if !l.is_ascii_digit() {
ptr = i;
break;
}
}
ret |= line[ptr..].trim_start().starts_with(b"FETCH");
}
ret
}
}
#[test]
fn test_imap_required_responses() {
let mut ret = Vec::new();
let required_responses = RequiredResponses::FETCH_REQUIRED;
let response =
&b"* 1040 FETCH (UID 1064 FLAGS ())\r\nM15 OK Fetch completed (0.001 + 0.299 secs).\r\n"[..];
for l in response.split_rn() {
/* debug!("check line: {}", &l); */
if required_responses.check(l) {
ret.extend_from_slice(l);
}
}
assert_eq!(ret.as_slice(), &b"* 1040 FETCH (UID 1064 FLAGS ())\r\n"[..]);
let v = protocol_parser::uid_fetch_flags_responses(response)
.unwrap()
.1;
assert_eq!(v.len(), 1);
}
#[derive(Debug, PartialEq)]
pub struct Alert(String);
pub type ImapParseResult<'a, T> = Result<(&'a [u8], T, Option<Alert>)>;
pub struct ImapLineIterator<'a> {
slice: &'a [u8],
}
#[derive(Debug, PartialEq)]
pub enum ResponseCode {
///The human-readable text contains a special alert that MUST be presented
/// to the user in a fashion that calls the user's attention to the message.
Alert(String),
///Optionally followed by a parenthesized list of charsets. A SEARCH
/// failed because the given charset is not supported by this
/// implementation. If the optional list of charsets is given, this lists
/// the charsets that are supported by this implementation.
Badcharset(Option<String>),
/// Followed by a list of capabilities. This can appear in the initial OK
/// or PREAUTH response to transmit an initial capabilities list. This
/// makes it unnecessary for a client to send a separate CAPABILITY command
/// if it recognizes this response.
Capability,
/// The human-readable text represents an error in parsing the [RFC-2822]
/// header or [MIME-IMB] headers of a message in the mailbox.
Parse(String),
/// Followed by a parenthesized list of flags, indicates which of the known
/// flags the client can change permanently. Any flags that are in the
/// FLAGS untagged response, but not the PERMANENTFLAGS list, can not be set
/// permanently. If the client attempts to STORE a flag that is not in the
/// PERMANENTFLAGS list, the server will either ignore the change or store
/// the state change for the remainder of the current session only. The
/// PERMANENTFLAGS list can also include the special flag \*, which
/// indicates that it is possible to create new keywords by attempting to
/// store those flags in the mailbox.
Permanentflags(String),
/// The mailbox is selected read-only, or its access while selected has
/// changed from read-write to read-only.
ReadOnly,
/// The mailbox is selected read-write, or its access while selected has
/// changed from read-only to read-write.
ReadWrite,
/// An APPEND or COPY attempt is failing because the target mailbox does not
/// exist (as opposed to some other reason). This is a hint to the client
/// that the operation can succeed if the mailbox is first created by the
/// CREATE command.
Trycreate,
/// Followed by a decimal number, indicates the next unique identifier
/// value. Refer to section 2.3.1.1 for more information.
Uidnext(UID),
/// Followed by a decimal number, indicates the unique identifier validity
/// value. Refer to section 2.3.1.1 for more information.
Uidvalidity(UID),
/// Followed by a decimal number, indicates the number of the first message
/// without the \Seen flag set.
Unseen(ImapNum),
}
impl std::fmt::Display for ResponseCode {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
use ResponseCode::*;
match self {
Alert(s) => write!(fmt, "ALERT: {}", s),
Badcharset(None) => write!(fmt, "Given charset is not supported by this server."),
Badcharset(Some(s)) => write!(
fmt,
"Given charset is not supported by this server. Supported ones are: {}",
s
),
Capability => write!(fmt, "Capability response"),
Parse(s) => write!(fmt, "Server error in parsing message headers: {}", s),
Permanentflags(s) => write!(fmt, "Mailbox supports these flags: {}", s),
ReadOnly => write!(fmt, "This mailbox is selected read-only."),
ReadWrite => write!(fmt, "This mailbox is selected with read-write permissions."),
Trycreate => write!(
fmt,
"Failed to operate on the target mailbox because it doesn't exist. Try creating \
it first."
),
Uidnext(uid) => write!(fmt, "Next UID value is {}", uid),
Uidvalidity(uid) => write!(fmt, "Next UIDVALIDITY value is {}", uid),
Unseen(uid) => write!(fmt, "First message without the \\Seen flag is {}", uid),
}
}
}
impl ResponseCode {
fn from(val: &[u8]) -> ResponseCode {
use ResponseCode::*;
if !val.starts_with(b"[") {
let msg = val.trim();
return Alert(String::from_utf8_lossy(msg).to_string());
}
let val = &val[1..];
if val.starts_with(b"BADCHARSET") {
let charsets = val.find(b"(").map(|pos| val[pos + 1..].trim());
Badcharset(charsets.map(|charsets| String::from_utf8_lossy(charsets).to_string()))
} else if val.starts_with(b"READONLY") {
ReadOnly
} else if val.starts_with(b"READWRITE") {
ReadWrite
} else if val.starts_with(b"TRYCREATE") {
Trycreate
} else if val.starts_with(b"UIDNEXT") {
//FIXME
Uidnext(0)
} else if val.starts_with(b"UIDVALIDITY") {
//FIXME
Uidvalidity(0)
} else if val.starts_with(b"UNSEEN") {
//FIXME
Unseen(0)
} else {
let msg = &val[val.find(b"] ").unwrap() + 1..].trim();
Alert(String::from_utf8_lossy(msg).to_string())
}
}
}
#[derive(Debug, PartialEq)]
pub enum ImapResponse {
Ok(ResponseCode),
No(ResponseCode),
Bad(ResponseCode),
Preauth(ResponseCode),
Bye(ResponseCode),
}
impl TryFrom<&'_ [u8]> for ImapResponse {
type Error = Error;
fn try_from(val: &'_ [u8]) -> Result<ImapResponse> {
let val: &[u8] = val.split_rn().last().unwrap_or(val);
let mut val = val[val.find(b" ").ok_or_else(|| {
Error::new(format!(
"Expected tagged IMAP response (OK,NO,BAD, etc) but found {:?}",
val
))
})? + 1..]
.trim();
// M12 NO [CANNOT] Invalid mailbox name: Name must not have \'/\' characters
// (0.000 + 0.098 + 0.097 secs).\r\n
if val.ends_with(b" secs).") {
val = &val[..val.rfind(b"(").ok_or_else(|| {
Error::new(format!(
"Expected tagged IMAP response (OK,NO,BAD, etc) but found {:?}",
val
))
})?];
}
Ok(if val.starts_with(b"OK") {
Self::Ok(ResponseCode::from(&val[b"OK ".len()..]))
} else if val.starts_with(b"NO") {
Self::No(ResponseCode::from(&val[b"NO ".len()..]))
} else if val.starts_with(b"BAD") {
Self::Bad(ResponseCode::from(&val[b"BAD ".len()..]))
} else if val.starts_with(b"PREAUTH") {
Self::Preauth(ResponseCode::from(&val[b"PREAUTH ".len()..]))
} else if val.starts_with(b"BYE") {
Self::Bye(ResponseCode::from(&val[b"BYE ".len()..]))
} else {
return Err(Error::new(format!(
"Expected tagged IMAP response (OK,NO,BAD, etc) but found {:?}",
val
)));
})
}
}
impl Into<Result<()>> for ImapResponse {
fn into(self) -> Result<()> {
match self {
Self::Ok(_) | Self::Preauth(_) | Self::Bye(_) => Ok(()),
Self::No(ResponseCode::Alert(msg)) | Self::Bad(ResponseCode::Alert(msg)) => {
Err(Error::new(msg))
}
Self::No(err) => Err(Error::new(format!("{:?}", err)))
.chain_err_summary(|| "IMAP NO Response.".to_string()),
Self::Bad(err) => Err(Error::new(format!("{:?}", err)))
.chain_err_summary(|| "IMAP BAD Response.".to_string()),
}
}
}
#[test]
fn test_imap_response() {
assert_eq!(ImapResponse::try_from(&b"M12 NO [CANNOT] Invalid mailbox name: Name must not have \'/\' characters (0.000 + 0.098 + 0.097 secs).\r\n"[..]).unwrap(), ImapResponse::No(ResponseCode::Alert("Invalid mailbox name: Name must not have '/' characters".to_string())));
}
impl<'a> Iterator for ImapLineIterator<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<&'a [u8]> {
if self.slice.is_empty() {
return None;
}
let mut i = 0;
loop {
let cur_slice = &self.slice[i..];
if let Some(pos) = cur_slice.find(b"\r\n") {
/* Skip literal continuation line */
if cur_slice.get(pos.saturating_sub(1)) == Some(&b'}') {
i += pos + 2;
continue;
}
let ret = self.slice.get(..i + pos + 2).unwrap_or_default();
self.slice = self.slice.get(i + pos + 2..).unwrap_or_default();
return Some(ret);
} else {
let ret = self.slice;
self.slice = self.slice.get(ret.len()..).unwrap_or_default();
return Some(ret);
}
}
}
}
pub trait ImapLineSplit {
fn split_rn(&self) -> ImapLineIterator;
}
impl ImapLineSplit for [u8] {
fn split_rn(&self) -> ImapLineIterator {
ImapLineIterator { slice: self }
}
}
macro_rules! to_str (
($v:expr) => (unsafe{ std::str::from_utf8_unchecked($v) })
);
#[test]
fn test_imap_line_iterator() {
{
let s = b"* 1429 FETCH (UID 1505 FLAGS (\\Seen) RFC822 {26}\r\nReturn-Path: <blah blah...\r\n* 1430 FETCH (UID 1506 FLAGS (\\Seen)\r\n* 1431 FETCH (UID 1507 FLAGS (\\Seen)\r\n* 1432 FETCH (UID 1500 FLAGS (\\Seen) RFC822 {4}\r\nnull\r\n";
let line_a =
b"* 1429 FETCH (UID 1505 FLAGS (\\Seen) RFC822 {26}\r\nReturn-Path: <blah blah...\r\n";
let line_b = b"* 1430 FETCH (UID 1506 FLAGS (\\Seen)\r\n";
let line_c = b"* 1431 FETCH (UID 1507 FLAGS (\\Seen)\r\n";
let line_d = b"* 1432 FETCH (UID 1500 FLAGS (\\Seen) RFC822 {4}\r\nnull\r\n";
let mut iter = s.split_rn();
assert_eq!(to_str!(iter.next().unwrap()), to_str!(line_a));
assert_eq!(to_str!(iter.next().unwrap()), to_str!(line_b));
assert_eq!(to_str!(iter.next().unwrap()), to_str!(line_c));
assert_eq!(to_str!(iter.next().unwrap()), to_str!(line_d));
assert!(iter.next().is_none());
}
{
let s = b"* 23 FETCH (FLAGS (\\Seen) RFC822.SIZE 44827)\r\n";
let mut iter = s.split_rn();
assert_eq!(to_str!(iter.next().unwrap()), to_str!(s));
assert!(iter.next().is_none());
}
{
let s = b"";
let mut iter = s.split_rn();
assert!(iter.next().is_none());
}
{
let s = b"* 172 EXISTS\r\n* 1 RECENT\r\n* OK [UNSEEN 12] Message 12 is first unseen\r\n* OK [UIDVALIDITY 3857529045] UIDs valid\r\n* OK [UIDNEXT 4392] Predicted next UID\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n* OK [NOMODSEQ] Sorry, this mailbox format doesn't support modsequences\r\n* A142 OK [READ-WRITE] SELECT completed\r\n";
let mut iter = s.split_rn();
for l in &[
&b"* 172 EXISTS\r\n"[..],
&b"* 1 RECENT\r\n"[..],
&b"* OK [UNSEEN 12] Message 12 is first unseen\r\n"[..],
&b"* OK [UIDVALIDITY 3857529045] UIDs valid\r\n"[..],
&b"* OK [UIDNEXT 4392] Predicted next UID\r\n"[..],
&b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n"[..],
&b"* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n"[..],
&b"* OK [NOMODSEQ] Sorry, this mailbox format doesn't support modsequences\r\n"[..],
&b"* A142 OK [READ-WRITE] SELECT completed\r\n"[..],
] {
assert_eq!(to_str!(iter.next().unwrap()), to_str!(l));
}
assert!(iter.next().is_none());
}
}
/*macro_rules! dbg_dmp (
($i: expr, $submac:ident!( $($args:tt)* )) => (
{
let l = line!();
match $submac!($i, $($args)*) {
nom::IResult::Error(a) => {
debug!("Error({:?}) at l.{} by ' {} '\n{}", a, l, stringify!($submac!($($args)*)), unsafe{ std::str::from_utf8_unchecked($i) });
nom::IResult::Error(a)
},
nom::IResult::Incomplete(a) => {
debug!("Incomplete({:?}) at {} by ' {} '\n{}", a, l, stringify!($submac!($($args)*)), unsafe{ std::str::from_utf8_unchecked($i) });
nom::IResult::Incomplete(a)
},
a => a
}
}
);
($i:expr, $f:ident) => (
dbg_dmp!($i, call!($f));
);
);
*/
/*
* LIST (\HasNoChildren) "." INBOX.Sent
* LIST (\HasChildren) "." INBOX
*/
pub fn list_mailbox_result(input: &[u8]) -> IResult<&[u8], ImapMailbox> {
let (input, _) = alt((tag("* LIST ("), tag("* LSUB (")))(input.ltrim())?;
let (input, properties) = take_until(&b")"[0..])(input)?;
let (input, _) = tag(b") ")(input)?;
let (input, separator) = delimited(tag(b"\""), take(1_u32), tag(b"\""))(input)?;
let (input, _) = take(1_u32)(input)?;
let (input, path) = mailbox_token(input)?;
let (input, _) = tag("\r\n")(input)?;
Ok((
input,
({
let separator: u8 = separator[0];
let mut f = ImapMailbox::default();
f.no_select = false;
f.is_subscribed = false;
if path.eq_ignore_ascii_case("INBOX") {
f.is_subscribed = true;
let _ = f.set_special_usage(SpecialUsageMailbox::Inbox);
}
for p in properties.split(|&b| b == b' ') {
if p.eq_ignore_ascii_case(b"\\NoSelect") || p.eq_ignore_ascii_case(b"\\NonExistent")
{
f.no_select = true;
} else if p.eq_ignore_ascii_case(b"\\Subscribed") {
f.is_subscribed = true;
} else if p.eq_ignore_ascii_case(b"\\Sent") {
let _ = f.set_special_usage(SpecialUsageMailbox::Sent);
} else if p.eq_ignore_ascii_case(b"\\Junk") {
let _ = f.set_special_usage(SpecialUsageMailbox::Junk);
} else if p.eq_ignore_ascii_case(b"\\Trash") {
let _ = f.set_special_usage(SpecialUsageMailbox::Trash);
} else if p.eq_ignore_ascii_case(b"\\Drafts") {
let _ = f.set_special_usage(SpecialUsageMailbox::Drafts);
} else if p.eq_ignore_ascii_case(b"\\Flagged") {
let _ = f.set_special_usage(SpecialUsageMailbox::Flagged);
} else if p.eq_ignore_ascii_case(b"\\Archive") {
let _ = f.set_special_usage(SpecialUsageMailbox::Archive);
}
}
f.imap_path = path.to_string();
f.hash = MailboxHash::from_bytes(f.imap_path.as_bytes());
f.path = if separator == b'/' {
f.imap_path.clone()
} else {
f.imap_path.replace(separator as char, "/")
};
f.name = if let Some(pos) = f.imap_path.as_bytes().iter().rposition(|&c| c == separator)
{
f.parent = Some(MailboxHash::from_bytes(f.imap_path[..pos].as_bytes()));
f.imap_path[pos + 1..].to_string()
} else {
f.imap_path.clone()
};
f.separator = separator;
debug!(f)
}),
))
}
#[derive(Debug, Clone, PartialEq)]
pub struct FetchResponse<'a> {
pub uid: Option<UID>,
pub message_sequence_number: MessageSequenceNumber,
pub modseq: Option<ModSequence>,
pub flags: Option<(Flag, Vec<String>)>,
pub body: Option<Cow<'a, [u8]>>,
pub references: Option<&'a [u8]>,
pub envelope: Option<Envelope>,
pub raw_fetch_value: &'a [u8],
}
pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
match Response::decode(input) {
Ok((remainder, response)) => {
match response {
Response::Data(Data::Fetch {
seq_or_uid,
attributes,
}) => {
let mut ret = FetchResponse {
uid: None,
message_sequence_number: seq_or_uid.get() as usize, // TODO: Can't be both.
modseq: None, // TODO: HIGHESTMODSEQ?
flags: None,
body: None,
references: None,
envelope: None,
raw_fetch_value: &[], // TODO
};
for attribute in attributes.into_iter() {
// TODO: What should be present and what not?
match attribute {
FetchAttributeValue::Uid(uid) => ret.uid = Some(uid.get() as usize),
FetchAttributeValue::Flags(flags) => {
ret.flags = Some((
// TODO
flags
.into_iter()
.map(|flag| crate::Flag::from(flag))
.collect(),
// TODO
vec![],
))
}
FetchAttributeValue::BodyExt {
section: None,
origin: None,
data,
} => {
ret.body = data.into_option();
}
//TODO: Make query of REFERENCES more ergonomic.
FetchAttributeValue::BodyExt {
section: Some(Section::HeaderFields(None, fields)),
origin: None,
data,
} if fields
.into_iter()
.map(|i| i.as_ref())
.collect::<Vec<_>>()
.into_iter()
.collect::<Vec<_>>()
.contains(b"REFERENCES") =>
{
// TODO
println!("YAY!")
}
FetchAttributeValue::Envelope(envelope) => {
// TODO
}
FetchAttributeValue::BodyStructure(body_structure) => {
// TODO
}
unexpected => {
// Ignore unexpected attributes.
panic!("Unexpected attribute `{:?}`", unexpected);
}
}
}
Ok((remainder, ret, None))
}
_ => todo!(),
}
}
Err(error) => {
todo!()
}
}
}
pub fn fetch_responses(mut input: &[u8]) -> ImapParseResult<Vec<FetchResponse<'_>>> {
let mut ret = Vec::new();
let mut alert: Option<Alert> = None;
while input.starts_with(b"* ") {
let next_response = fetch_response(input);
match next_response {
Ok((rest, el, el_alert)) => {
if let Some(el_alert) = el_alert {
match &mut alert {
Some(Alert(ref mut alert)) => {
alert.push_str(&el_alert.0);
}
a @ None => *a = Some(el_alert),
}
}
input = rest;
ret.push(el);
}
Err(err) => {
return Err(Error::new(format!(
"Unexpected input while parsing UID FETCH responses: {} `{:.40}`",
err,
String::from_utf8_lossy(input),
)));
}
}
}
if !input.is_empty() && ret.is_empty() {
if let Ok(ImapResponse::Ok(_)) = ImapResponse::try_from(input) {
} else {
return Err(Error::new(format!(
"310Unexpected input while parsing UID FETCH responses: `{:.40}`",
String::from_utf8_lossy(input)
)));
}
}
Ok((input, ret, alert))
}
pub fn uid_fetch_flags_responses(input: &[u8]) -> IResult<&[u8], Vec<(UID, (Flag, Vec<String>))>> {
many0(uid_fetch_flags_response)(input)
}
pub fn uid_fetch_flags_response(input: &[u8]) -> IResult<&[u8], (UID, (Flag, Vec<String>))> {
let (input, _) = tag("* ")(input)?;
let (input, _msn) = take_while(is_digit)(input)?;
let (input, _) = tag(" FETCH (")(input)?;
let (input, uid_flags) = permutation((
preceded(
alt((tag("UID "), tag(" UID "))),
map_res(digit1, |s| UID::from_str(to_str!(s))),
),
preceded(
alt((tag("FLAGS "), tag(" FLAGS "))),
delimited(tag("("), byte_flags, tag(")")),
),
))(input)?;
let (input, _) = tag(")\r\n")(input)?;
Ok((input, (uid_flags.0, uid_flags.1)))
}
macro_rules! flags_to_imap_list {
($flags:ident) => {{
let mut ret = String::new();
if !($flags & Flag::REPLIED).is_empty() {
ret.push_str("\\Answered");
}
if !($flags & Flag::FLAGGED).is_empty() {
if !ret.is_empty() {
ret.push(' ');
}
ret.push_str("\\Flagged");
}
if !($flags & Flag::TRASHED).is_empty() {
if !ret.is_empty() {
ret.push(' ');
}
ret.push_str("\\Deleted");
}
if !($flags & Flag::SEEN).is_empty() {
if !ret.is_empty() {
ret.push(' ');
}
ret.push_str("\\Seen");
}
if !($flags & Flag::DRAFT).is_empty() {
if !ret.is_empty() {
ret.push(' ');
}
ret.push_str("\\Draft");
}
ret
}};
}
/* Input Example:
* ==============
*
* "M0 OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE
* IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT
* MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS
* LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN
* CONTEXT=SEARCH LIST-STATUS BINARY MOVE SPECIAL-USE] Logged in\r\n"
* "* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN
* X-GM-EXT-1 XYZZY SASL-IR AUTH=XOAUTH2 AUTH=PLAIN AUTH=PLAIN-CLIENT TOKEN
* AUTH=OAUTHBEARER AUTH=XOAUTH\r\n" "* CAPABILITY IMAP4rev1 LITERAL+
* SASL-IR LOGIN-REFERRALS ID ENABLE IDLE AUTH=PLAIN\r\n"
*/
pub fn capabilities(input: &[u8]) -> IResult<&[u8], Vec<&[u8]>> {
let (input, _) = take_until("CAPABILITY ")(input)?;
let (input, _) = tag("CAPABILITY ")(input)?;
let (input, ret) = separated_list1(tag(" "), is_not(" ]\r\n"))(input)?;
let (input, _) = take_until("\r\n")(input)?;
let (input, _) = tag("\r\n")(input)?;
Ok((input, ret))
}
/// This enum represents the server's untagged responses detailed in `7. Server
/// Responses` of RFC 3501 INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1
#[derive(Debug, PartialEq)]
pub enum UntaggedResponse<'s> {
/// ```text
/// 7.4.1. EXPUNGE Response
///
/// The EXPUNGE response reports that the specified message sequence
/// number has been permanently removed from the mailbox. The message
/// sequence number for each successive message in the mailbox is
/// immediately decremented by 1, and this decrement is reflected in
/// message sequence numbers in subsequent responses (including other
/// untagged EXPUNGE responses).
///
/// The EXPUNGE response also decrements the number of messages in the
/// mailbox; it is not necessary to send an EXISTS response with the
/// new value.
///
/// As a result of the immediate decrement rule, message sequence
/// numbers that appear in a set of successive EXPUNGE responses
/// depend upon whether the messages are removed starting from lower
/// numbers to higher numbers, or from higher numbers to lower
/// numbers. For example, if the last 5 messages in a 9-message
/// mailbox are expunged, a "lower to higher" server will send five
/// untagged EXPUNGE responses for message sequence number 5, whereas
/// a "higher to lower server" will send successive untagged EXPUNGE
/// responses for message sequence numbers 9, 8, 7, 6, and 5.
///
/// An EXPUNGE response MUST NOT be sent when no command is in
/// progress, nor while responding to a FETCH, STORE, or SEARCH
/// command. This rule is necessary to prevent a loss of
/// synchronization of message sequence numbers between client and
/// server. A command is not "in progress" until the complete command
/// has been received; in particular, a command is not "in progress"
/// during the negotiation of command continuation.
///
/// Note: UID FETCH, UID STORE, and UID SEARCH are different
/// commands from FETCH, STORE, and SEARCH. An EXPUNGE
/// response MAY be sent during a UID command.
///
/// The update from the EXPUNGE response MUST be recorded by the
/// client.
/// ```
Expunge(MessageSequenceNumber),
/// ```text
/// 7.3.1. EXISTS Response
///
/// The EXISTS response reports the number of messages in the mailbox.
/// This response occurs as a result of a SELECT or EXAMINE command,
/// and if the size of the mailbox changes (e.g., new messages).
///
/// The update from the EXISTS response MUST be recorded by the
/// client.
/// ```
Exists(ImapNum),
/// ```text
/// 7.3.2. RECENT Response
/// The RECENT response reports the number of messages with the
/// \Recent flag set. This response occurs as a result of a SELECT or
/// EXAMINE command, and if the size of the mailbox changes (e.g., new
/// messages).
/// ```
Recent(ImapNum),
Fetch(FetchResponse<'s>),
Bye {
reason: &'s str,
},
}
pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedResponse<'_>>> {
let orig_input = input;
let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b"* ")(input)?;
let (input, num) = map_res::<_, _, _, (&[u8], nom::error::ErrorKind), _, _, _>(digit1, |s| {
ImapNum::from_str(unsafe { std::str::from_utf8_unchecked(s) })
})(input)?;
let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b" ")(input)?;
let (input, _tag) =
take_until::<_, &[u8], (&[u8], nom::error::ErrorKind)>(&b"\r\n"[..])(input)?;
let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b"\r\n")(input)?;
debug!(
"Parse untagged response from {:?}",
String::from_utf8_lossy(orig_input)
);
Ok((
input,
{
use UntaggedResponse::*;
match _tag {
b"EXPUNGE" => Some(Expunge(num)),
b"EXISTS" => Some(Exists(num)),
b"RECENT" => Some(Recent(num)),
_ if _tag.starts_with(b"FETCH ") => Some(Fetch(fetch_response(orig_input)?.1)),
_ => {
debug!(
"unknown untagged_response: {}",
String::from_utf8_lossy(_tag)
);
None
}
}
},
None,
))
}
#[test]
fn test_imap_untagged_responses() {
use UntaggedResponse::*;
assert_eq!(
untagged_responses(b"* 2 EXISTS\r\n")
.map(|(_, v, _)| v)
.unwrap()
.unwrap(),
Exists(2)
);
assert_eq!(
untagged_responses(b"* 1079 FETCH (UID 1103 MODSEQ (1365) FLAGS (\\Seen))\r\n")
.map(|(_, v, _)| v)
.unwrap()
.unwrap(),
Fetch(FetchResponse {
uid: Some(1103),
message_sequence_number: 1079,
modseq: Some(ModSequence(std::num::NonZeroU64::new(1365_u64).unwrap())),
flags: Some((Flag::SEEN, vec![])),
body: None,
references: None,
envelope: None,
raw_fetch_value: &b"* 1079 FETCH (UID 1103 MODSEQ (1365) FLAGS (\\Seen))\r\n"[..],
})
);
assert_eq!(
untagged_responses(b"* 1 FETCH (FLAGS (\\Seen))\r\n")
.map(|(_, v, _)| v)
.unwrap()
.unwrap(),
Fetch(FetchResponse {
uid: None,
message_sequence_number: 1,
modseq: None,
flags: Some((Flag::SEEN, vec![])),
body: None,
references: None,
envelope: None,
raw_fetch_value: &b"* 1 FETCH (FLAGS (\\Seen))\r\n"[..],
})
);
}
#[test]
fn test_imap_fetch_response() {
#[rustfmt::skip]
let input: &[u8] = b"* 198 FETCH (UID 7608 FLAGS (\\Seen) ENVELOPE (\"Fri, 24 Jun 2011 10:09:10 +0000\" \"xxxx/xxxx\" ((\"xx@xx.com\" NIL \"xx\" \"xx.com\")) NIL NIL ((\"xx@xx\" NIL \"xx\" \"xx.com\")) ((\"'xx, xx'\" NIL \"xx.xx\" \"xx.com\")(\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\")(\"'xx'\" NIL \"xx.xx\" \"xx.com\")(\"'xx xx'\" NIL \"xx.xx\" \"xx.com\")(\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\")) NIL NIL \"<xx@xx.com>\") BODY[HEADER.FIELDS (REFERENCES)] {2}\r\n\r\n BODYSTRUCTURE ((\"text\" \"html\" (\"charset\" \"us-ascii\") \"<xx@xx>\" NIL \"7BIT\" 17236 232 NIL NIL NIL NIL)(\"image\" \"jpeg\" (\"name\" \"image001.jpg\") \"<image001.jpg@xx.xx>\" \"image001.jpg\" \"base64\" 1918 NIL (\"inline\" (\"filename\" \"image001.jpg\" \"size\" \"1650\" \"creation-date\" \"Sun, 09 Aug 2015 20:56:04 GMT\" \"modification-date\" \"Sun, 14 Aug 2022 22:11:45 GMT\")) NIL NIL) \"related\" (\"boundary\" \"xx--xx\" \"type\" \"text/html\") NIL \"en-US\"))\r\n";
#[rustfmt::skip]
// ----------------------------------- ------------- --------------------------------------- --- --- ----------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- --- ---------------
// date subject from | | to cc bcc irt message-id
// | reply-to
// sender
let mut address = SmallVec::new();
address.push(Address::new(None, "xx@xx.com".to_string()));
let mut env = Envelope::new(EnvelopeHash::default());
env.set_subject("xxxx/xxxx".as_bytes().to_vec());
env.set_date("Fri, 24 Jun 2011 10:09:10 +0000".as_bytes());
env.set_from(address.clone());
env.set_to(address);
env.set_message_id("<xx@xx.com>".as_bytes());
assert_eq!(
fetch_response(input).unwrap(),
(
&b""[..],
FetchResponse {
uid: Some(7608),
message_sequence_number: 198,
flags: Some((Flag::SEEN, vec![])),
modseq: None,
body: None,
references: None, // TODO: Some?
envelope: Some(env),
raw_fetch_value: input,
},
None
)
);
}
pub fn search_results<'a>(input: &'a [u8]) -> IResult<&'a [u8], Vec<ImapNum>> {
alt((
|input: &'a [u8]| -> IResult<&'a [u8], Vec<ImapNum>> {
let (input, _) = tag("* SEARCH ")(input)?;
let (input, list) = separated_list1(
tag(b" "),
map_res(is_not(" \r\n"), |s: &[u8]| {
ImapNum::from_str(unsafe { std::str::from_utf8_unchecked(s) })
}),
)(input)?;
let (input, _) = tag("\r\n")(input)?;
Ok((input, list))
},
|input: &'a [u8]| -> IResult<&'a [u8], Vec<ImapNum>> {
let (input, _) = tag("* SEARCH\r\n")(input)?;
Ok((input, vec![]))
},
))(input)
}
pub fn search_results_raw<'a>(input: &'a [u8]) -> IResult<&'a [u8], &'a [u8]> {
alt((
|input: &'a [u8]| -> IResult<&'a [u8], &'a [u8]> {
let (input, _) = tag("* SEARCH ")(input)?;
let (input, list) = take_until("\r\n")(input)?;
let (input, _) = tag("\r\n")(input)?;
Ok((input, list))
},
|input: &'a [u8]| -> IResult<&'a [u8], &'a [u8]> {
let (input, _) = tag("* SEARCH\r\n")(input)?;
Ok((input, &b""[0..]))
},
))(input)
}
#[test]
fn test_imap_search() {
assert_eq!(search_results(b"* SEARCH\r\n").map(|(_, v)| v), Ok(vec![]));
assert_eq!(
search_results(b"* SEARCH 1\r\n").map(|(_, v)| v),
Ok(vec![1])
);
assert_eq!(
search_results(b"* SEARCH 1 2 3 4\r\n").map(|(_, v)| v),
Ok(vec![1, 2, 3, 4])
);
assert_eq!(
search_results_raw(b"* SEARCH 1 2 3 4\r\n").map(|(_, v)| v),
Ok(&b"1 2 3 4"[..])
);
}
#[derive(Debug, Default, PartialEq, Clone)]
pub struct SelectResponse {
pub exists: ImapNum,
pub recent: ImapNum,
pub flags: (Flag, Vec<String>),
pub first_unseen: MessageSequenceNumber,
pub uidvalidity: UIDVALIDITY,
pub uidnext: UID,
pub permanentflags: (Flag, Vec<String>),
/// if SELECT returns \* we can set arbritary flags permanently.
pub can_create_flags: bool,
pub read_only: bool,
pub highestmodseq: Option<std::result::Result<ModSequence, ()>>,
}
/*
* Example: C: A142 SELECT INBOX
* S: * 172 EXISTS
* S: * 1 RECENT
* S: * OK [UNSEEN 12] Message 12 is first unseen
* S: * OK [UIDVALIDITY 3857529045] UIDs valid
* S: * OK [UIDNEXT 4392] Predicted next UID
* S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft)
* S: * OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited
* S: A142 OK [READ-WRITE] SELECT completed
*/
/*
*
* * FLAGS (\Answered \Flagged \Deleted \Seen \Draft)
* * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)] Flags
* permitted.
* * 45 EXISTS
* * 0 RECENT
* * OK [UNSEEN 16] First unseen.
* * OK [UIDVALIDITY 1554422056] UIDs valid
* * OK [UIDNEXT 50] Predicted next UID
*/
pub fn select_response(input: &[u8]) -> Result<SelectResponse> {
if input.contains_subsequence(b"* OK") {
let mut ret = SelectResponse::default();
for l in input.split_rn() {
if l.starts_with(b"* ") && l.ends_with(b" EXISTS\r\n") {
ret.exists = ImapNum::from_str(&String::from_utf8_lossy(
&l[b"* ".len()..l.len() - b" EXISTS\r\n".len()],
))?;
} else if l.starts_with(b"* ") && l.ends_with(b" RECENT\r\n") {
ret.recent = ImapNum::from_str(&String::from_utf8_lossy(
&l[b"* ".len()..l.len() - b" RECENT\r\n".len()],
))?;
} else if l.starts_with(b"* FLAGS (") {
ret.flags = flags(&l[b"* FLAGS (".len()..l.len() - b")".len()]).map(|(_, v)| v)?;
} else if l.starts_with(b"* OK [UNSEEN ") {
ret.first_unseen = MessageSequenceNumber::from_str(&String::from_utf8_lossy(
&l[b"* OK [UNSEEN ".len()..l.find(b"]").unwrap()],
))?;
} else if l.starts_with(b"* OK [UIDVALIDITY ") {
ret.uidvalidity = UIDVALIDITY::from_str(&String::from_utf8_lossy(
&l[b"* OK [UIDVALIDITY ".len()..l.find(b"]").unwrap()],
))?;
} else if l.starts_with(b"* OK [UIDNEXT ") {
ret.uidnext = UID::from_str(&String::from_utf8_lossy(
&l[b"* OK [UIDNEXT ".len()..l.find(b"]").unwrap()],
))?;
} else if l.starts_with(b"* OK [PERMANENTFLAGS (") {
ret.permanentflags =
flags(&l[b"* OK [PERMANENTFLAGS (".len()..l.find(b")").unwrap()])
.map(|(_, v)| v)?;
ret.can_create_flags = l.contains_subsequence(b"\\*");
} else if l.contains_subsequence(b"OK [READ-WRITE]" as &[u8]) {
ret.read_only = false;
} else if l.contains_subsequence(b"OK [READ-ONLY]") {
ret.read_only = true;
} else if l.starts_with(b"* OK [HIGHESTMODSEQ ") {
let res: IResult<&[u8], &[u8]> =
take_until(&b"]"[..])(&l[b"* OK [HIGHESTMODSEQ ".len()..]);
let (_, highestmodseq) = res?;
ret.highestmodseq = Some(
std::num::NonZeroU64::new(u64::from_str(&String::from_utf8_lossy(
highestmodseq,
))?)
.map(|u| Ok(ModSequence(u)))
.unwrap_or(Err(())),
);
} else if l.starts_with(b"* OK [NOMODSEQ") {
ret.highestmodseq = Some(Err(()));
} else if !l.is_empty() {
debug!("select response: {}", String::from_utf8_lossy(l));
}
}
Ok(ret)
} else {
let ret = String::from_utf8_lossy(input).to_string();
debug!("BAD/NO response in select: {}", &ret);
Err(Error::new(ret))
}
}
#[test]
fn test_imap_select_response() {
let r = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)] Flags permitted.\r\n* 45 EXISTS\r\n* 0 RECENT\r\n* OK [UNSEEN 16] First unseen.\r\n* OK [UIDVALIDITY 1554422056] UIDs valid\r\n* OK [UIDNEXT 50] Predicted next UID\r\n";
assert_eq!(
select_response(r).expect("Could not parse IMAP select response"),
SelectResponse {
exists: 45,
recent: 0,
flags: (
Flag::REPLIED | Flag::SEEN | Flag::TRASHED | Flag::DRAFT | Flag::FLAGGED,
Vec::new()
),
first_unseen: 16,
uidvalidity: 1554422056,
uidnext: 50,
permanentflags: (
Flag::REPLIED | Flag::SEEN | Flag::TRASHED | Flag::DRAFT | Flag::FLAGGED,
vec!["*".into()]
),
can_create_flags: true,
read_only: false,
highestmodseq: None
}
);
let r = b"* 172 EXISTS\r\n* 1 RECENT\r\n* OK [UNSEEN 12] Message 12 is first unseen\r\n* OK [UIDVALIDITY 3857529045] UIDs valid\r\n* OK [UIDNEXT 4392] Predicted next UID\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n* OK [HIGHESTMODSEQ 715194045007]\r\n* A142 OK [READ-WRITE] SELECT completed\r\n";
assert_eq!(
select_response(r).expect("Could not parse IMAP select response"),
SelectResponse {
exists: 172,
recent: 1,
flags: (
Flag::REPLIED | Flag::SEEN | Flag::TRASHED | Flag::DRAFT | Flag::FLAGGED,
Vec::new()
),
first_unseen: 12,
uidvalidity: 3857529045,
uidnext: 4392,
permanentflags: (Flag::SEEN | Flag::TRASHED, vec!["*".into()]),
can_create_flags: true,
read_only: false,
highestmodseq: Some(Ok(ModSequence(
std::num::NonZeroU64::new(715194045007_u64).unwrap()
))),
}
);
let r = b"* 172 EXISTS\r\n* 1 RECENT\r\n* OK [UNSEEN 12] Message 12 is first unseen\r\n* OK [UIDVALIDITY 3857529045] UIDs valid\r\n* OK [UIDNEXT 4392] Predicted next UID\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n* OK [NOMODSEQ] Sorry, this mailbox format doesn't support modsequences\r\n* A142 OK [READ-WRITE] SELECT completed\r\n";
assert_eq!(
select_response(r).expect("Could not parse IMAP select response"),
SelectResponse {
exists: 172,
recent: 1,
flags: (
Flag::REPLIED | Flag::SEEN | Flag::TRASHED | Flag::DRAFT | Flag::FLAGGED,
Vec::new()
),
first_unseen: 12,
uidvalidity: 3857529045,
uidnext: 4392,
permanentflags: (Flag::SEEN | Flag::TRASHED, vec!["*".into()]),
can_create_flags: true,
read_only: false,
highestmodseq: Some(Err(())),
}
);
}
pub fn flags(input: &[u8]) -> IResult<&[u8], (Flag, Vec<String>)> {
let mut ret = Flag::default();
let mut keywords = Vec::new();
let mut input = input;
while !input.starts_with(b")") && !input.is_empty() {
let is_system_flag = input.starts_with(b"\\");
if is_system_flag {
input = &input[1..];
}
let mut match_end = 0;
while match_end < input.len() {
if input[match_end..].starts_with(b" ") || input[match_end..].starts_with(b")") {
break;
}
match_end += 1;
}
match (is_system_flag, &input[..match_end]) {
(true, t) if t.eq_ignore_ascii_case(b"Answered") => {
ret.set(Flag::REPLIED, true);
}
(true, t) if t.eq_ignore_ascii_case(b"Flagged") => {
ret.set(Flag::FLAGGED, true);
}
(true, t) if t.eq_ignore_ascii_case(b"Deleted") => {
ret.set(Flag::TRASHED, true);
}
(true, t) if t.eq_ignore_ascii_case(b"Seen") => {
ret.set(Flag::SEEN, true);
}
(true, t) if t.eq_ignore_ascii_case(b"Draft") => {
ret.set(Flag::DRAFT, true);
}
(true, t) if t.eq_ignore_ascii_case(b"Recent") => { /* ignore */ }
(_, f) => {
keywords.push(String::from_utf8_lossy(f).into());
}
}
input = &input[match_end..];
input = input.trim_start();
}
Ok((input, (ret, keywords)))
}
pub fn byte_flags(input: &[u8]) -> IResult<&[u8], (Flag, Vec<String>)> {
match flags(input) {
Ok((rest, ret)) => Ok((rest, ret)),
Err(nom::Err::Error(err)) => Err(nom::Err::Error(err)),
Err(nom::Err::Failure(err)) => Err(nom::Err::Error(err)),
Err(nom::Err::Incomplete(_)) => {
Err(nom::Err::Error((input, "byte_flags(): incomplete").into()))
}
}
}
/*
* The fields of the envelope structure are in the following
* order: date, subject, from, sender, reply-to, to, cc, bcc,
* in-reply-to, and message-id. The date, subject, in-reply-to,
* and message-id fields are strings. The from, sender, reply-to,
* to, cc, and bcc fields are parenthesized lists of address
* structures.
* An address structure is a parenthesized list that describes an
* electronic mail address. The fields of an address structure
* are in the following order: personal name, [SMTP]
* at-domain-list (source route), mailbox name, and host name.
*/
/*
* * 12 FETCH (FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700"
* RFC822.SIZE 4286 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>")
*/
pub fn envelope(input: &[u8]) -> IResult<&[u8], Envelope> {
let (input, _) = tag("(")(input)?;
let (input, _) = opt(is_a("\r\n\t "))(input)?;
let (input, date) = quoted_or_nil(input)?;
let (input, _) = opt(is_a("\r\n\t "))(input)?;
let (input, subject) = quoted_or_nil(input)?;
let (input, _) = opt(is_a("\r\n\t "))(input)?;
let (input, from) = envelope_addresses(input)?;
let (input, _) = opt(is_a("\r\n\t "))(input)?;
let (input, _sender) = envelope_addresses(input)?;
let (input, _) = opt(is_a("\r\n\t "))(input)?;
let (input, _reply_to) = envelope_addresses(input)?;
let (input, _) = opt(is_a("\r\n\t "))(input)?;
let (input, to) = envelope_addresses(input)?;
let (input, _) = opt(is_a("\r\n\t "))(input)?;
let (input, cc) = envelope_addresses(input)?;
let (input, _) = opt(is_a("\r\n\t "))(input)?;
let (input, bcc) = envelope_addresses(input)?;
let (input, _) = opt(is_a("\r\n\t "))(input)?;
let (input, in_reply_to) = quoted_or_nil(input)?;
let (input, _) = opt(is_a("\r\n\t "))(input)?;
let (input, message_id) = quoted_or_nil(input)?;
let (input, _) = opt(is_a("\r\n\t "))(input)?;
let (input, _) = tag(")")(input)?;
Ok((
input,
({
let mut env = Envelope::new(EnvelopeHash::default());
if let Some(date) = date {
env.set_date(&date);
if let Ok(d) =
crate::email::parser::dates::rfc5322_date(env.date_as_str().as_bytes())
{
env.set_datetime(d);
}
}
if let Some(subject) = subject {
env.set_subject(subject.to_vec());
}
if let Some(from) = from {
env.set_from(from);
}
if let Some(to) = to {
env.set_to(to);
}
if let Some(cc) = cc {
env.set_cc(cc);
}
if let Some(bcc) = bcc {
env.set_bcc(bcc.to_vec());
}
if let Some(in_reply_to) = in_reply_to {
env.set_in_reply_to(&in_reply_to);
if let Some(in_reply_to) = env.in_reply_to().cloned() {
env.push_references(in_reply_to);
}
}
if let Some(message_id) = message_id {
env.set_message_id(&message_id);
}
env
}),
))
}
#[test]
fn test_imap_envelope() {
let input: &[u8] = b"(\"Fri, 24 Jun 2011 10:09:10 +0000\" \"xxxx/xxxx\" ((\"xx@xx.com\" NIL \"xx\" \"xx.com\")) NIL NIL ((\"xx@xx\" NIL \"xx\" \"xx.com\")) ((\"'xx, xx'\" NIL \"xx.xx\" \"xx.com\") (\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\") (\"'xx'\" NIL \"xx.xx\" \"xx.com\") (\"'xx xx'\" NIL \"xx.xx\" \"xx.com\") (\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\")) NIL NIL \"<xx@xx.com>\")";
_ = envelope(input).unwrap();
}
/* Helper to build StrBuilder for Address structs */
macro_rules! str_builder {
($offset:expr, $length:expr) => {
StrBuilder {
offset: $offset,
length: $length,
}
};
}
// Parse a list of addresses in the format of the ENVELOPE structure
pub fn envelope_addresses<'a>(
input: &'a [u8],
) -> IResult<&'a [u8], Option<SmallVec<[Address; 1]>>> {
alt((
map(tag("NIL"), |_| None),
|input: &'a [u8]| -> IResult<&'a [u8], Option<SmallVec<[Address; 1]>>> {
let (input, _) = tag("(")(input)?;
let (input, envelopes) = fold_many1(
delimited(tag("("), envelope_address, alt((tag(") "), tag(")")))),
SmallVec::new,
|mut acc, item| {
acc.push(item);
acc
},
)(input.ltrim())?;
let (input, _) = tag(")")(input)?;
Ok((input, Some(envelopes)))
},
map(tag("\"\""), |_| None),
))(input)
}
// Parse an address in the format of the ENVELOPE structure eg
// ("Terry Gray" NIL "gray" "cac.washington.edu")
pub fn envelope_address(input: &[u8]) -> IResult<&[u8], Address> {
let (input, name) = alt((quoted, map(tag("NIL"), |_| Vec::new())))(input)?;
let (input, _) = is_a("\r\n\t ")(input)?;
let (input, _) = alt((quoted, map(tag("NIL"), |_| Vec::new())))(input)?;
let (input, _) = is_a("\r\n\t ")(input)?;
let (input, mailbox_name) = alt((quoted, map(tag("NIL"), |_| Vec::new())))(input)?;
let (input, host_name) = opt(preceded(
is_a("\r\n\t "),
alt((quoted, map(tag("NIL"), |_| Vec::new()))),
))(input)?;
Ok((
input,
Address::Mailbox(MailboxAddress {
raw: if let Some(host_name) = host_name.as_ref() {
format!(
"{}{}<{}@{}>",
to_str!(&name),
if name.is_empty() { "" } else { " " },
to_str!(&mailbox_name),
to_str!(host_name)
)
.into_bytes()
} else {
format!(
"{}{}{}",
to_str!(&name),
if name.is_empty() { "" } else { " " },
to_str!(&mailbox_name),
)
.into_bytes()
},
display_name: str_builder!(0, name.len()),
address_spec: if let Some(host_name) = host_name.as_ref() {
str_builder!(
if name.is_empty() { 1 } else { name.len() + 2 },
mailbox_name.len() + host_name.len() + 1
)
} else {
str_builder!(
if name.is_empty() { 0 } else { name.len() + 1 },
mailbox_name.len()
)
},
}),
))
}
// Read a literal ie a byte sequence prefixed with a tag containing its length
// delimited in {}s
pub fn literal(input: &[u8]) -> IResult<&[u8], &[u8]> {
length_data(delimited(
tag("{"),
map_res(digit1, |s| {
usize::from_str(unsafe { std::str::from_utf8_unchecked(s) })
}),
tag("}\r\n"),
))(input)
}
// Return a byte sequence surrounded by "s and decoded if necessary
pub fn quoted(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
if let Ok((r, o)) = literal(input) {
return match crate::email::parser::encodings::phrase(o, false) {
Ok((_, out)) => Ok((r, out)),
e => e,
};
}
if input.is_empty() || input[0] != b'"' {
return Err(nom::Err::Error((input, "quoted(): EOF").into()));
}
let mut i = 1;
while i < input.len() {
if input[i] == b'\"' && input[i - 1] != b'\\' {
return match crate::email::parser::encodings::phrase(&input[1..i], false) {
Ok((_, out)) => Ok((&input[i + 1..], out)),
e => e,
};
}
i += 1;
}
Err(nom::Err::Error(
(input, "quoted(): not a quoted phrase").into(),
))
}
pub fn quoted_or_nil(input: &[u8]) -> IResult<&[u8], Option<Vec<u8>>> {
alt((map(tag("NIL"), |_| None), map(quoted, Some)))(input.ltrim())
}
pub fn uid_fetch_envelopes_response<'a>(
input: &'a [u8],
) -> IResult<&'a [u8], Vec<(UID, Option<(Flag, Vec<String>)>, Envelope)>> {
many0(
|input: &'a [u8]| -> IResult<&'a [u8], (UID, Option<(Flag, Vec<String>)>, Envelope)> {
let (input, _) = tag("* ")(input)?;
let (input, _) = take_while(is_digit)(input)?;
let (input, _) = tag(" FETCH (")(input)?;
let (input, uid_flags) = permutation((
preceded(
alt((tag("UID "), tag(" UID "))),
map_res(digit1, |s| {
UID::from_str(unsafe { std::str::from_utf8_unchecked(s) })
}),
),
opt(preceded(
alt((tag("FLAGS "), tag(" FLAGS "))),
delimited(tag("("), byte_flags, tag(")")),
)),
))(input.ltrim())?;
let (input, _) = tag(" ENVELOPE ")(input)?;
let (input, env) = envelope(input.ltrim())?;
let (input, _) = tag("BODYSTRUCTURE ")(input)?;
let (input, has_attachments) = bodystructure_has_attachments(input)?;
let (input, _) = tag(")\r\n")(input)?;
Ok((input, {
let mut env = env;
env.set_has_attachments(has_attachments);
(uid_flags.0, uid_flags.1, env)
}))
},
)(input)
}
pub fn bodystructure_has_attachments(input: &[u8]) -> IResult<&[u8], bool> {
let (input, _) = eat_whitespace(input)?;
let (input, _) = tag("(")(input)?;
let (mut input, _) = eat_whitespace(input)?;
let mut has_attachments = false;
let mut first_in_line = true;
while !input.is_empty() && !input.starts_with(b")") {
if input.starts_with(b"\"") || input[0].is_ascii_alphanumeric() || input[0] == b'{' {
let (_input, token) = astring_token(input)?;
input = _input;
if first_in_line {
has_attachments |= token.eq_ignore_ascii_case(b"attachment");
}
} else if input.starts_with(b"(") {
let (_input, _has_attachments) = bodystructure_has_attachments(input)?;
has_attachments |= _has_attachments;
input = _input;
}
let (_input, _) = eat_whitespace(input)?;
input = _input;
first_in_line = false;
}
let (input, _) = tag(")")(input)?;
Ok((input, has_attachments))
}
fn eat_whitespace(mut input: &[u8]) -> IResult<&[u8], ()> {
while !input.is_empty() {
if input[0] == b' ' || input[0] == b'\n' || input[0] == b'\t' {
input = &input[1..];
} else if input.starts_with(b"\r\n") {
input = &input[2..];
} else {
break;
}
}
Ok((input, ()))
}
#[derive(Debug, Default, Clone)]
pub struct StatusResponse {
pub mailbox: Option<MailboxHash>,
pub messages: Option<ImapNum>,
pub recent: Option<ImapNum>,
pub uidnext: Option<UID>,
pub uidvalidity: Option<UID>,
pub unseen: Option<ImapNum>,
}
// status = "STATUS" SP mailbox SP "(" status-att *(SP status-att) ")"
// status-att = "MESSAGES" / "RECENT" / "UIDNEXT" / "UIDVALIDITY" / "UNSEEN"
//* STATUS INBOX (MESSAGES 1057 UNSEEN 0)
pub fn status_response(input: &[u8]) -> IResult<&[u8], StatusResponse> {
let (input, _) = tag("* STATUS ")(input)?;
let (input, mailbox) = take_until(" (")(input)?;
let mailbox = mailbox_token(mailbox)
.map(|(_, m)| MailboxHash::from_bytes(m.as_bytes()))
.ok();
let (input, _) = tag(" (")(input)?;
let (input, result) = permutation((
opt(preceded(
alt((tag("MESSAGES "), tag(" MESSAGES "))),
map_res(digit1, |s| {
ImapNum::from_str(unsafe { std::str::from_utf8_unchecked(s) })
}),
)),
opt(preceded(
alt((tag("RECENT "), tag(" RECENT "))),
map_res(digit1, |s| {
ImapNum::from_str(unsafe { std::str::from_utf8_unchecked(s) })
}),
)),
opt(preceded(
alt((tag("UIDNEXT "), tag(" UIDNEXT "))),
map_res(digit1, |s| {
UID::from_str(unsafe { std::str::from_utf8_unchecked(s) })
}),
)),
opt(preceded(
alt((tag("UIDVALIDITY "), tag(" UIDVALIDITY "))),
map_res(digit1, |s| {
UIDVALIDITY::from_str(unsafe { std::str::from_utf8_unchecked(s) })
}),
)),
opt(preceded(
alt((tag("UNSEEN "), tag(" UNSEEN "))),
map_res(digit1, |s| {
ImapNum::from_str(unsafe { std::str::from_utf8_unchecked(s) })
}),
)),
))(input)?;
let (input, _) = tag(")\r\n")(input)?;
Ok((
input,
StatusResponse {
mailbox,
messages: result.0,
recent: result.1,
uidnext: result.2,
uidvalidity: result.3,
unseen: result.4,
},
))
}
// mailbox = "INBOX" / astring
// ; INBOX is case-insensitive. All case variants of
// ; INBOX (e.g., "iNbOx") MUST be interpreted as INBOX
// ; not as an astring. An astring which consists of
// ; the case-insensitive sequence "I" "N" "B" "O" "X"
// ; is considered to be INBOX and not an astring.
// ; Refer to section 5.1 for further
// ; semantic details of mailbox names.
pub fn mailbox_token(input: &'_ [u8]) -> IResult<&'_ [u8], std::borrow::Cow<'_, str>> {
let (input, astring) = astring_token(input)?;
if astring.eq_ignore_ascii_case(b"INBOX") {
return Ok((input, "INBOX".into()));
}
Ok((input, String::from_utf8_lossy(astring)))
}
// astring = 1*ASTRING-CHAR / string
pub fn astring_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
alt((string_token, astring_char))(input)
}
// string = quoted / literal
pub fn string_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
if let Ok((r, o)) = literal(input) {
return Ok((r, o));
}
if input.is_empty() || input[0] != b'"' {
return Err(nom::Err::Error((input, "string_token(): EOF").into()));
}
let mut i = 1;
while i < input.len() {
if input[i] == b'\"' && input[i - 1] != b'\\' {
return Ok((&input[i + 1..], &input[1..i]));
}
i += 1;
}
Err(nom::Err::Error(
(input, "string_token(): not a quoted phrase").into(),
))
}
// ASTRING-CHAR = ATOM-CHAR / resp-specials
// atom = 1*ATOM-CHAR
// ATOM-CHAR = <any CHAR except atom-specials>
// atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards / quoted-specials
// / resp-specials
fn astring_char(input: &[u8]) -> IResult<&[u8], &[u8]> {
let (rest, chars) = many1(atom_char)(input)?;
Ok((rest, &input[0..chars.len()]))
}
fn atom_char(mut input: &[u8]) -> IResult<&[u8], u8> {
if input.is_empty() {
return Err(nom::Err::Error(
(input, "astring_char_tokens(): EOF").into(),
));
}
if atom_specials(input).is_ok() {
return Err(nom::Err::Error(
(input, "astring_char_tokens(): invalid input").into(),
));
}
let ret = input[0];
input = &input[1..];
Ok((input, ret))
}
#[inline(always)]
fn atom_specials(input: &[u8]) -> IResult<&[u8], u8> {
alt((
raw_chars,
ctl,
list_wildcards,
quoted_specials,
resp_specials,
))(input)
}
#[inline(always)]
fn raw_chars(input: &[u8]) -> IResult<&[u8], u8> {
byte_in_slice(&[b'(', b')', b'{', b' '])(input)
}
#[inline(always)]
fn list_wildcards(input: &[u8]) -> IResult<&[u8], u8> {
byte_in_slice(&[b'%', b'*'])(input)
}
#[inline(always)]
fn quoted_specials(input: &[u8]) -> IResult<&[u8], u8> {
byte_in_slice(&[b'"', b'\\'])(input)
}
#[inline(always)]
fn resp_specials(input: &[u8]) -> IResult<&[u8], u8> {
byte_in_slice(&[b']'])(input)
}
#[inline(always)]
fn ctl(input: &[u8]) -> IResult<&[u8], u8> {
//U+0000—U+001F (C0 controls), U+007F (delete), and U+0080—U+009F (C1 controls
alt((
byte_in_range(0, 0x1f),
byte_in_range(0x7f, 0x7f),
byte_in_range(0x80, 0x9f),
))(input)
}
pub fn generate_envelope_hash(mailbox_path: &str, uid: &UID) -> EnvelopeHash {
let mut h = DefaultHasher::new();
h.write_usize(*uid);
h.write(mailbox_path.as_bytes());
EnvelopeHash(h.finish())
}