/*
* 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 .
*/
use std::{convert::TryFrom, str::FromStr};
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)>;
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),
/// 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 {
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> 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: (
{
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,
pub message_sequence_number: MessageSequenceNumber,
pub modseq: Option,
pub flags: Option<(Flag, Vec)>,
pub body: Option<&'a [u8]>,
pub references: Option<&'a [u8]>,
pub envelope: Option,
pub raw_fetch_value: &'a [u8],
}
pub fn fetch_response(input: &[u8]) -> ImapParseResult> {
macro_rules! should_start_with {
($input:expr, $tag:literal) => {
if !$input.starts_with($tag) {
return Err(Error::new(format!(
"Expected `{}` but got `{:.50}`",
String::from_utf8_lossy($tag),
String::from_utf8_lossy(&$input)
)));
}
};
}
should_start_with!(input, b"* ");
let mut i = b"* ".len();
macro_rules! bounds {
() => {
if i == input.len() {
return Err(Error::new(format!(
"Expected more input. Got: `{:.50}`",
String::from_utf8_lossy(&input)
)));
}
};
(break) => {
if i == input.len() {
break;
}
};
}
macro_rules! eat_whitespace {
() => {
while (input[i] as char).is_whitespace() {
i += 1;
bounds!();
}
};
(break) => {
while (input[i] as char).is_whitespace() {
i += 1;
bounds!(break);
}
};
}
let mut ret = FetchResponse {
uid: None,
message_sequence_number: 0,
modseq: None,
flags: None,
body: None,
references: None,
envelope: None,
raw_fetch_value: &[],
};
while input[i].is_ascii_digit() {
let b: u8 = input[i] - 0x30;
ret.message_sequence_number *= 10;
ret.message_sequence_number += b as MessageSequenceNumber;
i += 1;
bounds!();
}
eat_whitespace!();
should_start_with!(&input[i..], b"FETCH (");
i += b"FETCH (".len();
let mut has_attachments = false;
while i < input.len() {
eat_whitespace!(break);
bounds!(break);
if input[i..].starts_with(b"UID ") {
i += b"UID ".len();
if let Ok((rest, uid)) =
take_while::<_, &[u8], (&[u8], nom::error::ErrorKind)>(is_digit)(&input[i..])
{
i += input.len() - i - rest.len();
ret.uid =
Some(UID::from_str(unsafe { std::str::from_utf8_unchecked(uid) }).unwrap());
} else {
return debug!(Err(Error::new(format!(
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(input)
))));
}
} else if input[i..].starts_with(b"FLAGS (") {
i += b"FLAGS (".len();
if let Ok((rest, flags)) = flags(&input[i..]) {
ret.flags = Some(flags);
i += (input.len() - i - rest.len()) + 1;
} else {
return debug!(Err(Error::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse FLAGS: \
{:.40}.",
String::from_utf8_lossy(&input[i..])
))));
}
} else if input[i..].starts_with(b"MODSEQ (") {
i += b"MODSEQ (".len();
if let Ok((rest, modseq)) =
take_while::<_, &[u8], (&[u8], nom::error::ErrorKind)>(is_digit)(&input[i..])
{
i += (input.len() - i - rest.len()) + 1;
ret.modseq = u64::from_str(to_str!(modseq))
.ok()
.and_then(std::num::NonZeroU64::new)
.map(ModSequence);
} else {
return debug!(Err(Error::new(format!(
"Unexpected input while parsing MODSEQ in UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(input)
))));
}
} else if input[i..].starts_with(b"RFC822 {") {
i += b"RFC822 ".len();
if let Ok((rest, body)) =
length_data::<_, _, (&[u8], nom::error::ErrorKind), _>(delimited(
tag("{"),
map_res(digit1, |s| {
usize::from_str(unsafe { std::str::from_utf8_unchecked(s) })
}),
tag("}\r\n"),
))(&input[i..])
{
ret.body = Some(body);
i += input.len() - i - rest.len();
} else {
return debug!(Err(Error::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse RFC822: \
{:.40}",
String::from_utf8_lossy(&input[i..])
))));
}
} else if input[i..].starts_with(b"ENVELOPE (") {
i += b"ENVELOPE ".len();
if let Ok((rest, envelope)) = envelope(&input[i..]) {
ret.envelope = Some(envelope);
i += input.len() - i - rest.len();
} else {
return debug!(Err(Error::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse ENVELOPE: \
{:.40}",
String::from_utf8_lossy(&input[i..])
))));
}
} else if input[i..].starts_with(b"BODYSTRUCTURE ") {
i += b"BODYSTRUCTURE ".len();
let (rest, _has_attachments) = bodystructure_has_attachments(&input[i..])?;
has_attachments = _has_attachments;
i += input[i..].len() - rest.len();
} else if input[i..].starts_with(b"BODY[HEADER.FIELDS (REFERENCES)] ") {
i += b"BODY[HEADER.FIELDS (REFERENCES)] ".len();
if let Ok((rest, mut references)) = astring_token(&input[i..]) {
if !references.trim().is_empty() {
if let Ok((_, (_, v))) = crate::email::parser::headers::header(references) {
references = v;
}
ret.references = Some(references);
}
i += input.len() - i - rest.len();
} else {
return debug!(Err(Error::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse \
BODY[HEADER.FIELDS (REFERENCES)]: {:.40}",
String::from_utf8_lossy(&input[i..])
))));
}
} else if input[i..].starts_with(b"BODY[HEADER.FIELDS (\"REFERENCES\")] ") {
i += b"BODY[HEADER.FIELDS (\"REFERENCES\")] ".len();
if let Ok((rest, mut references)) = astring_token(&input[i..]) {
if !references.trim().is_empty() {
if let Ok((_, (_, v))) = crate::email::parser::headers::header(references) {
references = v;
}
ret.references = Some(references);
}
i += input.len() - i - rest.len();
} else {
return debug!(Err(Error::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse \
BODY[HEADER.FIELDS (\"REFERENCES\"): {:.40}",
String::from_utf8_lossy(&input[i..])
))));
}
} else if input[i..].starts_with(b")\r\n") {
i += b")\r\n".len();
break;
} else {
debug!(
"Got unexpected token while parsing UID FETCH response:\n`{}`\n",
String::from_utf8_lossy(input)
);
return debug!(Err(Error::new(format!(
"Got unexpected token while parsing UID FETCH response: `{:.40}`",
String::from_utf8_lossy(&input[i..])
))));
}
}
ret.raw_fetch_value = &input[..i];
if let Some(env) = ret.envelope.as_mut() {
env.set_has_attachments(has_attachments);
}
Ok((&input[i..], ret, None))
}
pub fn fetch_responses(mut input: &[u8]) -> ImapParseResult>> {
let mut ret = Vec::new();
let mut alert: Option = 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))>> {
many0(uid_fetch_flags_response)(input)
}
pub fn uid_fetch_flags_response(input: &[u8]) -> IResult<&[u8], (UID, (Flag, Vec))> {
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