diff --git a/imap/src/proto/mod.rs b/imap/src/proto/mod.rs index 71054e1..5f629da 100644 --- a/imap/src/proto/mod.rs +++ b/imap/src/proto/mod.rs @@ -8,6 +8,7 @@ pub mod command; pub mod response; // parsers +#[macro_use] pub mod parsers; pub mod rfc2234; pub mod rfc3501; diff --git a/imap/src/proto/parsers.rs b/imap/src/proto/parsers.rs index afeef71..4ef6211 100644 --- a/imap/src/proto/parsers.rs +++ b/imap/src/proto/parsers.rs @@ -1,12 +1,34 @@ use std::ops::RangeFrom; use nom::{ - error::{ErrorKind, ParseError}, + error::{Error, ErrorKind, ParseError}, Err, IResult, InputIter, Needed, Parser, Slice, }; -pub fn skip(mut f: F) -> impl FnMut(I) -> IResult +macro_rules! sep_list { + ($t:expr) => { + map(pair($t, many0(preceded(crate::proto::rfc2234::SP, $t))), + |(hd, mut tl)| { tl.insert(0, hd); tl }) + }; + ($t:expr, $d:expr) => { + map(pair($t, many0(preceded($d, $t))), + |(hd, mut tl)| { tl.insert(0, hd); tl }) + }; + (? $t:expr) => { + map(opt(pair($t, many0(preceded(crate::proto::rfc2234::SP, $t)))), + |opt| opt.map(|(hd, mut tl)| { tl.insert(0, hd); tl }).unwrap_or_else(Vec::new)) + }; + (? $t:expr, $d:expr) => { + map(opt(pair($t, many0(preceded($d, $t)))), + |opt| opt.map(|(hd, mut tl)| { tl.insert(0, hd); tl }).unwrap_or_else(Vec::new)) + }; +} + +pub fn never(i: I) -> IResult { Err(Err::Error(Error::new(i, ErrorKind::Not))) } + +pub fn skip(mut f: F) -> impl FnMut(I) -> IResult where +I: Clone, F: Parser, { move |i: I| match f.parse(i.clone()) { diff --git a/imap/src/proto/response.rs b/imap/src/proto/response.rs index 66b9904..69130c7 100644 --- a/imap/src/proto/response.rs +++ b/imap/src/proto/response.rs @@ -1,17 +1,43 @@ use std::borrow::Cow; +pub type Atom<'a> = Cow<'a, [u8]>; +pub type CowU8<'a> = Cow<'a, [u8]>; + #[derive(Clone, Debug)] -pub struct Tag(pub String); +pub struct Tag<'a>(pub CowU8<'a>); #[derive(Debug)] #[non_exhaustive] pub enum Response<'a> { + Capabilities(Vec>), + Continue(ResponseText<'a>), + Condition(Condition<'a>), Done(ResponseDone<'a>), + MailboxData(MailboxData<'a>), + Fetch(u32, Vec>), + Expunge(u32), + Fatal(Condition<'a>), + Tagged(Tag<'a>, Condition<'a>), } +#[derive(Debug)] +pub struct ResponseText<'a> { + pub code: Option>, + pub info: CowU8<'a>, +} + +#[derive(Debug)] +pub enum MessageAttribute<'a> { + Flags(Vec>), + Envelope(Envelope), +} + +#[derive(Debug)] +pub struct Envelope {} + #[derive(Debug)] pub struct ResponseDone<'a> { - pub tag: Tag, + pub tag: Tag<'a>, pub status: Status, pub code: Option>, pub info: Option>, @@ -21,7 +47,7 @@ pub struct ResponseDone<'a> { pub struct Condition<'a> { pub status: Status, pub code: Option>, - pub text: String, + pub info: CowU8<'a>, } #[derive(Debug)] @@ -43,6 +69,46 @@ pub enum ResponseCode<'a> { #[derive(Debug)] pub enum Capability<'a> { Imap4rev1, - Auth(Cow<'a, [u8]>), - Atom(Cow<'a, [u8]>), + Auth(Atom<'a>), + Atom(Atom<'a>), +} + +#[derive(Debug)] +pub enum MailboxData<'a> { + Flags(Vec>), + List(MailboxList<'a>), +} + +#[derive(Debug)] +pub enum Mailbox<'a> { + Inbox, + Name(CowU8<'a>), +} + +#[derive(Debug)] +pub enum Flag<'a> { + Answered, + Flagged, + Deleted, + Seen, + Draft, + Recent, + Keyword(Atom<'a>), + Extension(Atom<'a>), +} + +#[derive(Debug)] +pub struct MailboxList<'a> { + pub flags: Vec>, + pub delimiter: Option, + pub mailbox: Mailbox<'a>, +} + +#[derive(Debug)] +pub enum MailboxListFlag<'a> { + NoInferiors, + NoSelect, + Marked, + Unmarked, + Extension(Atom<'a>), } diff --git a/imap/src/proto/rfc2234.rs b/imap/src/proto/rfc2234.rs index e1eceee..9f9c26b 100644 --- a/imap/src/proto/rfc2234.rs +++ b/imap/src/proto/rfc2234.rs @@ -19,7 +19,8 @@ rule!(pub CRLF : (u8, u8) => pair(CR, LF)); pub fn is_ctl(c: u8) -> bool { c <= b'\x1f' || c == b'\x7f' } rule!(pub CTL : u8 => satisfy(is_ctl)); -rule!(pub DIGIT : u8 => satisfy(|c| c >= b'\x30' && c <= b'\x39')); +pub fn is_digit(c: u8) -> bool { c >= b'\x30' && c <= b'\x39' } +rule!(pub DIGIT : u8 => satisfy(is_digit)); pub(crate) fn is_dquote(c: u8) -> bool { c == b'\x22' } rule!(pub DQUOTE : u8 => satisfy(is_dquote)); diff --git a/imap/src/proto/rfc3501.rs b/imap/src/proto/rfc3501.rs index ef5f0bb..19b0ed6 100644 --- a/imap/src/proto/rfc3501.rs +++ b/imap/src/proto/rfc3501.rs @@ -6,27 +6,28 @@ use nom::{ branch::alt, bytes::streaming::{tag_no_case, take, take_while1}, character::streaming::char, - combinator::{map, map_res}, - multi::{many0, many1}, - sequence::{delimited, pair, preceded, separated_pair, terminated}, + combinator::{map, verify, map_res, opt}, + multi::{many0}, + sequence::{delimited, pair, tuple, preceded, separated_pair, terminated}, IResult, }; -use super::parsers::{byte, satisfy}; -use super::response::{Capability, ResponseCode}; -use super::rfc2234::{is_char, is_cr, is_ctl, is_dquote, is_lf, is_sp, CRLF, DIGIT, DQUOTE, SP}; +use super::parsers::{byte, never, satisfy}; +use super::response::{ + Atom, Capability, Condition, CowU8, Flag, Mailbox, MailboxData, MailboxList, MailboxListFlag, + Response, ResponseCode, Status, Tag, MessageAttribute, Envelope, ResponseText, +}; +use super::rfc2234::{is_char, is_cr, is_ctl, is_digit, is_dquote, is_lf, is_sp, CRLF, DQUOTE, SP}; -rule!(pub astring : Vec => alt((many1(ASTRING_CHAR), string))); +rule!(pub astring : CowU8 => alt((map(take_while1(is_astring_char), Cow::from), string))); +pub(crate) fn is_astring_char(c: u8) -> bool { is_atom_char(c) || is_resp_specials(c) } rule!(pub ASTRING_CHAR : u8 => alt((ATOM_CHAR, resp_specials))); -rule!(pub atom : Vec => many1(ATOM_CHAR)); +rule!(pub atom : CowU8 => map(take_while1(is_atom_char), Cow::from)); -// TODO: somehow incorporate CHAR in here? -// technically ATOM_CHAR is defined as -// but "except"-style rules don't really make sense except for character sets -// and some other niche cases so probably doesn't warrant a separate combinator -rule!(pub ATOM_CHAR : u8 => satisfy(pred!((is_char) && (!is_atom_specials)))); +pub(crate) fn is_atom_char(c: u8) -> bool { is_char(c) && !is_atom_specials(c) } +rule!(pub ATOM_CHAR : u8 => satisfy(is_atom_char)); pub(crate) fn is_atom_specials(c: u8) -> bool { c == b'(' @@ -40,7 +41,7 @@ pub(crate) fn is_atom_specials(c: u8) -> bool { } rule!(pub atom_specials : u8 => satisfy(is_atom_specials)); -rule!(pub auth_type : Vec => atom); +rule!(pub auth_type : Atom => atom); rule!(pub capability : Capability => alt(( map(preceded(tag_no_case("AUTH="), auth_type), |s| Capability::Auth(Cow::from(s))), @@ -55,6 +56,28 @@ rule!(pub capability_data : Vec => preceded(tag_no_case("CAPABILITY" ), |(mut a, b)| { a.extend(b); a }) })); +rule!(pub continue_req : Response => delimited(pair(byte(b'+'), SP), + // TODO: handle base64 case? + map(resp_text, Response::Continue), +CRLF)); + +rule!(pub envelope : Envelope => map(byte(b'('), |_| Envelope {})); + +rule!(pub flag : Flag => alt(( + map(tag_no_case("\\Answered"), |_| Flag::Answered), + map(tag_no_case("\\Flagged"), |_| Flag::Flagged), + map(tag_no_case("\\Deleted"), |_| Flag::Deleted), + map(tag_no_case("\\Seen"), |_| Flag::Seen), + map(tag_no_case("\\Draft"), |_| Flag::Draft), + map(flag_extension, Flag::Extension), +))); + +rule!(pub flag_extension : Atom => preceded(byte(b'\\'), atom)); + +rule!(pub flag_fetch : Flag => alt((flag, map(tag_no_case("\\Recent"), |_| Flag::Recent)))); + +rule!(pub flag_list : Vec => delimited(byte(b'('), sep_list!(?flag), byte(b')'))); + pub(crate) fn is_list_wildcards(c: u8) -> bool { c == b'%' || c == b'*' } rule!(pub list_wildcards : u8 => satisfy(is_list_wildcards)); @@ -63,32 +86,96 @@ rule!(pub list_wildcards : u8 => satisfy(is_list_wildcards)); // TODO: Future work, could possibly initialize writing to file if the length is // determined to exceed a certain threshold so we don't have insane amounts of // data in memory -pub fn literal(i: &[u8]) -> IResult<&[u8], Vec> { +pub fn literal(i: &[u8]) -> IResult<&[u8], CowU8> { let mut length_of = terminated(delimited(char('{'), number, char('}')), CRLF); let (i, length) = length_of(i)?; println!("length is: {:?}", (i, length)); - map(take(length), |s: &[u8]| s.to_vec())(i) + map(take(length), Cow::from)(i) } #[test] fn test_literal() { assert_eq!( - literal(b"{13}\r\nHello, world!").unwrap().1, + literal(b"{13}\r\nHello, world!").unwrap().1.as_ref(), b"Hello, world!" ); } +rule!(pub mailbox : Mailbox => alt(( + map(tag_no_case("INBOX"), |_| Mailbox::Inbox), + map(astring, Mailbox::Name), +))); + +rule!(pub mailbox_data : MailboxData => alt(( + map(preceded(pair(tag_no_case("FLAGS"), SP), flag_list), MailboxData::Flags), + map(preceded(pair(tag_no_case("LIST"), SP), mailbox_list), MailboxData::List), +))); + +rule!(pub mailbox_list : MailboxList => map(separated_pair( + delimited(byte(b'('), map(opt(mbx_list_flags), |opt| opt.unwrap_or_else(Vec::new)), byte(b')')), + SP, separated_pair( + alt(( + map(delimited(DQUOTE, QUOTED_CHAR, DQUOTE), Some), + map(nil, |_| None), + )), + SP, mailbox, + ), +), |(flags, (delimiter, mailbox))| MailboxList { flags, delimiter, mailbox })); + +rule!(pub mbx_list_flags : Vec => alt(( + map(tuple(( + many0(terminated(mbx_list_oflag, SP)), + mbx_list_sflag, + many0(preceded(SP, mbx_list_oflag)), + )), |(mut a, b, c)| { a.push(b); a.extend(c); a }), + sep_list!(mbx_list_oflag), +))); + +rule!(pub mbx_list_oflag : MailboxListFlag => alt(( + map(tag_no_case("\\Noinferiors"), |_| MailboxListFlag::NoInferiors), + map(flag_extension, MailboxListFlag::Extension), +))); + +rule!(pub mbx_list_sflag : MailboxListFlag => alt(( + map(tag_no_case("\\NoSelect"), |_| MailboxListFlag::NoSelect), + map(tag_no_case("\\Marked"), |_| MailboxListFlag::Marked), + map(tag_no_case("\\Unmarked"), |_| MailboxListFlag::Unmarked), +))); + +rule!(pub message_data : Response => alt(( + map(terminated(nz_number, pair(SP, tag_no_case("EXPUNGE"))), Response::Expunge), + map(separated_pair(nz_number, SP, preceded(pair(tag_no_case("FETCH"), SP), msg_att)), + |(n, attrs)| Response::Fetch(n, attrs)), +))); + +rule!(pub msg_att : Vec => delimited(byte(b'('), + sep_list!(alt((msg_att_dynamic, msg_att_static))), +byte(b')'))); + +rule!(pub msg_att_dynamic : MessageAttribute => alt(( + map(preceded(pair(tag_no_case("FLAGS"), SP), + delimited(byte(b'('), sep_list!(?flag_fetch), byte(b')'))), MessageAttribute::Flags), + never, +))); + +rule!(pub msg_att_static : MessageAttribute => alt(( + map(preceded(pair(tag_no_case("ENVELOPE"), SP), envelope), MessageAttribute::Envelope), + map(preceded(pair(tag_no_case("ENVELOPE"), SP), envelope), MessageAttribute::Envelope), +))); + rule!(pub nil : &[u8] => tag_no_case("NIL")); -rule!(pub nstring : Option> => alt((map(string, Some), map(nil, |_| None)))); +rule!(pub nstring : Option => alt((map(string, Some), map(nil, |_| None)))); pub(crate) fn number(i: &[u8]) -> IResult<&[u8], u32> { - map_res(map_res(many1(DIGIT), String::from_utf8), |s| { + map_res(map_res(take_while1(is_digit), std::str::from_utf8), |s| { s.parse::() })(i) } -rule!(pub quoted : Vec => delimited(DQUOTE, many0(QUOTED_CHAR), DQUOTE)); +rule!(pub nz_number : u32 => verify(number, |n| *n != 0)); + +rule!(pub quoted : CowU8 => delimited(DQUOTE, map(take_while1(is_quoted_char), Cow::from), DQUOTE)); fn is_quoted_char(c: u8) -> bool { is_char(c) && !is_quoted_specials(c) } rule!(pub QUOTED_CHAR : u8 => alt((satisfy(is_quoted_char), preceded(byte(b'\\'), quoted_specials)))); @@ -96,17 +183,60 @@ rule!(pub QUOTED_CHAR : u8 => alt((satisfy(is_quoted_char), preceded(byte(b'\\') pub(crate) fn is_quoted_specials(c: u8) -> bool { is_dquote(c) || c == b'\\' } rule!(pub quoted_specials : u8 => satisfy(is_quoted_specials)); +// TODO: technically, this is supposed to be +rule!(pub response : Response => alt((continue_req, response_data, response_done))); + +rule!(pub response_data : Response => delimited(pair(byte(b'*'), SP), alt(( + map(resp_cond_state, Response::Condition), + map(resp_cond_bye, Response::Condition), + map(mailbox_data, Response::MailboxData), + message_data, + map(capability_data, Response::Capabilities), +)), CRLF)); + +rule!(pub response_done : Response => alt((response_tagged, response_fatal))); + +rule!(pub response_fatal : Response => delimited(pair(byte(b'*'), SP), + map(resp_cond_bye, Response::Fatal), CRLF)); + +rule!(pub response_tagged : Response => map(terminated(separated_pair(tag, SP, resp_cond_state), CRLF), + |(tag, cond)| Response::Tagged(tag, cond))); + +rule!(pub resp_cond_bye : Condition => preceded(pair(tag_no_case("BYE"), SP), + map(resp_text, |ResponseText { code, info }| Condition { status: Status::Bye, code, info }))); + +rule!(pub resp_cond_state : Condition => map( + separated_pair( + alt(( + map(tag_no_case("OK"), |_| Status::Ok), + map(tag_no_case("NO"), |_| Status::No), + map(tag_no_case("BAD"), |_| Status::Bad), + )), + SP, + resp_text, + ), + |(status, ResponseText { code, info })| Condition { status, code, info } +)); + pub(crate) fn is_resp_specials(c: u8) -> bool { c == b']' } rule!(pub resp_specials : u8 => satisfy(is_resp_specials)); +rule!(pub resp_text : ResponseText => map(pair( + opt(terminated(delimited(byte(b'['), resp_text_code, byte(b']')), SP)), + text, +), |(code, info)| ResponseText { code, info })); + rule!(pub resp_text_code : ResponseCode => alt(( map(tag_no_case("ALERT"), |_| ResponseCode::Alert), map(capability_data, ResponseCode::Capabilities), ))); -rule!(pub string : Vec => alt((quoted, literal))); +rule!(pub string : CowU8 => alt((quoted, literal))); -rule!(pub text : &[u8] => take_while1(is_text_char)); +pub(crate) fn is_tag_char(c: u8) -> bool { is_astring_char(c) && c != b'+' } +rule!(pub tag : Tag => map(take_while1(is_tag_char), |s: &[u8]| Tag(s.into()))); + +rule!(pub text : CowU8 => map(take_while1(is_text_char), Cow::from)); pub(crate) fn is_text_char(c: u8) -> bool { is_char(c) && !is_cr(c) && !is_lf(c) } rule!(pub TEXT_CHAR : u8 => satisfy(is_text_char));