diff --git a/Cargo.lock b/Cargo.lock index 3ae7a7f..a37628a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1483,6 +1483,7 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pest" version = "2.1.3" +source = "git+https://github.com/iptq/pest?rev=6a4d3a3d10e42a3ee605ca979d0fcdac97a83a99#6a4d3a3d10e42a3ee605ca979d0fcdac97a83a99" dependencies = [ "log", "ucd-trie", @@ -1491,6 +1492,7 @@ dependencies = [ [[package]] name = "pest_derive" version = "2.1.0" +source = "git+https://github.com/iptq/pest?rev=6a4d3a3d10e42a3ee605ca979d0fcdac97a83a99#6a4d3a3d10e42a3ee605ca979d0fcdac97a83a99" dependencies = [ "pest", "pest_generator", @@ -1499,6 +1501,7 @@ dependencies = [ [[package]] name = "pest_generator" version = "2.1.3" +source = "git+https://github.com/iptq/pest?rev=6a4d3a3d10e42a3ee605ca979d0fcdac97a83a99#6a4d3a3d10e42a3ee605ca979d0fcdac97a83a99" dependencies = [ "pest", "pest_meta", @@ -1510,6 +1513,7 @@ dependencies = [ [[package]] name = "pest_meta" version = "2.1.3" +source = "git+https://github.com/iptq/pest?rev=6a4d3a3d10e42a3ee605ca979d0fcdac97a83a99#6a4d3a3d10e42a3ee605ca979d0fcdac97a83a99" dependencies = [ "cargo", "maplit", diff --git a/imap/src/parser/mod.rs b/imap/src/parser/mod.rs index 8919dae..ccae3c9 100644 --- a/imap/src/parser/mod.rs +++ b/imap/src/parser/mod.rs @@ -2,11 +2,13 @@ mod literal; +#[cfg(test)] +mod tests; + use std::fmt::Debug; -use std::mem; use std::str::FromStr; -use pest::{error::Error, iterators::Pair, ParseResult as PestResult, Parser, ParserState}; +use pest::{error::Error, iterators::Pair, Parser}; use crate::response::*; @@ -151,7 +153,7 @@ fn build_msg_att_static(pair: Pair) -> AttributeValue { match pair.as_rule() { Rule::msg_att_static_internaldate => { - AttributeValue::InternalDate(build_string(unwrap1(pair))) + AttributeValue::InternalDate(build_string(unwrap1(unwrap1(pair)))) } Rule::msg_att_static_rfc822_size => AttributeValue::Rfc822Size(build_number(unwrap1(pair))), Rule::msg_att_static_envelope => AttributeValue::Envelope(build_envelope(unwrap1(pair))), @@ -166,9 +168,32 @@ fn build_msg_att_static(pair: Pair) -> AttributeValue { } } -fn build_envelope(_pair: Pair) -> Envelope { +fn build_envelope(pair: Pair) -> Envelope { // TODO: do this - Envelope::default() + let mut pairs = pair.into_inner(); + let date = build_nstring(unwrap1(pairs.next().unwrap())); + let subject = build_nstring(unwrap1(pairs.next().unwrap())); + pairs.next().unwrap(); // env_from + pairs.next().unwrap(); // env_sender + pairs.next().unwrap(); // env_reply_to + pairs.next().unwrap(); // env_to + pairs.next().unwrap(); // env_cc + pairs.next().unwrap(); // env_bcc + let in_reply_to = build_nstring(unwrap1(pairs.next().unwrap())); + let message_id = build_nstring(unwrap1(pairs.next().unwrap())); + + Envelope { + date, + subject, + from: None, + sender: None, + reply_to: None, + to: None, + cc: None, + bcc: None, + in_reply_to, + message_id, + } } fn build_resp_cond_state(pair: Pair) -> (Status, Option, Option) { @@ -392,17 +417,26 @@ where /// /// [1]: self::build_string fn build_nstring(pair: Pair) -> Option { - if matches!(pair.as_rule(), Rule::nil) { - return None; + assert!(matches!(pair.as_rule(), Rule::nstring)); + let pair = unwrap1(pair); + match pair.as_rule() { + Rule::nil => None, + Rule::string => Some(build_string(pair)), + _ => unreachable!(), } - - Some(build_string(pair)) } /// Extracts a string-type, discarding the surrounding quotes and unescaping the escaped characters fn build_string(pair: Pair) -> String { - // TODO: actually get rid of the quotes and escaped chars - pair.as_str().to_owned() + assert!(matches!(pair.as_rule(), Rule::string)); + let pair = unwrap1(pair); + + match pair.as_rule() { + Rule::literal => build_literal(pair), + // TODO: escaping stuff? + Rule::quoted => pair.as_str().trim_start_matches("\"").trim_end_matches("\"").replace("\\\"", "\"").to_owned(), + _ => unreachable!(), + } } fn parse_literal(s: impl AsRef) -> ParseResult { @@ -419,129 +453,3 @@ fn build_literal(pair: Pair) -> String { let literal_str = pairs.next().unwrap(); literal_str.as_str().to_owned() } - -#[cfg(test)] -mod tests { - use super::*; - use crate::response::*; - use pest::Parser; - - #[test] - fn test_literal() { - assert_eq!(parse_literal("{7}\r\nhellosu"), Ok("hellosu".to_owned())); - } - - #[test] - #[rustfmt::skip] - fn test_capability() { - assert_eq!(parse_capability("IMAP4rev1"), Ok(Capability::Imap4rev1)); - assert_eq!(parse_capability("LOGINDISABLED"), Ok(Capability::Atom("LOGINDISABLED".to_owned()))); - assert_eq!(parse_capability("AUTH=PLAIN"), Ok(Capability::Auth("PLAIN".to_owned()))); - assert_eq!(parse_capability("auth=plain"), Ok(Capability::Auth("PLAIN".to_owned()))); - - assert!(parse_capability("(OSU)").is_err()); - assert!(parse_capability("\x01HELLO").is_err()); - } - - #[test] - #[rustfmt::skip] - fn test_nil() { - assert!(Rfc3501::parse(Rule::nil, "NIL").is_ok()); - assert!(Rfc3501::parse(Rule::nil, "anything else").is_err()); - } - - #[test] - fn test_section_8() { - // this little exchange is from section 8 of rfc3501 - // https://tools.ietf.org/html/rfc3501#section-8 - - assert_eq!( - parse_response("* OK IMAP4rev1 Service Ready\r\n"), - Ok(Response::Data(ResponseData { - status: Status::Ok, - code: None, - information: Some("IMAP4rev1 Service Ready".to_owned()), - })) - ); - - assert_eq!( - parse_response("a001 OK LOGIN completed\r\n"), - Ok(Response::Done(ResponseDone { - tag: "a001".to_owned(), - status: Status::Ok, - code: None, - information: Some("LOGIN completed".to_owned()), - })) - ); - - assert_eq!( - parse_response("* 18 EXISTS\r\n"), - Ok(Response::MailboxData(MailboxData::Exists(18))) - ); - - assert_eq!( - parse_response("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n"), - Ok(Response::MailboxData(MailboxData::Flags(vec![ - MailboxFlag::Answered, - MailboxFlag::Flagged, - MailboxFlag::Deleted, - MailboxFlag::Seen, - MailboxFlag::Draft, - ]))) - ); - - assert_eq!( - parse_response("* 2 RECENT\r\n"), - Ok(Response::MailboxData(MailboxData::Recent(2))) - ); - - assert_eq!( - parse_response("* OK [UNSEEN 17] Message 17 is the first unseen message\r\n"), - Ok(Response::Data(ResponseData { - status: Status::Ok, - code: Some(ResponseCode::Unseen(17)), - information: Some("Message 17 is the first unseen message".to_owned()), - })) - ); - - assert_eq!( - parse_response("* OK [UIDVALIDITY 3857529045] UIDs valid\r\n"), - Ok(Response::Data(ResponseData { - status: Status::Ok, - code: Some(ResponseCode::UidValidity(3857529045)), - information: Some("UIDs valid".to_owned()), - })) - ); - - assert_eq!( - parse_response("a002 OK [READ-WRITE] SELECT completed\r\n"), - Ok(Response::Done(ResponseDone { - tag: "a002".to_owned(), - status: Status::Ok, - code: Some(ResponseCode::ReadWrite), - information: Some("SELECT completed".to_owned()), - })) - ); - - assert_eq!( - parse_response(concat!( - r#"* 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 "") BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 302892))"#, - "\r\n", - )), - Ok(Response::Fetch( - 12, - vec![ - AttributeValue::Flags(vec![MailboxFlag::Seen]), - AttributeValue::InternalDate("\"17-Jul-1996 02:44:25 -0700\"".to_owned()), - AttributeValue::Rfc822Size(4286), - AttributeValue::Envelope(Envelope::default()), - AttributeValue::BodySection { - section: None, - index: None, - data: None, - }, - ] - )) - ); - } -} diff --git a/imap/src/parser/rfc3501.pest b/imap/src/parser/rfc3501.pest index 662a033..270c483 100644 --- a/imap/src/parser/rfc3501.pest +++ b/imap/src/parser/rfc3501.pest @@ -41,7 +41,9 @@ char8 = @{ '\x01'..'\xff' } continue_req = { "+" ~ sp ~ (resp_text | base64) ~ crlf } date_day_fixed = { (sp ~ digit) | digit{2} } date_month = { "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" | "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec" } -date_time = { dquote ~ date_day_fixed ~ "-" ~ date_month ~ "-" ~ date_year ~ sp ~ time ~ sp ~ zone ~ dquote } +// TODO: date_time is a real date time +// date_time = { dquote ~ date_day_fixed ~ "-" ~ date_month ~ "-" ~ date_year ~ sp ~ time ~ sp ~ zone ~ dquote } +date_time = ${ string } date_year = @{ digit{4} } digit_nz = @{ '\x31'..'\x39' } env_bcc = { "(" ~ address{1,} ~ ")" | nil } diff --git a/imap/src/parser/tests.rs b/imap/src/parser/tests.rs new file mode 100644 index 0000000..06ca4ed --- /dev/null +++ b/imap/src/parser/tests.rs @@ -0,0 +1,133 @@ +use super::*; +use crate::response::*; +use pest::Parser; + +#[test] +fn test_literal() { + assert_eq!(parse_literal("{7}\r\nhellosu"), Ok("hellosu".to_owned())); +} + +#[test] +#[rustfmt::skip] +fn test_capability() { + assert_eq!(parse_capability("IMAP4rev1"), Ok(Capability::Imap4rev1)); + assert_eq!(parse_capability("LOGINDISABLED"), Ok(Capability::Atom("LOGINDISABLED".to_owned()))); + assert_eq!(parse_capability("AUTH=PLAIN"), Ok(Capability::Auth("PLAIN".to_owned()))); + assert_eq!(parse_capability("auth=plain"), Ok(Capability::Auth("PLAIN".to_owned()))); + + assert!(parse_capability("(OSU)").is_err()); + assert!(parse_capability("\x01HELLO").is_err()); +} + +#[test] +#[rustfmt::skip] +fn test_nil() { + assert!(Rfc3501::parse(Rule::nil, "NIL").is_ok()); + assert!(Rfc3501::parse(Rule::nil, "anything else").is_err()); +} + +#[test] +fn test_section_8() { + // this little exchange is from section 8 of rfc3501 + // https://tools.ietf.org/html/rfc3501#section-8 + + assert_eq!( + parse_response("* OK IMAP4rev1 Service Ready\r\n"), + Ok(Response::Data(ResponseData { + status: Status::Ok, + code: None, + information: Some("IMAP4rev1 Service Ready".to_owned()), + })) + ); + + assert_eq!( + parse_response("a001 OK LOGIN completed\r\n"), + Ok(Response::Done(ResponseDone { + tag: "a001".to_owned(), + status: Status::Ok, + code: None, + information: Some("LOGIN completed".to_owned()), + })) + ); + + assert_eq!( + parse_response("* 18 EXISTS\r\n"), + Ok(Response::MailboxData(MailboxData::Exists(18))) + ); + + assert_eq!( + parse_response("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n"), + Ok(Response::MailboxData(MailboxData::Flags(vec![ + MailboxFlag::Answered, + MailboxFlag::Flagged, + MailboxFlag::Deleted, + MailboxFlag::Seen, + MailboxFlag::Draft, + ]))) + ); + + assert_eq!( + parse_response("* 2 RECENT\r\n"), + Ok(Response::MailboxData(MailboxData::Recent(2))) + ); + + assert_eq!( + parse_response("* OK [UNSEEN 17] Message 17 is the first unseen message\r\n"), + Ok(Response::Data(ResponseData { + status: Status::Ok, + code: Some(ResponseCode::Unseen(17)), + information: Some("Message 17 is the first unseen message".to_owned()), + })) + ); + + assert_eq!( + parse_response("* OK [UIDVALIDITY 3857529045] UIDs valid\r\n"), + Ok(Response::Data(ResponseData { + status: Status::Ok, + code: Some(ResponseCode::UidValidity(3857529045)), + information: Some("UIDs valid".to_owned()), + })) + ); + + assert_eq!( + parse_response("a002 OK [READ-WRITE] SELECT completed\r\n"), + Ok(Response::Done(ResponseDone { + tag: "a002".to_owned(), + status: Status::Ok, + code: Some(ResponseCode::ReadWrite), + information: Some("SELECT completed".to_owned()), + })) + ); + + assert_eq!( + parse_response(concat!( + r#"* 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 "") BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 302892))"#, + "\r\n", + )), + Ok(Response::Fetch( + 12, + vec![ + AttributeValue::Flags(vec![MailboxFlag::Seen]), + AttributeValue::InternalDate("17-Jul-1996 02:44:25 -0700".to_owned()), + AttributeValue::Rfc822Size(4286), + AttributeValue::Envelope(Envelope { + date: Some("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)".to_owned()), + subject: Some("IMAP4rev1 WG mtg summary and minutes".to_owned()), + from: None, + sender: None, + reply_to: None, + to: None, + cc: None, + bcc: None, + in_reply_to: None, + message_id: Some("".to_owned()), + }), + AttributeValue::BodySection { + section: None, + index: None, + data: None, + }, + ] + )) + ); +}