/* * 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>> { 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() { 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 \"\") BODY[HEADER.FIELDS (REFERENCES)] {2}\r\n\r\nBODYSTRUCTURE ((\"text\" \"html\" (\"charset\" \"us-ascii\") \"\" NIL \"7BIT\" 17236 232 NIL NIL NIL NIL)(\"image\" \"jpeg\" (\"name\" \"image001.jpg\") \"\" \"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"; 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("".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, envelope: Some(env), raw_fetch_value: input, }, None ) ); } pub fn search_results<'a>(input: &'a [u8]) -> IResult<&'a [u8], Vec> { alt(( |input: &'a [u8]| -> IResult<&'a [u8], Vec> { 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> { 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), pub first_unseen: MessageSequenceNumber, pub uidvalidity: UIDVALIDITY, pub uidnext: UID, pub permanentflags: (Flag, Vec), /// if SELECT returns \* we can set arbritary flags permanently. pub can_create_flags: bool, pub read_only: bool, pub highestmodseq: Option>, } /* * 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 { 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)> { 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)> { 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 * "") */ 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 \"\")"; _ = 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>> { alt(( map(tag("NIL"), |_| None), |input: &'a [u8]| -> IResult<&'a [u8], Option>> { 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> { 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>> { 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)>, Envelope)>> { many0( |input: &'a [u8]| -> IResult<&'a [u8], (UID, Option<(Flag, Vec)>, 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, pub messages: Option, pub recent: Option, pub uidnext: Option, pub uidvalidity: Option, pub unseen: Option, } // 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 = // 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()) }