response code

This commit is contained in:
Michael Zhang 2021-08-08 18:37:03 -05:00
parent a22a2ee35c
commit 3cc65d5ce7
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
5 changed files with 250 additions and 30 deletions

View file

@ -8,6 +8,7 @@ pub mod command;
pub mod response; pub mod response;
// parsers // parsers
#[macro_use]
pub mod parsers; pub mod parsers;
pub mod rfc2234; pub mod rfc2234;
pub mod rfc3501; pub mod rfc3501;

View file

@ -1,12 +1,34 @@
use std::ops::RangeFrom; use std::ops::RangeFrom;
use nom::{ use nom::{
error::{ErrorKind, ParseError}, error::{Error, ErrorKind, ParseError},
Err, IResult, InputIter, Needed, Parser, Slice, Err, IResult, InputIter, Needed, Parser, Slice,
}; };
pub fn skip<E, F, I: Clone, O>(mut f: F) -> impl FnMut(I) -> IResult<I, (), E> 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, O>(i: I) -> IResult<I, O> { Err(Err::Error(Error::new(i, ErrorKind::Not))) }
pub fn skip<E, F, I, O>(mut f: F) -> impl FnMut(I) -> IResult<I, (), E>
where where
I: Clone,
F: Parser<I, O, E>, F: Parser<I, O, E>,
{ {
move |i: I| match f.parse(i.clone()) { move |i: I| match f.parse(i.clone()) {

View file

@ -1,17 +1,43 @@
use std::borrow::Cow; use std::borrow::Cow;
pub type Atom<'a> = Cow<'a, [u8]>;
pub type CowU8<'a> = Cow<'a, [u8]>;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Tag(pub String); pub struct Tag<'a>(pub CowU8<'a>);
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive] #[non_exhaustive]
pub enum Response<'a> { pub enum Response<'a> {
Capabilities(Vec<Capability<'a>>),
Continue(ResponseText<'a>),
Condition(Condition<'a>),
Done(ResponseDone<'a>), Done(ResponseDone<'a>),
MailboxData(MailboxData<'a>),
Fetch(u32, Vec<MessageAttribute<'a>>),
Expunge(u32),
Fatal(Condition<'a>),
Tagged(Tag<'a>, Condition<'a>),
} }
#[derive(Debug)]
pub struct ResponseText<'a> {
pub code: Option<ResponseCode<'a>>,
pub info: CowU8<'a>,
}
#[derive(Debug)]
pub enum MessageAttribute<'a> {
Flags(Vec<Flag<'a>>),
Envelope(Envelope),
}
#[derive(Debug)]
pub struct Envelope {}
#[derive(Debug)] #[derive(Debug)]
pub struct ResponseDone<'a> { pub struct ResponseDone<'a> {
pub tag: Tag, pub tag: Tag<'a>,
pub status: Status, pub status: Status,
pub code: Option<ResponseCode<'a>>, pub code: Option<ResponseCode<'a>>,
pub info: Option<Cow<'a, str>>, pub info: Option<Cow<'a, str>>,
@ -21,7 +47,7 @@ pub struct ResponseDone<'a> {
pub struct Condition<'a> { pub struct Condition<'a> {
pub status: Status, pub status: Status,
pub code: Option<ResponseCode<'a>>, pub code: Option<ResponseCode<'a>>,
pub text: String, pub info: CowU8<'a>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -43,6 +69,46 @@ pub enum ResponseCode<'a> {
#[derive(Debug)] #[derive(Debug)]
pub enum Capability<'a> { pub enum Capability<'a> {
Imap4rev1, Imap4rev1,
Auth(Cow<'a, [u8]>), Auth(Atom<'a>),
Atom(Cow<'a, [u8]>), Atom(Atom<'a>),
}
#[derive(Debug)]
pub enum MailboxData<'a> {
Flags(Vec<Flag<'a>>),
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<MailboxListFlag<'a>>,
pub delimiter: Option<u8>,
pub mailbox: Mailbox<'a>,
}
#[derive(Debug)]
pub enum MailboxListFlag<'a> {
NoInferiors,
NoSelect,
Marked,
Unmarked,
Extension(Atom<'a>),
} }

View file

@ -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' } pub fn is_ctl(c: u8) -> bool { c <= b'\x1f' || c == b'\x7f' }
rule!(pub CTL : u8 => satisfy(is_ctl)); 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' } pub(crate) fn is_dquote(c: u8) -> bool { c == b'\x22' }
rule!(pub DQUOTE : u8 => satisfy(is_dquote)); rule!(pub DQUOTE : u8 => satisfy(is_dquote));

View file

@ -6,27 +6,28 @@ use nom::{
branch::alt, branch::alt,
bytes::streaming::{tag_no_case, take, take_while1}, bytes::streaming::{tag_no_case, take, take_while1},
character::streaming::char, character::streaming::char,
combinator::{map, map_res}, combinator::{map, verify, map_res, opt},
multi::{many0, many1}, multi::{many0},
sequence::{delimited, pair, preceded, separated_pair, terminated}, sequence::{delimited, pair, tuple, preceded, separated_pair, terminated},
IResult, IResult,
}; };
use super::parsers::{byte, satisfy}; use super::parsers::{byte, never, satisfy};
use super::response::{Capability, ResponseCode}; use super::response::{
use super::rfc2234::{is_char, is_cr, is_ctl, is_dquote, is_lf, is_sp, CRLF, DIGIT, DQUOTE, SP}; 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<u8> => 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 ASTRING_CHAR : u8 => alt((ATOM_CHAR, resp_specials)));
rule!(pub atom : Vec<u8> => many1(ATOM_CHAR)); rule!(pub atom : CowU8 => map(take_while1(is_atom_char), Cow::from));
// TODO: somehow incorporate CHAR in here? pub(crate) fn is_atom_char(c: u8) -> bool { is_char(c) && !is_atom_specials(c) }
// technically ATOM_CHAR is defined as <any CHAR except atom-specials> rule!(pub ATOM_CHAR : u8 => satisfy(is_atom_char));
// 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_specials(c: u8) -> bool { pub(crate) fn is_atom_specials(c: u8) -> bool {
c == b'(' 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 atom_specials : u8 => satisfy(is_atom_specials));
rule!(pub auth_type : Vec<u8> => atom); rule!(pub auth_type : Atom => atom);
rule!(pub capability : Capability => alt(( rule!(pub capability : Capability => alt((
map(preceded(tag_no_case("AUTH="), auth_type), |s| Capability::Auth(Cow::from(s))), map(preceded(tag_no_case("AUTH="), auth_type), |s| Capability::Auth(Cow::from(s))),
@ -55,6 +56,28 @@ rule!(pub capability_data : Vec<Capability> => preceded(tag_no_case("CAPABILITY"
), |(mut a, b)| { a.extend(b); a }) ), |(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<Flag> => delimited(byte(b'('), sep_list!(?flag), byte(b')')));
pub(crate) fn is_list_wildcards(c: u8) -> bool { c == b'%' || c == b'*' } pub(crate) fn is_list_wildcards(c: u8) -> bool { c == b'%' || c == b'*' }
rule!(pub list_wildcards : u8 => satisfy(is_list_wildcards)); 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 // 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 // determined to exceed a certain threshold so we don't have insane amounts of
// data in memory // data in memory
pub fn literal(i: &[u8]) -> IResult<&[u8], Vec<u8>> { pub fn literal(i: &[u8]) -> IResult<&[u8], CowU8> {
let mut length_of = terminated(delimited(char('{'), number, char('}')), CRLF); let mut length_of = terminated(delimited(char('{'), number, char('}')), CRLF);
let (i, length) = length_of(i)?; let (i, length) = length_of(i)?;
println!("length is: {:?}", (i, length)); println!("length is: {:?}", (i, length));
map(take(length), |s: &[u8]| s.to_vec())(i) map(take(length), Cow::from)(i)
} }
#[test] #[test]
fn test_literal() { fn test_literal() {
assert_eq!( assert_eq!(
literal(b"{13}\r\nHello, world!").unwrap().1, literal(b"{13}\r\nHello, world!").unwrap().1.as_ref(),
b"Hello, world!" 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<MailboxListFlag> => 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<MessageAttribute> => 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 nil : &[u8] => tag_no_case("NIL"));
rule!(pub nstring : Option<Vec<u8>> => alt((map(string, Some), map(nil, |_| None)))); rule!(pub nstring : Option<CowU8> => alt((map(string, Some), map(nil, |_| None))));
pub(crate) fn number(i: &[u8]) -> IResult<&[u8], u32> { 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::<u32>() s.parse::<u32>()
})(i) })(i)
} }
rule!(pub quoted : Vec<u8> => 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) } 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)))); 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'\\' } pub(crate) fn is_quoted_specials(c: u8) -> bool { is_dquote(c) || c == b'\\' }
rule!(pub quoted_specials : u8 => satisfy(is_quoted_specials)); 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']' } pub(crate) fn is_resp_specials(c: u8) -> bool { c == b']' }
rule!(pub resp_specials : u8 => satisfy(is_resp_specials)); 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(( rule!(pub resp_text_code : ResponseCode => alt((
map(tag_no_case("ALERT"), |_| ResponseCode::Alert), map(tag_no_case("ALERT"), |_| ResponseCode::Alert),
map(capability_data, ResponseCode::Capabilities), map(capability_data, ResponseCode::Capabilities),
))); )));
rule!(pub string : Vec<u8> => 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) } 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)); rule!(pub TEXT_CHAR : u8 => satisfy(is_text_char));