/*
* 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 super::*;
use crate::email::address::{Address, MailboxAddress};
use crate::email::parser::{
generic::{byte_in_range, byte_in_slice},
BytesExt, IResult,
};
use crate::error::ResultIntoMeliError;
use crate::get_path_hash;
use nom::{
branch::{alt, permutation},
bytes::complete::{is_a, is_not, tag, take, take_until, take_while},
character::complete::digit1,
character::is_digit,
combinator::{map, map_res, opt},
multi::{fold_many1, length_data, many0, many1, separated_nonempty_list},
sequence::{delimited, preceded},
};
use std::convert::TryFrom;
use std::str::FromStr;
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)]
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 = MeliError;
fn try_from(val: &'_ [u8]) -> Result {
let val: &[u8] = val.split_rn().last().unwrap_or_else(|| val.as_ref());
let mut val = val[val.find(b" ").ok_or_else(|| {
MeliError::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(|| {
MeliError::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(MeliError::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(MeliError::new(msg))
}
Self::No(err) => Err(MeliError::new(format!("{:?}", err)))
.chain_err_summary(|| "IMAP NO Response.".to_string()),
Self::Bad(err) => Err(MeliError::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 = get_path_hash!(&f.imap_path);
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(get_path_hash!(&f.imap_path[..pos]));
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(MeliError::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(MeliError::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(MeliError::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(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(input)
))));
}
} 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(MeliError::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(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(input)
))));
}
} 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(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Got: `{:.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(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Got: `{:.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(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Got: `{:.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(MeliError::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(MeliError::new(format!(
"Unexpected input while parsing UID FETCH responses: `{:.40}`, {}",
String::from_utf8_lossy(&input),
err
)));
}
}
}
if !input.is_empty() && ret.is_empty() {
if let Ok(ImapResponse::Ok(_)) = ImapResponse::try_from(input) {
} else {
return Err(MeliError::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_nonempty_list(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