From 4913b2ac9d9fa9c93f6e212446263086865c3246 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Mon, 22 Feb 2021 14:36:08 -0600 Subject: [PATCH 01/11] wip --- imap/src/client/inner.rs | 51 ++++++++++++++++++++--- imap/src/lib.rs | 6 ++- imap/src/parser/rfc3501/body.rs | 2 +- imap/src/parser/rfc3501/body_structure.rs | 2 +- imap/src/parser/rfc3501/mod.rs | 4 +- imap/src/parser/rfc4315.rs | 2 +- imap/src/parser/rfc4551.rs | 2 +- imap/src/parser/rfc5161.rs | 6 +-- imap/src/parser/rfc5464.rs | 2 +- imap/src/parser/rfc7162.rs | 2 +- imap/src/response/mod.rs | 2 +- imap/src/types.rs | 2 +- src/ui/mod.rs | 4 ++ 13 files changed, 67 insertions(+), 20 deletions(-) diff --git a/imap/src/client/inner.rs b/imap/src/client/inner.rs index ed79375..9f73a39 100644 --- a/imap/src/client/inner.rs +++ b/imap/src/client/inner.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll, Waker}; @@ -20,11 +20,11 @@ use tokio_rustls::{ use crate::command::Command; use crate::response::{Capability, Response, ResponseCode}; -use crate::types::Status; +use crate::types::{Capability as Capability_, Status}; use super::ClientConfig; -pub type CapsLock = Arc>>>; +pub type CapsLock = Arc>>>; pub type ResultMap = Arc, Option)>>>; pub type GreetingState = Arc)>>; pub const TAG_PREFIX: &str = "panorama"; @@ -130,6 +130,10 @@ where /// Attempts to upgrade this connection using STARTTLS pub async fn upgrade(mut self) -> Result>> { // TODO: make sure STARTTLS is in the capability list + if !self.has_capability("STARTTLS").await? { + bail!("server doesn't support this capability"); + } + // first, send the STARTTLS command let resp = self.execute(Command::Starttls).await?; debug!("server response to starttls: {:?}", resp); @@ -154,6 +158,34 @@ where Ok(Client::new(stream, self.config)) } + + /// Check if this client has a particular capability + pub async fn has_capability(&self, cap: impl AsRef) -> Result { + let cap = cap.as_ref().to_owned(); + debug!("checking for the capability: {:?}", cap); + + let cap_bytes = cap.as_bytes(); + debug!("cap_bytes {:?}", cap_bytes); + let (_, cap) = match crate::oldparser::rfc3501::capability(cap_bytes) { + Ok(v) => v, + Err(err) => { + error!("ERROR PARSING {:?} {} {:?}", cap, err, err); + use std::error::Error; + let bt = err.backtrace().unwrap(); + error!("{}", bt); + std::process::exit(1); + } + }; + let cap = Capability::from(cap); + + let caps = &*self.caps.read(); + // TODO: refresh caps + + let caps = caps.as_ref().unwrap(); + let result = caps.contains(&cap); + debug!("cap result: {:?}", result); + Ok(result) + } } pub struct GreetingWaiter(GreetingState); @@ -226,7 +258,7 @@ where } debug!("got a new line {:?}", next_line); - let (_, resp) = match crate::parser::parse_response(next_line.as_bytes()) { + let (_, resp) = match crate::oldparser::parse_response(next_line.as_bytes()) { Ok(v) => v, Err(err) => { debug!("shiet: {:?}", err); @@ -246,6 +278,7 @@ where let resp = Response::from(resp); debug!("resp: {:?}", resp); match &resp { + // capabilities list Response::Capabilities(new_caps) | Response::Data { status: Status::Ok, @@ -253,10 +286,18 @@ where .. } => { let caps = &mut *caps.write(); - *caps = Some(new_caps.clone()); + *caps = Some(new_caps.iter().cloned().collect()); debug!("new caps: {:?}", caps); } + // bye + Response::Data { + status: Status::Bye, + .. + } => { + bail!("disconnected"); + } + Response::Done { tag, .. } => { let tag_str = &tag.0; if tag_str.starts_with(TAG_PREFIX) { diff --git a/imap/src/lib.rs b/imap/src/lib.rs index f613dd8..b7a5439 100644 --- a/imap/src/lib.rs +++ b/imap/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(backtrace)] + #[macro_use] extern crate anyhow; #[macro_use] @@ -10,9 +12,9 @@ extern crate log; pub mod builders; pub mod client; pub mod command; -pub mod parser; +pub mod oldparser; pub mod response; pub mod types; -pub use crate::parser::ParseResult; +pub use crate::oldparser::ParseResult; pub use crate::types::*; diff --git a/imap/src/parser/rfc3501/body.rs b/imap/src/parser/rfc3501/body.rs index 1bdcf7d..d9d5f15 100644 --- a/imap/src/parser/rfc3501/body.rs +++ b/imap/src/parser/rfc3501/body.rs @@ -8,7 +8,7 @@ use nom::{ IResult, }; -use crate::{parser::core::*, types::*}; +use crate::{oldparser::core::*, types::*}; pub fn section_part(i: &[u8]) -> IResult<&[u8], Vec> { let (i, (part, mut rest)) = tuple((number, many0(preceded(char('.'), number))))(i)?; diff --git a/imap/src/parser/rfc3501/body_structure.rs b/imap/src/parser/rfc3501/body_structure.rs index 72a48d5..c81ab4e 100644 --- a/imap/src/parser/rfc3501/body_structure.rs +++ b/imap/src/parser/rfc3501/body_structure.rs @@ -9,7 +9,7 @@ use nom::{ }; use crate::{ - parser::{core::*, rfc3501::envelope}, + oldparser::{core::*, rfc3501::envelope}, types::*, }; diff --git a/imap/src/parser/rfc3501/mod.rs b/imap/src/parser/rfc3501/mod.rs index 460ae16..c125582 100644 --- a/imap/src/parser/rfc3501/mod.rs +++ b/imap/src/parser/rfc3501/mod.rs @@ -17,7 +17,7 @@ use nom::{ }; use crate::{ - parser::{ + oldparser::{ core::*, rfc3501::body::*, rfc3501::body_structure::*, rfc4315, rfc4551, rfc5161, rfc5464, rfc7162, }, @@ -181,7 +181,7 @@ fn resp_text_code(i: &[u8]) -> IResult<&[u8], ResponseCode> { )(i) } -fn capability(i: &[u8]) -> IResult<&[u8], Capability> { +pub fn capability(i: &[u8]) -> IResult<&[u8], Capability> { alt(( map(tag_no_case(b"IMAP4rev1"), |_| Capability::Imap4rev1), map(preceded(tag_no_case(b"AUTH="), atom), Capability::Auth), diff --git a/imap/src/parser/rfc4315.rs b/imap/src/parser/rfc4315.rs index 9b2b13f..ac01876 100644 --- a/imap/src/parser/rfc4315.rs +++ b/imap/src/parser/rfc4315.rs @@ -13,7 +13,7 @@ use nom::{ IResult, }; -use crate::parser::core::number; +use crate::oldparser::core::number; use crate::types::*; /// Extends resp-text-code as follows: diff --git a/imap/src/parser/rfc4551.rs b/imap/src/parser/rfc4551.rs index 443062d..5c05e24 100644 --- a/imap/src/parser/rfc4551.rs +++ b/imap/src/parser/rfc4551.rs @@ -8,7 +8,7 @@ use nom::{bytes::streaming::tag_no_case, sequence::tuple, IResult}; use crate::{ - parser::core::{number_64, paren_delimited}, + oldparser::core::{number_64, paren_delimited}, types::*, }; diff --git a/imap/src/parser/rfc5161.rs b/imap/src/parser/rfc5161.rs index 3a1f7c4..09be547 100644 --- a/imap/src/parser/rfc5161.rs +++ b/imap/src/parser/rfc5161.rs @@ -13,7 +13,7 @@ use nom::{ IResult, }; -use crate::parser::core::atom; +use crate::oldparser::core::atom; use crate::types::*; // The ENABLED response lists capabilities that were enabled in response @@ -23,7 +23,7 @@ pub(crate) fn resp_enabled(i: &[u8]) -> IResult<&[u8], Response> { map(enabled_data, Response::Capabilities)(i) } -fn enabled_data(i: &[u8]) -> IResult<&[u8], Vec> { +pub fn enabled_data(i: &[u8]) -> IResult<&[u8], Vec> { let (i, (_, capabilities)) = tuple(( tag_no_case("ENABLED"), many0(preceded(char(' '), capability)), @@ -31,6 +31,6 @@ fn enabled_data(i: &[u8]) -> IResult<&[u8], Vec> { Ok((i, capabilities)) } -fn capability(i: &[u8]) -> IResult<&[u8], Capability> { +pub fn capability(i: &[u8]) -> IResult<&[u8], Capability> { map(atom, Capability::Atom)(i) } diff --git a/imap/src/parser/rfc5464.rs b/imap/src/parser/rfc5464.rs index 6b2ba3b..e50ff9e 100644 --- a/imap/src/parser/rfc5464.rs +++ b/imap/src/parser/rfc5464.rs @@ -13,7 +13,7 @@ use nom::{ IResult, }; -use crate::{parser::core::*, types::*}; +use crate::{oldparser::core::*, types::*}; fn is_entry_component_char(c: u8) -> bool { c < 0x80 && c > 0x19 && c != b'*' && c != b'%' && c != b'/' diff --git a/imap/src/parser/rfc7162.rs b/imap/src/parser/rfc7162.rs index afa7ae6..e23fb1a 100644 --- a/imap/src/parser/rfc7162.rs +++ b/imap/src/parser/rfc7162.rs @@ -10,7 +10,7 @@ use nom::{ IResult, }; -use crate::parser::core::sequence_set; +use crate::oldparser::core::sequence_set; use crate::types::*; // The VANISHED response reports that the specified UIDs have been diff --git a/imap/src/response/mod.rs b/imap/src/response/mod.rs index 74589a3..ba009f1 100644 --- a/imap/src/response/mod.rs +++ b/imap/src/response/mod.rs @@ -70,7 +70,7 @@ impl<'a> From> for Response { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Hash, PartialEq, Eq)] pub enum Capability { Imap4rev1, Auth(String), diff --git a/imap/src/types.rs b/imap/src/types.rs index 63fe5a7..7366b6c 100644 --- a/imap/src/types.rs +++ b/imap/src/types.rs @@ -40,7 +40,7 @@ pub enum Response<'a> { impl<'a> Response<'a> { pub fn from_bytes(buf: &'a [u8]) -> crate::ParseResult { - crate::parser::parse_response(buf) + crate::oldparser::parse_response(buf) } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index a2bb49e..27915e8 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -28,6 +28,10 @@ pub struct Rect(u16, u16, u16, u16); /// UI entrypoint. pub async fn run_ui(mut w: impl Write + Debug, exit: ExitSender) -> Result<()> { + loop { + tokio::time::sleep(Duration::from_secs(4000)).await; + } + execute!(w, cursor::Hide, terminal::EnterAlternateScreen)?; terminal::enable_raw_mode()?; From aa3578999c16c436e55e873c8c1b3400eddc626b Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Mon, 22 Feb 2021 14:36:53 -0600 Subject: [PATCH 02/11] asdf --- .github/workflows/doc.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 983af5a..1545f75 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -1,4 +1,7 @@ -on: [push] +on: + push: + branches: + - master name: CI From 4f3af86211e48ba7e92b5b603d66d8816694b09e Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Mon, 22 Feb 2021 14:43:09 -0600 Subject: [PATCH 03/11] rename the old shit --- imap/src/client/inner.rs | 53 +++++++++++++------------ imap/src/lib.rs | 13 +++---- imap/src/response/mod.rs | 83 +++++----------------------------------- 3 files changed, 42 insertions(+), 107 deletions(-) diff --git a/imap/src/client/inner.rs b/imap/src/client/inner.rs index 9f73a39..c529aa0 100644 --- a/imap/src/client/inner.rs +++ b/imap/src/client/inner.rs @@ -5,13 +5,12 @@ use std::task::{Context, Poll, Waker}; use anyhow::{Context as AnyhowContext, Result}; use futures::future::{self, Either, Future, FutureExt}; -use parking_lot::{Mutex, RwLock}; +use parking_lot::RwLock; use tokio::{ io::{ - self, AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, - ReadHalf, WriteHalf, + self, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadHalf, WriteHalf, }, - sync::{mpsc, oneshot}, + sync::mpsc, task::JoinHandle, }; use tokio_rustls::{ @@ -19,8 +18,8 @@ use tokio_rustls::{ }; use crate::command::Command; -use crate::response::{Capability, Response, ResponseCode}; -use crate::types::{Capability as Capability_, Status}; +use crate::response::{Capability, Response, ResponseCode, Status}; +// use crate::types::{Capability as Capability_, Status}; use super::ClientConfig; @@ -166,17 +165,17 @@ where let cap_bytes = cap.as_bytes(); debug!("cap_bytes {:?}", cap_bytes); - let (_, cap) = match crate::oldparser::rfc3501::capability(cap_bytes) { - Ok(v) => v, - Err(err) => { - error!("ERROR PARSING {:?} {} {:?}", cap, err, err); - use std::error::Error; - let bt = err.backtrace().unwrap(); - error!("{}", bt); - std::process::exit(1); - } - }; - let cap = Capability::from(cap); + // let (_, cap) = match crate::oldparser::rfc3501::capability(cap_bytes) { + // Ok(v) => v, + // Err(err) => { + // error!("ERROR PARSING {:?} {} {:?}", cap, err, err); + // use std::error::Error; + // let bt = err.backtrace().unwrap(); + // error!("{}", bt); + // std::process::exit(1); + // } + // }; + let cap = Capability::from(Capability::Atom(cap)); let caps = &*self.caps.read(); // TODO: refresh caps @@ -258,13 +257,14 @@ where } debug!("got a new line {:?}", next_line); - let (_, resp) = match crate::oldparser::parse_response(next_line.as_bytes()) { - Ok(v) => v, - Err(err) => { - debug!("shiet: {:?}", err); - continue; - } - }; + let resp = Response::Capabilities(vec![]); + // let (_, resp) = match crate::oldparser::parse_response(next_line.as_bytes()) { + // Ok(v) => v, + // Err(err) => { + // debug!("shiet: {:?}", err); + // continue; + // } + // }; if let Some(greeting) = greeting.take() { let (greeting, waker) = &mut *greeting.write(); @@ -299,9 +299,8 @@ where } Response::Done { tag, .. } => { - let tag_str = &tag.0; - if tag_str.starts_with(TAG_PREFIX) { - let id = tag_str.trim_start_matches(TAG_PREFIX).parse::()?; + if tag.starts_with(TAG_PREFIX) { + let id = tag.trim_start_matches(TAG_PREFIX).parse::()?; let mut results = results.write(); if let Some((c, waker)) = results.get_mut(&id) { *c = Some(resp); diff --git a/imap/src/lib.rs b/imap/src/lib.rs index b7a5439..4846616 100644 --- a/imap/src/lib.rs +++ b/imap/src/lib.rs @@ -1,5 +1,3 @@ -#![feature(backtrace)] - #[macro_use] extern crate anyhow; #[macro_use] @@ -9,12 +7,13 @@ extern crate futures; #[macro_use] extern crate log; -pub mod builders; pub mod client; pub mod command; -pub mod oldparser; pub mod response; -pub mod types; -pub use crate::oldparser::ParseResult; -pub use crate::types::*; +// pub mod builders; +// pub mod oldparser; +// pub mod types; + +// pub use crate::oldparser::ParseResult; +// pub use crate::types::*; diff --git a/imap/src/response/mod.rs b/imap/src/response/mod.rs index ba009f1..df8af08 100644 --- a/imap/src/response/mod.rs +++ b/imap/src/response/mod.rs @@ -1,10 +1,5 @@ use std::ops::RangeInclusive; -use crate::types::{ - AttributeValue as AttributeValue_, Capability as Capability_, MailboxDatum as MailboxDatum_, - RequestId, Response as Response_, ResponseCode as ResponseCode_, State, Status, -}; - #[derive(Clone, Debug)] pub enum Response { Capabilities(Vec), @@ -13,7 +8,7 @@ pub enum Response { information: Option, }, Done { - tag: RequestId, + tag: String, status: Status, code: Option, information: Option, @@ -32,44 +27,6 @@ pub enum Response { MailboxData(MailboxDatum), } -impl<'a> From> for Response { - fn from(b: Response_) -> Self { - use Response_::*; - match b { - Capabilities(caps) => { - Response::Capabilities(caps.into_iter().map(Capability::from).collect()) - } - Continue { code, information } => Response::Continue { - code: code.map(ResponseCode::from), - information: information.map(str::to_owned), - }, - Done { - tag, - status, - code, - information, - } => Response::Done { - tag, - status, - code: code.map(ResponseCode::from), - information: information.map(str::to_owned), - }, - Data { - status, - code, - information, - } => Response::Data { - status, - code: code.map(ResponseCode::from), - information: information.map(str::to_owned), - }, - Expunge(n) => Response::Expunge(n), - Vanished { earlier, uids } => Response::Vanished { earlier, uids }, - _ => todo!("nyi: {:?}", b), - } - } -} - #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub enum Capability { Imap4rev1, @@ -77,17 +34,6 @@ pub enum Capability { Atom(String), } -impl<'a> From> for Capability { - fn from(b: Capability_) -> Self { - use Capability_::*; - match b { - Imap4rev1 => Capability::Imap4rev1, - Auth(s) => Capability::Auth(s.to_owned()), - Atom(s) => Capability::Atom(s.to_owned()), - } - } -} - #[derive(Clone, Debug)] pub enum ResponseCode { Alert, @@ -107,24 +53,6 @@ pub enum ResponseCode { UidNotSticky, } -impl<'a> From> for ResponseCode { - fn from(b: ResponseCode_) -> Self { - use ResponseCode_::*; - match b { - Alert => ResponseCode::Alert, - BadCharset(s) => { - ResponseCode::BadCharset(s.map(|v| v.into_iter().map(str::to_owned).collect())) - } - Capabilities(v) => { - ResponseCode::Capabilities(v.into_iter().map(Capability::from).collect()) - } - HighestModSeq(n) => ResponseCode::HighestModSeq(n), - Parse => ResponseCode::Parse, - _ => todo!("nyi: {:?}", b), - } - } -} - #[derive(Clone, Debug)] pub enum UidSetMember { UidRange(RangeInclusive), @@ -136,3 +64,12 @@ pub enum AttributeValue {} #[derive(Clone, Debug)] pub enum MailboxDatum {} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Status { + Ok, + No, + Bad, + PreAuth, + Bye, +} From e0ca51ef795730c4463ccfdb8465ecfe43b0e3f5 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Mon, 22 Feb 2021 15:33:30 -0600 Subject: [PATCH 04/11] capability parser --- Cargo.lock | 189 ++++++++++++------ imap/Cargo.toml | 6 +- imap/src/lib.rs | 3 + .../{parser => oldparser}/bodystructure.rs | 0 imap/src/{parser => oldparser}/core.rs | 0 imap/src/oldparser/mod.rs | 25 +++ .../src/{parser => oldparser}/rfc3501/body.rs | 0 .../rfc3501/body_structure.rs | 0 imap/src/{parser => oldparser}/rfc3501/mod.rs | 0 imap/src/{parser => oldparser}/rfc4315.rs | 0 imap/src/{parser => oldparser}/rfc4551.rs | 0 imap/src/{parser => oldparser}/rfc5161.rs | 0 imap/src/{parser => oldparser}/rfc5464.rs | 0 imap/src/{parser => oldparser}/rfc7162.rs | 0 imap/src/{parser => oldparser}/tests.rs | 0 imap/src/parser/mod.rs | 67 +++++-- imap/src/parser/rfc3501.pest | 17 ++ 17 files changed, 224 insertions(+), 83 deletions(-) rename imap/src/{parser => oldparser}/bodystructure.rs (100%) rename imap/src/{parser => oldparser}/core.rs (100%) create mode 100644 imap/src/oldparser/mod.rs rename imap/src/{parser => oldparser}/rfc3501/body.rs (100%) rename imap/src/{parser => oldparser}/rfc3501/body_structure.rs (100%) rename imap/src/{parser => oldparser}/rfc3501/mod.rs (100%) rename imap/src/{parser => oldparser}/rfc4315.rs (100%) rename imap/src/{parser => oldparser}/rfc4551.rs (100%) rename imap/src/{parser => oldparser}/rfc5161.rs (100%) rename imap/src/{parser => oldparser}/rfc5464.rs (100%) rename imap/src/{parser => oldparser}/rfc7162.rs (100%) rename imap/src/{parser => oldparser}/tests.rs (100%) create mode 100644 imap/src/parser/rfc3501.pest diff --git a/Cargo.lock b/Cargo.lock index f104a08..97b405b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,15 +77,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] -name = "bitvec" -version = "0.19.4" +name = "block-buffer" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" dependencies = [ - "funty", - "radium", - "tap", - "wyz", + "block-padding", + "byte-tools", + "byteorder", + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", ] [[package]] @@ -100,6 +109,12 @@ version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + [[package]] name = "byteorder" version = "1.4.2" @@ -264,6 +279,21 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + [[package]] name = "fast_chemail" version = "0.9.6" @@ -326,12 +356,6 @@ dependencies = [ "syn", ] -[[package]] -name = "funty" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" - [[package]] name = "futures" version = "0.3.12" @@ -427,6 +451,15 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" +dependencies = [ + "typenum", +] + [[package]] name = "getrandom" version = "0.2.2" @@ -536,7 +569,7 @@ dependencies = [ "hostname", "log", "native-tls", - "nom 4.2.3", + "nom", "serde", "serde_derive", "serde_json", @@ -566,6 +599,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "memchr" version = "2.3.4" @@ -623,18 +662,6 @@ dependencies = [ "version_check 0.1.5", ] -[[package]] -name = "nom" -version = "6.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" -dependencies = [ - "bitvec", - "funty", - "memchr", - "version_check 0.9.2", -] - [[package]] name = "ntapi" version = "0.3.6" @@ -679,6 +706,12 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + [[package]] name = "openssl" version = "0.10.32" @@ -712,15 +745,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "owning_ref" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" -dependencies = [ - "stable_deref_trait", -] - [[package]] name = "panorama" version = "0.0.1" @@ -760,9 +784,9 @@ dependencies = [ "derive_builder", "futures", "log", - "nom 6.1.2", - "owning_ref", "parking_lot", + "pest", + "pest_derive", "tokio", "tokio-rustls", "webpki-roots", @@ -793,6 +817,49 @@ dependencies = [ "winapi", ] +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1", +] + [[package]] name = "pin-project" version = "1.0.5" @@ -891,12 +958,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" - [[package]] name = "rand" version = "0.8.3" @@ -1093,6 +1154,18 @@ dependencies = [ "serde", ] +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer", + "digest", + "fake-simd", + "opaque-debug", +] + [[package]] name = "signal-hook" version = "0.1.17" @@ -1142,12 +1215,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "strsim" version = "0.8.0" @@ -1195,12 +1262,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "tempfile" version = "3.2.0" @@ -1314,6 +1375,18 @@ dependencies = [ "serde", ] +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + [[package]] name = "unicode-segmentation" version = "1.7.1" @@ -1482,12 +1555,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "wyz" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" - [[package]] name = "xdg" version = "2.2.0" diff --git a/imap/Cargo.toml b/imap/Cargo.toml index 401de54..caad1dc 100644 --- a/imap/Cargo.toml +++ b/imap/Cargo.toml @@ -16,9 +16,11 @@ anyhow = "1.0.38" derive_builder = "0.9.0" futures = "0.3.12" log = "0.4.14" -nom = { version = "6.1.2", default-features = false, features = ["std"] } -owning_ref = "0.4.1" +# nom = { version = "6.1.2", default-features = false, features = ["std"] } +# owning_ref = "0.4.1" parking_lot = "0.11.1" +pest = "2.1.3" +pest_derive = "2.1.0" tokio = { version = "1.1.1", features = ["full"] } tokio-rustls = "0.22.0" webpki-roots = "0.21.0" diff --git a/imap/src/lib.rs b/imap/src/lib.rs index 4846616..475b4d8 100644 --- a/imap/src/lib.rs +++ b/imap/src/lib.rs @@ -6,9 +6,12 @@ extern crate derive_builder; extern crate futures; #[macro_use] extern crate log; +#[macro_use] +extern crate pest_derive; pub mod client; pub mod command; +pub mod parser; pub mod response; // pub mod builders; diff --git a/imap/src/parser/bodystructure.rs b/imap/src/oldparser/bodystructure.rs similarity index 100% rename from imap/src/parser/bodystructure.rs rename to imap/src/oldparser/bodystructure.rs diff --git a/imap/src/parser/core.rs b/imap/src/oldparser/core.rs similarity index 100% rename from imap/src/parser/core.rs rename to imap/src/oldparser/core.rs diff --git a/imap/src/oldparser/mod.rs b/imap/src/oldparser/mod.rs new file mode 100644 index 0000000..a37fd91 --- /dev/null +++ b/imap/src/oldparser/mod.rs @@ -0,0 +1,25 @@ +use crate::types::Response; +use nom::{branch::alt, IResult}; + +pub mod core; + +pub mod bodystructure; +pub mod rfc3501; +pub mod rfc4315; +pub mod rfc4551; +pub mod rfc5161; +pub mod rfc5464; +pub mod rfc7162; + +#[cfg(test)] +mod tests; + +pub fn parse_response(msg: &[u8]) -> ParseResult { + alt(( + rfc3501::continue_req, + rfc3501::response_data, + rfc3501::response_tagged, + ))(msg) +} + +pub type ParseResult<'a> = IResult<&'a [u8], Response<'a>>; diff --git a/imap/src/parser/rfc3501/body.rs b/imap/src/oldparser/rfc3501/body.rs similarity index 100% rename from imap/src/parser/rfc3501/body.rs rename to imap/src/oldparser/rfc3501/body.rs diff --git a/imap/src/parser/rfc3501/body_structure.rs b/imap/src/oldparser/rfc3501/body_structure.rs similarity index 100% rename from imap/src/parser/rfc3501/body_structure.rs rename to imap/src/oldparser/rfc3501/body_structure.rs diff --git a/imap/src/parser/rfc3501/mod.rs b/imap/src/oldparser/rfc3501/mod.rs similarity index 100% rename from imap/src/parser/rfc3501/mod.rs rename to imap/src/oldparser/rfc3501/mod.rs diff --git a/imap/src/parser/rfc4315.rs b/imap/src/oldparser/rfc4315.rs similarity index 100% rename from imap/src/parser/rfc4315.rs rename to imap/src/oldparser/rfc4315.rs diff --git a/imap/src/parser/rfc4551.rs b/imap/src/oldparser/rfc4551.rs similarity index 100% rename from imap/src/parser/rfc4551.rs rename to imap/src/oldparser/rfc4551.rs diff --git a/imap/src/parser/rfc5161.rs b/imap/src/oldparser/rfc5161.rs similarity index 100% rename from imap/src/parser/rfc5161.rs rename to imap/src/oldparser/rfc5161.rs diff --git a/imap/src/parser/rfc5464.rs b/imap/src/oldparser/rfc5464.rs similarity index 100% rename from imap/src/parser/rfc5464.rs rename to imap/src/oldparser/rfc5464.rs diff --git a/imap/src/parser/rfc7162.rs b/imap/src/oldparser/rfc7162.rs similarity index 100% rename from imap/src/parser/rfc7162.rs rename to imap/src/oldparser/rfc7162.rs diff --git a/imap/src/parser/tests.rs b/imap/src/oldparser/tests.rs similarity index 100% rename from imap/src/parser/tests.rs rename to imap/src/oldparser/tests.rs diff --git a/imap/src/parser/mod.rs b/imap/src/parser/mod.rs index a37fd91..4c7c222 100644 --- a/imap/src/parser/mod.rs +++ b/imap/src/parser/mod.rs @@ -1,25 +1,52 @@ -use crate::types::Response; -use nom::{branch::alt, IResult}; +use pest::{error::Error, Parser}; -pub mod core; +use crate::response::*; -pub mod bodystructure; -pub mod rfc3501; -pub mod rfc4315; -pub mod rfc4551; -pub mod rfc5161; -pub mod rfc5464; -pub mod rfc7162; +#[derive(Parser)] +#[grammar = "parser/rfc3501.pest"] +struct Rfc3501; -#[cfg(test)] -mod tests; - -pub fn parse_response(msg: &[u8]) -> ParseResult { - alt(( - rfc3501::continue_req, - rfc3501::response_data, - rfc3501::response_tagged, - ))(msg) +pub fn parse_capability(s: &str) -> Result> { + let mut pairs = Rfc3501::parse(Rule::capability, s)?; + let pair = pairs.next().unwrap(); + let cap = match pair.as_rule() { + Rule::capability => { + let mut inner = pair.into_inner(); + let pair = inner.next().unwrap(); + match pair.as_rule() { + Rule::auth_type => Capability::Auth(pair.as_str().to_owned()), + Rule::atom => match pair.as_str() { + "IMAP4rev1" => Capability::Imap4rev1, + s => Capability::Atom(s.to_owned()), + }, + _ => unreachable!("{:?}", pair), + } + } + _ => unreachable!("{:?}", pair), + }; + Ok(cap) } -pub type ParseResult<'a> = IResult<&'a [u8], Response<'a>>; +#[cfg(test)] +#[rustfmt::skip] +mod tests { + use super::*; + use crate::response::*; + use pest::Parser; + + #[test] + 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!(parse_capability("(OSU)").is_err()); + assert!(parse_capability("\x01HELLO").is_err()); + } + + #[test] + fn test_nil() { + assert!(Rfc3501::parse(Rule::nil, "NIL").is_ok()); + assert!(Rfc3501::parse(Rule::nil, "anything else").is_err()); + } +} diff --git a/imap/src/parser/rfc3501.pest b/imap/src/parser/rfc3501.pest new file mode 100644 index 0000000..2fecd78 --- /dev/null +++ b/imap/src/parser/rfc3501.pest @@ -0,0 +1,17 @@ +// formal syntax from https://tools.ietf.org/html/rfc3501#section-9 +atom = @{ atom_char{1,} } +atom_char = @{ !atom_specials ~ char } +atom_specials = @{ "(" | ")" | "{" | sp | ctl | list_wildcards | quoted_specials | resp_specials } +auth_type = { atom } +capability = ${ "AUTH=" ~ auth_type | atom } +list_wildcards = @{ "%" | "*" } +quoted_specials = @{ dquote | "\\" } +resp_specials = @{ "]" } +nil = { "NIL" } + +// core rules from https://tools.ietf.org/html/rfc2234#section-6.1 +char = @{ '\x01'..'\x7f' } +ctl = @{ '\x00'..'\x1f' | "\x7f" } +dquote = @{ "\"" } +sp = @{ " " } + From 875041edfd9b9a43880161b44c65fa311065acae Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Mon, 22 Feb 2021 16:32:00 -0600 Subject: [PATCH 05/11] add rfc3501 rules that relate to response parsing --- imap/src/parser/mod.rs | 9 ++- imap/src/parser/rfc3501.pest | 111 ++++++++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/imap/src/parser/mod.rs b/imap/src/parser/mod.rs index 4c7c222..88f1d52 100644 --- a/imap/src/parser/mod.rs +++ b/imap/src/parser/mod.rs @@ -14,10 +14,10 @@ pub fn parse_capability(s: &str) -> Result> { let mut inner = pair.into_inner(); let pair = inner.next().unwrap(); match pair.as_rule() { - Rule::auth_type => Capability::Auth(pair.as_str().to_owned()), + Rule::auth_type => Capability::Auth(pair.as_str().to_uppercase().to_owned()), Rule::atom => match pair.as_str() { "IMAP4rev1" => Capability::Imap4rev1, - s => Capability::Atom(s.to_owned()), + s => Capability::Atom(s.to_uppercase().to_owned()), }, _ => unreachable!("{:?}", pair), } @@ -27,6 +27,10 @@ pub fn parse_capability(s: &str) -> Result> { Ok(cap) } +pub fn parse_response(s: &str) -> Result> { + todo!() +} + #[cfg(test)] #[rustfmt::skip] mod tests { @@ -39,6 +43,7 @@ mod tests { 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()); diff --git a/imap/src/parser/rfc3501.pest b/imap/src/parser/rfc3501.pest index 2fecd78..efde2c1 100644 --- a/imap/src/parser/rfc3501.pest +++ b/imap/src/parser/rfc3501.pest @@ -1,17 +1,124 @@ // formal syntax from https://tools.ietf.org/html/rfc3501#section-9 +addr_adl = { nstring } +addr_host = { nstring } +addr_mailbox = { nstring } +addr_name = { nstring } +address = { "(" ~ addr_name ~ sp ~ addr_adl ~ sp ~ addr_mailbox ~ sp ~ addr_host ~ ")" } +astring = @{ astring_char{1,} | string } +astring_char = @{ atom_char | resp_specials } atom = @{ atom_char{1,} } atom_char = @{ !atom_specials ~ char } atom_specials = @{ "(" | ")" | "{" | sp | ctl | list_wildcards | quoted_specials | resp_specials } auth_type = { atom } -capability = ${ "AUTH=" ~ auth_type | atom } +base64 = @{ (base64_char{4})* ~ base64_terminal } +base64_char = @{ alpha | digit | "+" | "/" } +base64_terminal = @{ (base64_char{2} ~ "==") | (base64_char{3} ~ "=") } +body = { "(" ~ (body_type_1part | body_type_mpart) ~ ")" } +body_ext_1part = { body_fld_md5 ~ (sp ~ body_fld_dsp ~ (sp ~ body_fld_lang ~ (sp ~ body_fld_loc ~ (sp ~ body_extension)*)?)?)? } +body_ext_mpart = { body_fld_param ~ (sp ~ body_fld_dsp ~ (sp ~ body_fld_lang ~ (sp ~ body_fld_loc ~ (sp ~ body_extension)*)?)?)? } +body_extension = { nstring | number | "(" ~ body_extension ~ (sp ~ body_extension)* ~ ")" } +body_fields = { body_fld_param ~ sp ~ body_fld_id ~ sp ~ body_fld_desc ~ sp ~ body_fld_enc ~ sp ~ body_fld_octets } +body_fld_desc = { nstring } +body_fld_dsp = { "(" ~ string ~ sp ~ body_fld_param ~ ")" | nil } +body_fld_enc = { (dquote ~ (^"7BIT" | ^"8BIT" | ^"BINARY" | ^"BASE64" | ^"QUOTED-PRINTABLE") ~ dquote) | string } +body_fld_id = { nstring } +body_fld_lang = { nstring | "(" ~ string ~ (sp ~ string)* ~ ")" } +body_fld_lines = { number } +body_fld_loc = { nstring } +body_fld_md5 = { nstring } +body_fld_octets = { number } +body_fld_param = { "(" ~ string ~ sp ~ string ~ (sp ~ string ~ sp ~ string)* ~ ")" | nil} +body_type_1part = { (body_type_basic | body_type_msg | body_type_text) ~ (sp ~ body_ext_1part)? } +body_type_basic = { media_basic ~ sp ~ body_fields } +body_type_mpart = { body{1,} ~ sp ~ media_subtype ~ (sp ~ body_ext_mpart)? } +body_type_msg = { media_message ~ sp ~ body_fields ~ sp ~ envelope ~ sp ~ body ~ sp ~ body_fld_lines } +body_type_text = { media_text ~ sp ~ body_fields ~ sp ~ body_fld_lines } +capability = ${ ^"AUTH=" ~ auth_type | atom } +capability_data = { ^"CAPABILITY" ~ (sp ~ capability)* ~ sp ~ "IMAP4rev1" ~ (sp ~ capability)* } +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 } +date_year = { digit{4} } +digit_nz = @{ '\x31'..'\x39' } +env_bcc = { "(" ~ address{1,} ~ ")" | nil } +env_cc = { "(" ~ address{1,} ~ ")" | nil } +env_date = { nstring } +env_from = { "(" ~ address{1,} ~ ")" | nil } +env_in_reply_to = { nstring } +env_message_id = { nstring } +env_reply_to = { "(" ~ address{1,} ~ ")" | nil } +env_sender = { "(" ~ address{1,} ~ ")" | nil } +env_subject = { nstring } +env_to = { "(" ~ address{1,} ~ ")" | nil } +envelope = { "(" ~ env_date ~ sp ~ env_subject ~ sp ~ env_from ~ sp ~ env_sender ~ sp ~ env_reply_to ~ sp ~ env_to ~ sp ~ env_cc ~ sp ~ env_bcc ~ sp ~ env_in_reply_to ~ sp ~ env_message_id ~ ")" } +flag = { "\\Answered" | "\\Flagged" | "\\Deleted" | "\\Seen" | "\\Draft" | flag_keyword | flag_extension } +flag_extension = @{ "\\" ~ atom } +flag_fetch = { flag | "\\Recent" } +flag_keyword = @{ atom } +flag_list = { "(" ~ (flag ~ (sp ~ flag)*)? ~ ")" } +flag_perm = { flag | "\\*" } +header_fld_name = { astring } +header_list = { "(" ~ header_fld_name ~ (sp ~ header_fld_name)* ~ ")" } list_wildcards = @{ "%" | "*" } +literal = @{ "{" ~ number ~ "}" ~ crlf ~ char8* } +mailbox = { ^"INBOX" | astring } +mailbox_data = { (^"FLAGS" ~ sp ~ flag_list) | (^"LIST" ~ sp ~ mailbox_list) | (^"LSUB" ~ sp ~ mailbox_list) | (^"SEARCH" ~ (sp ~ nz_number)*) | (^"STATUS" ~ sp ~ mailbox ~ sp ~ ^"(" ~ status_att_list? ~ ^")") | (number ~ sp ~ ^"EXISTS") | (number ~ sp ~ ^"RECENT") } +mailbox_list = { "(" ~ mbx_list_flags* ~ ")" ~ sp ~ (dquote ~ quoted_char ~ dquote | nil) ~ sp ~ mailbox } +mbx_list_flags = { (mbx_list_oflag ~ sp)* ~ mbx_list_sflag ~ (sp ~ mbx_list_oflag)* | mbx_list_oflag ~ (sp ~ mbx_list_oflag)* } +mbx_list_oflag = { "\\NoInferiors" | flag_extension } +mbx_list_sflag = { "\\NoSelect" | "\\Marked" | "\\Unmarked" } +media_basic = { ((dquote ~ ("APPLICATION" | "AUDIO" | "IMAGE" | "MESSAGE" | "VIDEO") ~ dquote) | string) ~ sp ~ media_subtype } +media_message = { dquote ~ "MESSAGE" ~ dquote ~ sp ~ dquote ~ "RFC822" ~ dquote } +media_subtype = { string } +media_text = { dquote ~ "TEXT" ~ dquote ~ sp ~ media_subtype } +message_data = { nz_number ~ sp ~ (^"EXPUNGE" | (^"FETCH" ~ sp ~ msg_att)) } +msg_att = { "(" ~ (msg_att_dynamic | msg_att_static) ~ (sp ~ (msg_att_dynamic | msg_att_static))* ~ ")" } +msg_att_dynamic = { ^"FLAGS" ~ sp ~ "(" ~ (flag_fetch ~ (sp ~ flag_fetch)*)? ~ ")" } +msg_att_static = { (^"ENVELOPE" ~ sp ~ envelope) | (^"INTERNALDATE" ~ sp ~ date_time) | (^"RFC822" ~ (^".HEADER" | ^".TEXT") ~ sp ~ nstring) | (^"RFC822.SIZE" ~ sp ~ number) | (^"BODY" ~ ^"STRUCTURE"? ~ sp ~ body) | (^"BODY" ~ section ~ ("<" ~ number ~ ">")? ~ sp ~ nstring) | (^"UID" ~ sp ~ uniqueid) } +nil = { ^"NIL" } +nstring = { string | nil } +number = @{ digit{1,} } +nz_number = @{ digit_nz ~ digit* } +quoted = @{ dquote ~ quoted_char* ~ dquote } +quoted_char = @{ (!quoted_specials ~ char) | ("\\" ~ quoted_specials) } quoted_specials = @{ dquote | "\\" } +resp_cond_bye = { ^"BYE" ~ sp ~ resp_text } +resp_cond_state = { (^"OK" | ^"NO" | ^"BAD") ~ resp_text } resp_specials = @{ "]" } -nil = { "NIL" } +resp_text = { ("[" ~ resp_text_code ~ "]" ~ sp)? ~ text } +resp_text_code = { ^"ALERT" | (^"BADCHARSET" ~ (sp ~ "(" ~ astring ~ (sp ~ astring)* ~ ")")?) | capability_data | ^"PARSE" | (^"PERMANENTFLAGS" ~ sp ~ "(" ~ (flag_perm ~ (sp ~ flag_perm)*)? ~ ")") | ^"READ-ONLY" | ^"READ-WRITE" | ^"TRYCREATE" | (^"UIDNEXT" ~ sp ~ nz_number) | (^"UIDVALIDITY" ~ sp ~ nz_number) | (^"UNSEEN" ~ sp ~ nz_number) | (atom ~ (sp ~ resp_text_code_atom)?) } +resp_text_code_atom = @{ (!"]" ~ text_char){1,} } +response = { continue_req | response_data | response_done } +response_data = { "*" ~ sp ~ (resp_cond_state | resp_cond_bye | mailbox_data | message_data | capability_data) ~ crlf } +response_done = { response_tagged | response_fatal } +response_fatal = { "*" ~ sp ~ resp_cond_bye ~ crlf } +response_tagged = { tag ~ sp ~ resp_cond_state ~ crlf } +section = { "[" ~ section_spec? ~ "]" } +section_msgtext = { ^"HEADER" | (^"HEADER.FIELDS" ~ ^".NOT"? ~ sp ~ header_list) | ^"TEXT" } +section_part = { nz_number ~ ("." ~ nz_number)* } +section_spec = { section_msgtext | (section_part ~ ("." ~ section_text)?) } +section_text = { section_msgtext | "MIME" } +status_att = { ^"MESSAGES" | ^"RECENT" | ^"UIDNEXT" | ^"UIDVALIDITY" | ^"UNSEEN" } +status_att_list = { status_att ~ sp ~ number ~ (sp ~ status_att ~ sp ~ number)* } +string = @{ quoted | literal } +tag = @{ tag_char{1,} } +tag_char = @{ !"+" ~ astring_char } +text = @{ text_char{1,} } +text_char = @{ !cr ~ !lf ~ char } +time = { digit{2} ~ ":" ~ digit{2} ~ ":" ~ digit{2} } +uniqueid = { nz_number } +zone = { ("+" | "-") ~ digit{4} } // core rules from https://tools.ietf.org/html/rfc2234#section-6.1 +alpha = @{ '\x41'..'\x5a' | '\x61'..'\x7a' } char = @{ '\x01'..'\x7f' } +cr = @{ "\x0d" } +crlf = @{ cr ~ lf } ctl = @{ '\x00'..'\x1f' | "\x7f" } +digit = @{ '\x30'..'\x39' } dquote = @{ "\"" } +lf = @{ "\x0a" } sp = @{ " " } From fed8031f5c24046ab156bafcf939f34b4cfbbeb3 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Mon, 22 Feb 2021 16:56:23 -0600 Subject: [PATCH 06/11] more work on response parser --- imap/src/parser/mod.rs | 70 +++++++++++++++++++++++++++++++++--- imap/src/parser/rfc3501.pest | 5 +-- imap/src/response/mod.rs | 10 +++--- 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/imap/src/parser/mod.rs b/imap/src/parser/mod.rs index 88f1d52..2c02b9a 100644 --- a/imap/src/parser/mod.rs +++ b/imap/src/parser/mod.rs @@ -1,4 +1,4 @@ -use pest::{error::Error, Parser}; +use pest::{error::Error, Parser, iterators::{Pair, Pairs}}; use crate::response::*; @@ -11,8 +11,8 @@ pub fn parse_capability(s: &str) -> Result> { let pair = pairs.next().unwrap(); let cap = match pair.as_rule() { Rule::capability => { - let mut inner = pair.into_inner(); - let pair = inner.next().unwrap(); + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); match pair.as_rule() { Rule::auth_type => Capability::Auth(pair.as_str().to_uppercase().to_owned()), Rule::atom => match pair.as_str() { @@ -28,7 +28,57 @@ pub fn parse_capability(s: &str) -> Result> { } pub fn parse_response(s: &str) -> Result> { - todo!() + let mut pairs = Rfc3501::parse(Rule::response, s)?; + let pair = pairs.next().unwrap(); + Ok(build_response(pair)) +} + +fn build_response(pair: Pair) -> Response { + match pair.as_rule() { + Rule::response => { + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + match pair.as_rule() { + Rule::response_data => { + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + match pair.as_rule() { + Rule::resp_cond_state => { + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + let status = build_status(pair); + let mut code = None; + let mut information = None; + + for pair in pairs { + if let resp_text = pair.as_rule() { + information = Some(pair.as_str().to_owned()); + } + } + Response::Data { status, code, information } + } + _ => unreachable!("{:?}", pair), + } + } + _ => unreachable!("{:?}", pair), + } + } + _ => unreachable!("{:?}", pair), + } +} + +fn build_status(pair: Pair) -> Status { + match pair.as_rule() { + Rule::resp_status => { + match pair.as_str().to_uppercase().as_str() { + "OK" => Status::Ok, + "NO" => Status::No, + "BAD" => Status::Bad, + s => unreachable!("invalid status {:?}", s), + } + } + _ => unreachable!("{:?}", pair), + } } #[cfg(test)] @@ -54,4 +104,16 @@ mod tests { 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 { + status: Status::Ok, + code: None, + information: Some("IMAP4rev1 Service Ready".to_owned()), + })); + } } diff --git a/imap/src/parser/rfc3501.pest b/imap/src/parser/rfc3501.pest index efde2c1..b9b6270 100644 --- a/imap/src/parser/rfc3501.pest +++ b/imap/src/parser/rfc3501.pest @@ -85,7 +85,8 @@ quoted = @{ dquote ~ quoted_char* ~ dquote } quoted_char = @{ (!quoted_specials ~ char) | ("\\" ~ quoted_specials) } quoted_specials = @{ dquote | "\\" } resp_cond_bye = { ^"BYE" ~ sp ~ resp_text } -resp_cond_state = { (^"OK" | ^"NO" | ^"BAD") ~ resp_text } +resp_cond_state = { resp_status ~ sp ~ resp_text } +resp_status = { (^"OK" | ^"NO" | ^"BAD") } resp_specials = @{ "]" } resp_text = { ("[" ~ resp_text_code ~ "]" ~ sp)? ~ text } resp_text_code = { ^"ALERT" | (^"BADCHARSET" ~ (sp ~ "(" ~ astring ~ (sp ~ astring)* ~ ")")?) | capability_data | ^"PARSE" | (^"PERMANENTFLAGS" ~ sp ~ "(" ~ (flag_perm ~ (sp ~ flag_perm)*)? ~ ")") | ^"READ-ONLY" | ^"READ-WRITE" | ^"TRYCREATE" | (^"UIDNEXT" ~ sp ~ nz_number) | (^"UIDVALIDITY" ~ sp ~ nz_number) | (^"UNSEEN" ~ sp ~ nz_number) | (atom ~ (sp ~ resp_text_code_atom)?) } @@ -120,5 +121,5 @@ ctl = @{ '\x00'..'\x1f' | "\x7f" } digit = @{ '\x30'..'\x39' } dquote = @{ "\"" } lf = @{ "\x0a" } -sp = @{ " " } +sp = _{ " " } diff --git a/imap/src/response/mod.rs b/imap/src/response/mod.rs index df8af08..b8be675 100644 --- a/imap/src/response/mod.rs +++ b/imap/src/response/mod.rs @@ -1,6 +1,6 @@ use std::ops::RangeInclusive; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Response { Capabilities(Vec), Continue { @@ -34,7 +34,7 @@ pub enum Capability { Atom(String), } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum ResponseCode { Alert, BadCharset(Option>), @@ -53,16 +53,16 @@ pub enum ResponseCode { UidNotSticky, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum UidSetMember { UidRange(RangeInclusive), Uid(u32), } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum AttributeValue {} -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum MailboxDatum {} #[derive(Clone, Debug, Eq, PartialEq)] From 1276ead25f0e4e0e2137511f0ab93aa6b3b02122 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Mon, 22 Feb 2021 17:23:13 -0600 Subject: [PATCH 07/11] mailbox_data --- imap/src/parser/mod.rs | 128 ++++++++++++++++++++++++++--------- imap/src/parser/rfc3501.pest | 5 +- imap/src/response/mod.rs | 43 +++++++++++- 3 files changed, 140 insertions(+), 36 deletions(-) diff --git a/imap/src/parser/mod.rs b/imap/src/parser/mod.rs index 2c02b9a..16575ba 100644 --- a/imap/src/parser/mod.rs +++ b/imap/src/parser/mod.rs @@ -1,4 +1,8 @@ -use pest::{error::Error, Parser, iterators::{Pair, Pairs}}; +use pest::{ + error::Error, + iterators::{Pair, Pairs}, + Parser, +}; use crate::response::*; @@ -6,8 +10,8 @@ use crate::response::*; #[grammar = "parser/rfc3501.pest"] struct Rfc3501; -pub fn parse_capability(s: &str) -> Result> { - let mut pairs = Rfc3501::parse(Rule::capability, s)?; +pub fn parse_capability(s: impl AsRef) -> Result> { + let mut pairs = Rfc3501::parse(Rule::capability, s.as_ref())?; let pair = pairs.next().unwrap(); let cap = match pair.as_rule() { Rule::capability => { @@ -27,60 +31,111 @@ pub fn parse_capability(s: &str) -> Result> { Ok(cap) } -pub fn parse_response(s: &str) -> Result> { - let mut pairs = Rfc3501::parse(Rule::response, s)?; +pub fn parse_response(s: impl AsRef) -> Result> { + let mut pairs = Rfc3501::parse(Rule::response, s.as_ref())?; let pair = pairs.next().unwrap(); Ok(build_response(pair)) } fn build_response(pair: Pair) -> Response { + if !matches!(pair.as_rule(), Rule::response) { + unreachable!("{:#?}", pair); + } + + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); match pair.as_rule() { - Rule::response => { + Rule::response_done => { let mut pairs = pair.into_inner(); let pair = pairs.next().unwrap(); match pair.as_rule() { - Rule::response_data => { + Rule::response_tagged => { let mut pairs = pair.into_inner(); let pair = pairs.next().unwrap(); - match pair.as_rule() { - Rule::resp_cond_state => { - let mut pairs = pair.into_inner(); - let pair = pairs.next().unwrap(); - let status = build_status(pair); - let mut code = None; - let mut information = None; + let tag = pair.as_str().to_owned(); - for pair in pairs { - if let resp_text = pair.as_rule() { - information = Some(pair.as_str().to_owned()); - } - } - Response::Data { status, code, information } - } - _ => unreachable!("{:?}", pair), + let pair = pairs.next().unwrap(); + let (status, code, information) = build_resp_cond_state(pair); + Response::Done { + tag, + status, + code, + information, } } - _ => unreachable!("{:?}", pair), + _ => unreachable!("{:#?}", pair), } } - _ => unreachable!("{:?}", pair), + Rule::response_data => { + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + match pair.as_rule() { + Rule::resp_cond_state => { + let (status, code, information) = build_resp_cond_state(pair); + Response::Data { + status, + code, + information, + } + } + Rule::mailbox_data => Response::MailboxData(build_mailbox_data(pair)), + _ => unreachable!("{:#?}", pair), + } + } + _ => unreachable!("{:#?}", pair), } } +fn build_resp_cond_state(pair: Pair) -> (Status, Option, Option) { + if !matches!(pair.as_rule(), Rule::resp_cond_state) { + unreachable!("{:#?}", pair); + } + + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + let status = build_status(pair); + let mut code = None; + let mut information = None; + + for pair in pairs { + match pair.as_rule() { + Rule::resp_text => information = Some(pair.as_str().to_owned()), + _ => unreachable!("{:#?}", pair), + } + } + (status, code, information) +} + fn build_status(pair: Pair) -> Status { match pair.as_rule() { - Rule::resp_status => { - match pair.as_str().to_uppercase().as_str() { - "OK" => Status::Ok, - "NO" => Status::No, - "BAD" => Status::Bad, - s => unreachable!("invalid status {:?}", s), - } - } + Rule::resp_status => match pair.as_str().to_uppercase().as_str() { + "OK" => Status::Ok, + "NO" => Status::No, + "BAD" => Status::Bad, + s => unreachable!("invalid status {:?}", s), + }, _ => unreachable!("{:?}", pair), } } +fn build_mailbox_data(pair: Pair) -> MailboxData { + if !matches!(pair.as_rule(), Rule::mailbox_data) { + unreachable!("{:#?}", pair); + } + + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + match pair.as_rule() { + Rule::mailbox_data_exists => { + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + let number = pair.as_str().parse::().unwrap(); + MailboxData::Exists(number) + } + _ => unreachable!("{:#?}", pair), + } +} + #[cfg(test)] #[rustfmt::skip] mod tests { @@ -115,5 +170,14 @@ mod tests { code: None, information: Some("IMAP4rev1 Service Ready".to_owned()), })); + + assert_eq!(parse_response("a001 OK LOGIN completed\r\n"), Ok(Response::Done { + 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)))); } } diff --git a/imap/src/parser/rfc3501.pest b/imap/src/parser/rfc3501.pest index b9b6270..69e38a3 100644 --- a/imap/src/parser/rfc3501.pest +++ b/imap/src/parser/rfc3501.pest @@ -64,7 +64,8 @@ header_list = { "(" ~ header_fld_name ~ (sp ~ header_fld_name)* ~ ")" } list_wildcards = @{ "%" | "*" } literal = @{ "{" ~ number ~ "}" ~ crlf ~ char8* } mailbox = { ^"INBOX" | astring } -mailbox_data = { (^"FLAGS" ~ sp ~ flag_list) | (^"LIST" ~ sp ~ mailbox_list) | (^"LSUB" ~ sp ~ mailbox_list) | (^"SEARCH" ~ (sp ~ nz_number)*) | (^"STATUS" ~ sp ~ mailbox ~ sp ~ ^"(" ~ status_att_list? ~ ^")") | (number ~ sp ~ ^"EXISTS") | (number ~ sp ~ ^"RECENT") } +mailbox_data = { (^"FLAGS" ~ sp ~ flag_list) | (^"LIST" ~ sp ~ mailbox_list) | (^"LSUB" ~ sp ~ mailbox_list) | (^"SEARCH" ~ (sp ~ nz_number)*) | (^"STATUS" ~ sp ~ mailbox ~ sp ~ ^"(" ~ status_att_list? ~ ^")") | mailbox_data_exists | (number ~ sp ~ ^"RECENT") } +mailbox_data_exists = { number ~ sp ~ ^"EXISTS" } mailbox_list = { "(" ~ mbx_list_flags* ~ ")" ~ sp ~ (dquote ~ quoted_char ~ dquote | nil) ~ sp ~ mailbox } mbx_list_flags = { (mbx_list_oflag ~ sp)* ~ mbx_list_sflag ~ (sp ~ mbx_list_oflag)* | mbx_list_oflag ~ (sp ~ mbx_list_oflag)* } mbx_list_oflag = { "\\NoInferiors" | flag_extension } @@ -116,7 +117,7 @@ zone = { ("+" | "-") ~ digit{4} } alpha = @{ '\x41'..'\x5a' | '\x61'..'\x7a' } char = @{ '\x01'..'\x7f' } cr = @{ "\x0d" } -crlf = @{ cr ~ lf } +crlf = _{ cr ~ lf } ctl = @{ '\x00'..'\x1f' | "\x7f" } digit = @{ '\x30'..'\x39' } dquote = @{ "\"" } diff --git a/imap/src/response/mod.rs b/imap/src/response/mod.rs index b8be675..026f650 100644 --- a/imap/src/response/mod.rs +++ b/imap/src/response/mod.rs @@ -24,7 +24,7 @@ pub enum Response { uids: Vec>, }, Fetch(u32, Vec), - MailboxData(MailboxDatum), + MailboxData(MailboxData), } #[derive(Clone, Debug, Hash, PartialEq, Eq)] @@ -63,7 +63,46 @@ pub enum UidSetMember { pub enum AttributeValue {} #[derive(Clone, Debug, PartialEq, Eq)] -pub enum MailboxDatum {} +pub enum MailboxData { + Exists(u32), + Flags(Vec), + List { + flags: Vec, + delimiter: Option, + name: String, + }, + Search(Vec), + Status { + mailbox: String, + status: Vec, + }, + Recent(u32), + MetadataSolicited { + mailbox: String, + values: Vec, + }, + MetadataUnsolicited { + mailbox: String, + values: Vec, + }, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct Metadata { + pub entry: String, + pub value: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum StatusAttribute { + HighestModSeq(u64), // RFC 4551 + Messages(u32), + Recent(u32), + UidNext(u32), + UidValidity(u32), + Unseen(u32), +} #[derive(Clone, Debug, Eq, PartialEq)] pub enum Status { From 0dac4f4e965647918252894ff92540ed3a74d6ea Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Mon, 22 Feb 2021 17:39:50 -0600 Subject: [PATCH 08/11] flags --- imap/src/parser/mod.rs | 77 ++++++++++++++++++++++++++++++------ imap/src/parser/rfc3501.pest | 3 +- imap/src/response/mod.rs | 12 +++++- 3 files changed, 77 insertions(+), 15 deletions(-) diff --git a/imap/src/parser/mod.rs b/imap/src/parser/mod.rs index 16575ba..1a92245 100644 --- a/imap/src/parser/mod.rs +++ b/imap/src/parser/mod.rs @@ -118,6 +118,30 @@ fn build_status(pair: Pair) -> Status { } } +fn build_flag_list(pair: Pair) -> Vec { + if !matches!(pair.as_rule(), Rule::flag_list) { + unreachable!("{:#?}", pair); + } + + pair.into_inner().map(build_flag).collect() +} + +fn build_flag(pair: Pair) -> Flag { + if !matches!(pair.as_rule(), Rule::flag) { + unreachable!("{:#?}", pair); + } + + match pair.as_str() { + "\\Answered" => Flag::Answered, + "\\Flagged" => Flag::Flagged, + "\\Deleted" => Flag::Deleted, + "\\Seen" => Flag::Seen, + "\\Draft" => Flag::Draft, + s if s.starts_with("\\") => Flag::Ext(s.to_owned()), + _ => unreachable!("{:#?}", pair.as_str()), + } +} + fn build_mailbox_data(pair: Pair) -> MailboxData { if !matches!(pair.as_rule(), Rule::mailbox_data) { unreachable!("{:#?}", pair); @@ -132,18 +156,24 @@ fn build_mailbox_data(pair: Pair) -> MailboxData { let number = pair.as_str().parse::().unwrap(); MailboxData::Exists(number) } + Rule::mailbox_data_flags => { + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + let flags = build_flag_list(pair); + MailboxData::Flags(flags) + } _ => unreachable!("{:#?}", pair), } } #[cfg(test)] -#[rustfmt::skip] mod tests { use super::*; use crate::response::*; use pest::Parser; #[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()))); @@ -155,6 +185,7 @@ mod tests { } #[test] +#[rustfmt::skip] fn test_nil() { assert!(Rfc3501::parse(Rule::nil, "NIL").is_ok()); assert!(Rfc3501::parse(Rule::nil, "anything else").is_err()); @@ -165,19 +196,39 @@ mod tests { // 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 { - status: Status::Ok, - code: None, - information: Some("IMAP4rev1 Service Ready".to_owned()), - })); + assert_eq!( + parse_response("* OK IMAP4rev1 Service Ready\r\n"), + Ok(Response::Data { + 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 { - tag: "a001".to_owned(), - status: Status::Ok, - code: None, - information: Some("LOGIN completed".to_owned()), - })); + assert_eq!( + parse_response("a001 OK LOGIN completed\r\n"), + Ok(Response::Done { + 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("* 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![ + Flag::Answered, + Flag::Flagged, + Flag::Deleted, + Flag::Seen, + Flag::Draft, + ]))) + ); } } diff --git a/imap/src/parser/rfc3501.pest b/imap/src/parser/rfc3501.pest index 69e38a3..44929fa 100644 --- a/imap/src/parser/rfc3501.pest +++ b/imap/src/parser/rfc3501.pest @@ -64,8 +64,9 @@ header_list = { "(" ~ header_fld_name ~ (sp ~ header_fld_name)* ~ ")" } list_wildcards = @{ "%" | "*" } literal = @{ "{" ~ number ~ "}" ~ crlf ~ char8* } mailbox = { ^"INBOX" | astring } -mailbox_data = { (^"FLAGS" ~ sp ~ flag_list) | (^"LIST" ~ sp ~ mailbox_list) | (^"LSUB" ~ sp ~ mailbox_list) | (^"SEARCH" ~ (sp ~ nz_number)*) | (^"STATUS" ~ sp ~ mailbox ~ sp ~ ^"(" ~ status_att_list? ~ ^")") | mailbox_data_exists | (number ~ sp ~ ^"RECENT") } +mailbox_data = { mailbox_data_flags | (^"LIST" ~ sp ~ mailbox_list) | (^"LSUB" ~ sp ~ mailbox_list) | (^"SEARCH" ~ (sp ~ nz_number)*) | (^"STATUS" ~ sp ~ mailbox ~ sp ~ ^"(" ~ status_att_list? ~ ^")") | mailbox_data_exists | (number ~ sp ~ ^"RECENT") } mailbox_data_exists = { number ~ sp ~ ^"EXISTS" } +mailbox_data_flags = { ^"FLAGS" ~ sp ~ flag_list } mailbox_list = { "(" ~ mbx_list_flags* ~ ")" ~ sp ~ (dquote ~ quoted_char ~ dquote | nil) ~ sp ~ mailbox } mbx_list_flags = { (mbx_list_oflag ~ sp)* ~ mbx_list_sflag ~ (sp ~ mbx_list_oflag)* | mbx_list_oflag ~ (sp ~ mbx_list_oflag)* } mbx_list_oflag = { "\\NoInferiors" | flag_extension } diff --git a/imap/src/response/mod.rs b/imap/src/response/mod.rs index 026f650..c7adfa4 100644 --- a/imap/src/response/mod.rs +++ b/imap/src/response/mod.rs @@ -65,7 +65,7 @@ pub enum AttributeValue {} #[derive(Clone, Debug, PartialEq, Eq)] pub enum MailboxData { Exists(u32), - Flags(Vec), + Flags(Vec), List { flags: Vec, delimiter: Option, @@ -87,6 +87,16 @@ pub enum MailboxData { }, } +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum Flag { + Answered, + Flagged, + Deleted, + Seen, + Draft, + Ext(String), +} + #[derive(Debug, Eq, PartialEq, Clone)] pub struct Metadata { pub entry: String, From 3d0724e342f9f4c846e924d7a4e763ba4ab6194e Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Mon, 22 Feb 2021 17:51:59 -0600 Subject: [PATCH 09/11] asdf --- imap/.ignore | 2 +- imap/src/parser/mod.rs | 45 +++++++++++++++++++++++++++++++++++- imap/src/parser/rfc3501.pest | 8 ++++--- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/imap/.ignore b/imap/.ignore index 54d47a5..a5a0c0b 100644 --- a/imap/.ignore +++ b/imap/.ignore @@ -1,2 +1,2 @@ src/builders -src/parser +src/oldparser diff --git a/imap/src/parser/mod.rs b/imap/src/parser/mod.rs index 1a92245..f79de63 100644 --- a/imap/src/parser/mod.rs +++ b/imap/src/parser/mod.rs @@ -97,15 +97,38 @@ fn build_resp_cond_state(pair: Pair) -> (Status, Option, Opt let mut code = None; let mut information = None; + println!("pairs: {:#?}", pairs); + let pair = pairs.next().unwrap(); + let mut pairs = pair.into_inner(); for pair in pairs { match pair.as_rule() { - Rule::resp_text => information = Some(pair.as_str().to_owned()), + Rule::resp_text_code => code = Some(build_resp_code(pair)), + Rule::text => information = Some(pair.as_str().to_owned()), _ => unreachable!("{:#?}", pair), } } + (status, code, information) } +fn build_resp_code(pair: Pair) -> ResponseCode { + if !matches!(pair.as_rule(), Rule::resp_text_code) { + unreachable!("{:#?}", pair); + } + + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + match pair.as_rule() { + Rule::resp_text_code_unseen => { + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + let number = pair.as_str().parse::().unwrap(); + ResponseCode::Unseen(number) + } + _ => unreachable!("{:#?}", pair), + } +} + fn build_status(pair: Pair) -> Status { match pair.as_rule() { Rule::resp_status => match pair.as_str().to_uppercase().as_str() { @@ -162,6 +185,12 @@ fn build_mailbox_data(pair: Pair) -> MailboxData { let flags = build_flag_list(pair); MailboxData::Flags(flags) } + Rule::mailbox_data_recent => { + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + let number = pair.as_str().parse::().unwrap(); + MailboxData::Recent(number) + } _ => unreachable!("{:#?}", pair), } } @@ -230,5 +259,19 @@ mod tests { Flag::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 { + status: Status::Ok, + code: Some(ResponseCode::Unseen(17)), + information: Some("Message 17 is the first unseen message".to_owned()), + }) + ); } } diff --git a/imap/src/parser/rfc3501.pest b/imap/src/parser/rfc3501.pest index 44929fa..8fb67b9 100644 --- a/imap/src/parser/rfc3501.pest +++ b/imap/src/parser/rfc3501.pest @@ -64,9 +64,10 @@ header_list = { "(" ~ header_fld_name ~ (sp ~ header_fld_name)* ~ ")" } list_wildcards = @{ "%" | "*" } literal = @{ "{" ~ number ~ "}" ~ crlf ~ char8* } mailbox = { ^"INBOX" | astring } -mailbox_data = { mailbox_data_flags | (^"LIST" ~ sp ~ mailbox_list) | (^"LSUB" ~ sp ~ mailbox_list) | (^"SEARCH" ~ (sp ~ nz_number)*) | (^"STATUS" ~ sp ~ mailbox ~ sp ~ ^"(" ~ status_att_list? ~ ^")") | mailbox_data_exists | (number ~ sp ~ ^"RECENT") } +mailbox_data = { mailbox_data_flags | (^"LIST" ~ sp ~ mailbox_list) | (^"LSUB" ~ sp ~ mailbox_list) | (^"SEARCH" ~ (sp ~ nz_number)*) | (^"STATUS" ~ sp ~ mailbox ~ sp ~ ^"(" ~ status_att_list? ~ ^")") | mailbox_data_exists | mailbox_data_recent } mailbox_data_exists = { number ~ sp ~ ^"EXISTS" } mailbox_data_flags = { ^"FLAGS" ~ sp ~ flag_list } +mailbox_data_recent = { number ~ sp ~ ^"RECENT" } mailbox_list = { "(" ~ mbx_list_flags* ~ ")" ~ sp ~ (dquote ~ quoted_char ~ dquote | nil) ~ sp ~ mailbox } mbx_list_flags = { (mbx_list_oflag ~ sp)* ~ mbx_list_sflag ~ (sp ~ mbx_list_oflag)* | mbx_list_oflag ~ (sp ~ mbx_list_oflag)* } mbx_list_oflag = { "\\NoInferiors" | flag_extension } @@ -88,10 +89,11 @@ quoted_char = @{ (!quoted_specials ~ char) | ("\\" ~ quoted_specials) } quoted_specials = @{ dquote | "\\" } resp_cond_bye = { ^"BYE" ~ sp ~ resp_text } resp_cond_state = { resp_status ~ sp ~ resp_text } -resp_status = { (^"OK" | ^"NO" | ^"BAD") } resp_specials = @{ "]" } +resp_status = { (^"OK" | ^"NO" | ^"BAD") } resp_text = { ("[" ~ resp_text_code ~ "]" ~ sp)? ~ text } -resp_text_code = { ^"ALERT" | (^"BADCHARSET" ~ (sp ~ "(" ~ astring ~ (sp ~ astring)* ~ ")")?) | capability_data | ^"PARSE" | (^"PERMANENTFLAGS" ~ sp ~ "(" ~ (flag_perm ~ (sp ~ flag_perm)*)? ~ ")") | ^"READ-ONLY" | ^"READ-WRITE" | ^"TRYCREATE" | (^"UIDNEXT" ~ sp ~ nz_number) | (^"UIDVALIDITY" ~ sp ~ nz_number) | (^"UNSEEN" ~ sp ~ nz_number) | (atom ~ (sp ~ resp_text_code_atom)?) } +resp_text_code = { ^"ALERT" | (^"BADCHARSET" ~ (sp ~ "(" ~ astring ~ (sp ~ astring)* ~ ")")?) | capability_data | ^"PARSE" | (^"PERMANENTFLAGS" ~ sp ~ "(" ~ (flag_perm ~ (sp ~ flag_perm)*)? ~ ")") | ^"READ-ONLY" | ^"READ-WRITE" | ^"TRYCREATE" | (^"UIDNEXT" ~ sp ~ nz_number) | (^"UIDVALIDITY" ~ sp ~ nz_number) | resp_text_code_unseen | (atom ~ (sp ~ resp_text_code_atom)?) } +resp_text_code_unseen = { ^"UNSEEN" ~ sp ~ nz_number } resp_text_code_atom = @{ (!"]" ~ text_char){1,} } response = { continue_req | response_data | response_done } response_data = { "*" ~ sp ~ (resp_cond_state | resp_cond_bye | mailbox_data | message_data | capability_data) ~ crlf } From 27818bd93a3638e4e77171f7b4bcde610933878e Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Mon, 22 Feb 2021 18:21:12 -0600 Subject: [PATCH 10/11] ouais --- README.md | 1 + imap/src/parser/mod.rs | 67 +++++++++++++++++++++++------------- imap/src/parser/rfc3501.pest | 6 ++-- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 5684d47..c8f1b68 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Goals: Stretch goals: - Unified "feed" that any app can submit to. - Submit notifications to gotify-shaped notification servers. +- JMAP implementation. - RSS aggregator. - IRC client?? diff --git a/imap/src/parser/mod.rs b/imap/src/parser/mod.rs index f79de63..59a13c8 100644 --- a/imap/src/parser/mod.rs +++ b/imap/src/parser/mod.rs @@ -1,3 +1,6 @@ +use std::fmt::Debug; +use std::str::FromStr; + use pest::{ error::Error, iterators::{Pair, Pairs}, @@ -99,10 +102,10 @@ fn build_resp_cond_state(pair: Pair) -> (Status, Option, Opt println!("pairs: {:#?}", pairs); let pair = pairs.next().unwrap(); - let mut pairs = pair.into_inner(); + let pairs = pair.into_inner(); for pair in pairs { match pair.as_rule() { - Rule::resp_text_code => code = Some(build_resp_code(pair)), + Rule::resp_text_code => code = build_resp_code(pair), Rule::text => information = Some(pair.as_str().to_owned()), _ => unreachable!("{:#?}", pair), } @@ -111,22 +114,19 @@ fn build_resp_cond_state(pair: Pair) -> (Status, Option, Opt (status, code, information) } -fn build_resp_code(pair: Pair) -> ResponseCode { +fn build_resp_code(pair: Pair) -> Option { if !matches!(pair.as_rule(), Rule::resp_text_code) { unreachable!("{:#?}", pair); } let mut pairs = pair.into_inner(); - let pair = pairs.next().unwrap(); - match pair.as_rule() { - Rule::resp_text_code_unseen => { - let mut pairs = pair.into_inner(); - let pair = pairs.next().unwrap(); - let number = pair.as_str().parse::().unwrap(); - ResponseCode::Unseen(number) - } + let pair = pairs.next()?; + Some(match pair.as_rule() { + Rule::resp_text_code_readwrite => ResponseCode::ReadWrite, + Rule::resp_text_code_uidvalidity => ResponseCode::UidValidity(build_number(pair)), + Rule::resp_text_code_unseen => ResponseCode::Unseen(build_number(pair)), _ => unreachable!("{:#?}", pair), - } + }) } fn build_status(pair: Pair) -> Status { @@ -173,28 +173,28 @@ fn build_mailbox_data(pair: Pair) -> MailboxData { let mut pairs = pair.into_inner(); let pair = pairs.next().unwrap(); match pair.as_rule() { - Rule::mailbox_data_exists => { - let mut pairs = pair.into_inner(); - let pair = pairs.next().unwrap(); - let number = pair.as_str().parse::().unwrap(); - MailboxData::Exists(number) - } + Rule::mailbox_data_exists => MailboxData::Exists(build_number(pair)), Rule::mailbox_data_flags => { let mut pairs = pair.into_inner(); let pair = pairs.next().unwrap(); let flags = build_flag_list(pair); MailboxData::Flags(flags) } - Rule::mailbox_data_recent => { - let mut pairs = pair.into_inner(); - let pair = pairs.next().unwrap(); - let number = pair.as_str().parse::().unwrap(); - MailboxData::Recent(number) - } + Rule::mailbox_data_recent => MailboxData::Recent(build_number(pair)), _ => unreachable!("{:#?}", pair), } } +fn build_number(pair: Pair) -> T +where + T: FromStr, + T::Err: Debug, +{ + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + pair.as_str().parse::().unwrap() +} + #[cfg(test)] mod tests { use super::*; @@ -273,5 +273,24 @@ mod tests { 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 { + 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 { + tag: "a002".to_owned(), + status: Status::Ok, + code: Some(ResponseCode::ReadWrite), + information: Some("SELECT completed".to_owned()), + }) + ); } } diff --git a/imap/src/parser/rfc3501.pest b/imap/src/parser/rfc3501.pest index 8fb67b9..d72904f 100644 --- a/imap/src/parser/rfc3501.pest +++ b/imap/src/parser/rfc3501.pest @@ -92,9 +92,11 @@ resp_cond_state = { resp_status ~ sp ~ resp_text } resp_specials = @{ "]" } resp_status = { (^"OK" | ^"NO" | ^"BAD") } resp_text = { ("[" ~ resp_text_code ~ "]" ~ sp)? ~ text } -resp_text_code = { ^"ALERT" | (^"BADCHARSET" ~ (sp ~ "(" ~ astring ~ (sp ~ astring)* ~ ")")?) | capability_data | ^"PARSE" | (^"PERMANENTFLAGS" ~ sp ~ "(" ~ (flag_perm ~ (sp ~ flag_perm)*)? ~ ")") | ^"READ-ONLY" | ^"READ-WRITE" | ^"TRYCREATE" | (^"UIDNEXT" ~ sp ~ nz_number) | (^"UIDVALIDITY" ~ sp ~ nz_number) | resp_text_code_unseen | (atom ~ (sp ~ resp_text_code_atom)?) } -resp_text_code_unseen = { ^"UNSEEN" ~ sp ~ nz_number } +resp_text_code = { ^"ALERT" | (^"BADCHARSET" ~ (sp ~ "(" ~ astring ~ (sp ~ astring)* ~ ")")?) | capability_data | ^"PARSE" | (^"PERMANENTFLAGS" ~ sp ~ "(" ~ (flag_perm ~ (sp ~ flag_perm)*)? ~ ")") | ^"READ-ONLY" | resp_text_code_readwrite | ^"TRYCREATE" | (^"UIDNEXT" ~ sp ~ nz_number) | resp_text_code_uidvalidity | resp_text_code_unseen | (atom ~ (sp ~ resp_text_code_atom)?) } resp_text_code_atom = @{ (!"]" ~ text_char){1,} } +resp_text_code_readwrite = { ^"READ-WRITE" } +resp_text_code_uidvalidity = { ^"UIDVALIDITY" ~ sp ~ nz_number } +resp_text_code_unseen = { ^"UNSEEN" ~ sp ~ nz_number } response = { continue_req | response_data | response_done } response_data = { "*" ~ sp ~ (resp_cond_state | resp_cond_bye | mailbox_data | message_data | capability_data) ~ crlf } response_done = { response_tagged | response_fatal } From f76e5d4753d0686f2020b170966183d85d459201 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Mon, 22 Feb 2021 20:47:00 -0600 Subject: [PATCH 11/11] authentication! --- imap/src/client/inner.rs | 134 +++++++++++++++++++---------------- imap/src/client/mod.rs | 19 +++-- imap/src/command/mod.rs | 2 + imap/src/parser/mod.rs | 60 +++++++++++----- imap/src/parser/rfc3501.pest | 2 +- src/mail/mod.rs | 10 ++- 6 files changed, 138 insertions(+), 89 deletions(-) diff --git a/imap/src/client/inner.rs b/imap/src/client/inner.rs index c529aa0..e62165c 100644 --- a/imap/src/client/inner.rs +++ b/imap/src/client/inner.rs @@ -1,11 +1,12 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::mem; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll, Waker}; use anyhow::{Context as AnyhowContext, Result}; use futures::future::{self, Either, Future, FutureExt}; -use parking_lot::RwLock; +use parking_lot::{RwLock, RwLockWriteGuard}; use tokio::{ io::{ self, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadHalf, WriteHalf, @@ -18,14 +19,15 @@ use tokio_rustls::{ }; use crate::command::Command; +use crate::parser::{parse_capability, parse_response}; use crate::response::{Capability, Response, ResponseCode, Status}; // use crate::types::{Capability as Capability_, Status}; use super::ClientConfig; pub type CapsLock = Arc>>>; -pub type ResultMap = Arc, Option)>>>; -pub type GreetingState = Arc)>>; +pub type ResultMap = Arc, Vec, Option)>>>; +pub type GreetingState = Arc, Option)>>; pub const TAG_PREFIX: &str = "panorama"; /// The lower-level Client struct, that is shared by all of the exported structs in the state machine. @@ -56,9 +58,9 @@ where /// Creates a new client that wraps a connection pub fn new(conn: C, config: ClientConfig) -> Self { let (read_half, write_half) = io::split(conn); - let results = Arc::new(RwLock::new(HashMap::new())); + let results = Arc::new(RwLock::new(VecDeque::new())); let (exit_tx, exit_rx) = mpsc::channel(1); - let greeting = Arc::new(RwLock::new((false, None))); + let greeting = Arc::new(RwLock::new((None, None))); let caps: CapsLock = Arc::new(RwLock::new(None)); let listener_handle = tokio::spawn(listen( @@ -88,13 +90,13 @@ where } /// Sends a command to the server and returns a handle to retrieve the result - pub async fn execute(&mut self, cmd: Command) -> Result { + pub async fn execute(&mut self, cmd: Command) -> Result<(Response, Vec)> { debug!("executing command {:?}", cmd); let id = self.id; self.id += 1; { let mut handlers = self.results.write(); - handlers.insert(id, (None, None)); + handlers.push_back((id, None, vec![], None)); } let cmd_str = format!("{}{} {}\r\n", TAG_PREFIX, id, cmd); @@ -103,23 +105,39 @@ where self.conn.flush().await?; debug!("[{}] written.", id); - ExecWaiter(self, id).await; - let resp = { - let mut handlers = self.results.write(); - handlers.remove(&id).unwrap().0.unwrap() - }; + let resp = ExecWaiter(self, id, false).await; + // let resp = { + // let mut handlers = self.results.write(); + // handlers.remove(&id).unwrap().0.unwrap() + // }; Ok(resp) } /// Executes the CAPABILITY command - pub async fn capabilities(&mut self) -> Result<()> { + pub async fn capabilities(&mut self, force: bool) -> Result<()> { + { + let caps = &*self.caps.read(); + if caps.is_some() && !force { + return Ok(()); + } + } + let cmd = Command::Capability; debug!("sending: {:?} {:?}", cmd, cmd.to_string()); - let result = self + let (result, intermediate) = self .execute(cmd) .await .context("error executing CAPABILITY command")?; debug!("cap resp: {:?}", result); + + if let Some(Response::Capabilities(new_caps)) = intermediate + .iter() + .find(|resp| matches!(resp, Response::Capabilities(_))) + { + let caps = &mut *self.caps.write(); + *caps = Some(new_caps.iter().cloned().collect()); + } + // if let Response::Capabilities(caps) = resp { // debug!("capabilities: {:?}", caps); // } @@ -159,24 +177,12 @@ where } /// Check if this client has a particular capability - pub async fn has_capability(&self, cap: impl AsRef) -> Result { + pub async fn has_capability(&mut self, cap: impl AsRef) -> Result { let cap = cap.as_ref().to_owned(); debug!("checking for the capability: {:?}", cap); + let cap = parse_capability(cap)?; - let cap_bytes = cap.as_bytes(); - debug!("cap_bytes {:?}", cap_bytes); - // let (_, cap) = match crate::oldparser::rfc3501::capability(cap_bytes) { - // Ok(v) => v, - // Err(err) => { - // error!("ERROR PARSING {:?} {} {:?}", cap, err, err); - // use std::error::Error; - // let bt = err.backtrace().unwrap(); - // error!("{}", bt); - // std::process::exit(1); - // } - // }; - let cap = Capability::from(Capability::Atom(cap)); - + self.capabilities(false).await?; let caps = &*self.caps.read(); // TODO: refresh caps @@ -190,7 +196,7 @@ where pub struct GreetingWaiter(GreetingState); impl Future for GreetingWaiter { - type Output = (); + type Output = Response; fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { let (state, waker) = &mut *self.0.write(); debug!("g {:?}", state); @@ -198,32 +204,43 @@ impl Future for GreetingWaiter { *waker = Some(cx.waker().clone()); } - match state { - true => Poll::Ready(()), - false => Poll::Pending, + match state.take() { + Some(v) => Poll::Ready(v), + None => Poll::Pending, } } } -pub struct ExecWaiter<'a, C>(&'a Client, usize); +pub struct ExecWaiter<'a, C>(&'a Client, usize, bool); impl<'a, C> Future for ExecWaiter<'a, C> { - type Output = (); - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - let mut handlers = self.0.results.write(); - let state = handlers.get_mut(&self.1); - - // TODO: handle the None case here - debug!("f[{}] {:?}", self.1, state); - let (result, waker) = state.unwrap(); - - match result { - Some(_) => Poll::Ready(()), - None => { - *waker = Some(cx.waker().clone()); - Poll::Pending + type Output = (Response, Vec); + fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { + // add the waker + let mut results = self.0.results.write(); + if !self.2 { + if let Some((_, _, _, waker_ref)) = + results.iter_mut().find(|(id, _, _, _)| *id == self.1) + { + let waker = cx.waker().clone(); + *waker_ref = Some(waker); + self.2 = true; } } + + // if this struct exists then there's definitely at least one entry + let (id, last_response, _, _) = &results[0]; + if *id != self.1 || last_response.is_none() { + return Poll::Pending; + } + + let (_, last_response, intermediate_responses, _) = results.pop_front().unwrap(); + mem::drop(results); + + Poll::Ready(( + last_response.expect("already checked"), + intermediate_responses, + )) } } @@ -257,26 +274,17 @@ where } debug!("got a new line {:?}", next_line); - let resp = Response::Capabilities(vec![]); - // let (_, resp) = match crate::oldparser::parse_response(next_line.as_bytes()) { - // Ok(v) => v, - // Err(err) => { - // debug!("shiet: {:?}", err); - // continue; - // } - // }; + let resp = parse_response(next_line)?; if let Some(greeting) = greeting.take() { let (greeting, waker) = &mut *greeting.write(); debug!("received greeting!"); - *greeting = true; + *greeting = Some(resp.clone()); if let Some(waker) = waker.take() { waker.wake(); } } - let resp = Response::from(resp); - debug!("resp: {:?}", resp); match &resp { // capabilities list Response::Capabilities(new_caps) @@ -302,8 +310,8 @@ where if tag.starts_with(TAG_PREFIX) { let id = tag.trim_start_matches(TAG_PREFIX).parse::()?; let mut results = results.write(); - if let Some((c, waker)) = results.get_mut(&id) { - *c = Some(resp); + if let Some((_, opt, _, waker)) = results.iter_mut().next() { + *opt = Some(resp); if let Some(waker) = waker.take() { waker.wake(); } @@ -311,7 +319,7 @@ where } } - _ => todo!("unhandled response: {:?}", resp), + _ => {} } // debug!("parsed as: {:?}", resp); diff --git a/imap/src/client/mod.rs b/imap/src/client/mod.rs index 23b63f6..409a056 100644 --- a/imap/src/client/mod.rs +++ b/imap/src/client/mod.rs @@ -43,6 +43,9 @@ use tokio_rustls::{ client::TlsStream, rustls::ClientConfig as RustlsConfig, webpki::DNSNameRef, TlsConnector, }; +use crate::command::Command; +use crate::response::Response; + pub use self::inner::Client; /// Struct used to start building the config for a client. @@ -118,12 +121,20 @@ impl ClientUnauthenticated { } } - pub async fn capabilities(&mut self) -> Result<()> { + /// TODO: Exposing low-level execute , shoudl remove later + pub async fn execute(&mut self, cmd: Command) -> Result<(Response, Vec)> { match self { - ClientUnauthenticated::Encrypted(e) => e.inner.capabilities().await?, - ClientUnauthenticated::Unencrypted(e) => e.inner.capabilities().await?, + ClientUnauthenticated::Encrypted(e) => e.inner.execute(cmd).await, + ClientUnauthenticated::Unencrypted(e) => e.inner.execute(cmd).await, + } + } + + /// Checks if the server that the client is talking to has support for the given capability. + pub async fn has_capability(&mut self, cap: impl AsRef) -> Result { + match self { + ClientUnauthenticated::Encrypted(e) => e.inner.has_capability(cap).await, + ClientUnauthenticated::Unencrypted(e) => e.inner.has_capability(cap).await, } - Ok(()) } } diff --git a/imap/src/command/mod.rs b/imap/src/command/mod.rs index 5bbcb54..b922226 100644 --- a/imap/src/command/mod.rs +++ b/imap/src/command/mod.rs @@ -5,6 +5,7 @@ use std::fmt; pub enum Command { Capability, Starttls, + Login { username: String, password: String }, } impl fmt::Display for Command { @@ -12,6 +13,7 @@ impl fmt::Display for Command { match self { Command::Capability => write!(f, "CAPABILITY"), Command::Starttls => write!(f, "STARTTLS"), + Command::Login { username, password } => write!(f, "LOGIN {} {}", username, password), } } } diff --git a/imap/src/parser/mod.rs b/imap/src/parser/mod.rs index 59a13c8..3483e9b 100644 --- a/imap/src/parser/mod.rs +++ b/imap/src/parser/mod.rs @@ -16,22 +16,7 @@ struct Rfc3501; pub fn parse_capability(s: impl AsRef) -> Result> { let mut pairs = Rfc3501::parse(Rule::capability, s.as_ref())?; let pair = pairs.next().unwrap(); - let cap = match pair.as_rule() { - Rule::capability => { - let mut pairs = pair.into_inner(); - let pair = pairs.next().unwrap(); - match pair.as_rule() { - Rule::auth_type => Capability::Auth(pair.as_str().to_uppercase().to_owned()), - Rule::atom => match pair.as_str() { - "IMAP4rev1" => Capability::Imap4rev1, - s => Capability::Atom(s.to_uppercase().to_owned()), - }, - _ => unreachable!("{:?}", pair), - } - } - _ => unreachable!("{:?}", pair), - }; - Ok(cap) + Ok(build_capability(pair)) } pub fn parse_response(s: impl AsRef) -> Result> { @@ -82,6 +67,7 @@ fn build_response(pair: Pair) -> Response { } } Rule::mailbox_data => Response::MailboxData(build_mailbox_data(pair)), + Rule::capability_data => Response::Capabilities(build_capabilities(pair)), _ => unreachable!("{:#?}", pair), } } @@ -100,7 +86,6 @@ fn build_resp_cond_state(pair: Pair) -> (Status, Option, Opt let mut code = None; let mut information = None; - println!("pairs: {:#?}", pairs); let pair = pairs.next().unwrap(); let pairs = pair.into_inner(); for pair in pairs { @@ -119,9 +104,13 @@ fn build_resp_code(pair: Pair) -> Option { unreachable!("{:#?}", pair); } + // panic!("pair: {:#?}", pair); + debug!("pair: {:#?}", pair); + let mut pairs = pair.into_inner(); let pair = pairs.next()?; Some(match pair.as_rule() { + Rule::capability_data => ResponseCode::Capabilities(build_capabilities(pair)), Rule::resp_text_code_readwrite => ResponseCode::ReadWrite, Rule::resp_text_code_uidvalidity => ResponseCode::UidValidity(build_number(pair)), Rule::resp_text_code_unseen => ResponseCode::Unseen(build_number(pair)), @@ -129,6 +118,31 @@ fn build_resp_code(pair: Pair) -> Option { }) } +fn build_capability(pair: Pair) -> Capability { + if !matches!(pair.as_rule(), Rule::capability) { + unreachable!("{:#?}", pair); + } + + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + match pair.as_rule() { + Rule::auth_type => Capability::Auth(pair.as_str().to_uppercase().to_owned()), + Rule::atom => match pair.as_str() { + "IMAP4rev1" => Capability::Imap4rev1, + s => Capability::Atom(s.to_uppercase().to_owned()), + }, + _ => unreachable!("{:?}", pair), + } +} + +fn build_capabilities(pair: Pair) -> Vec { + if !matches!(pair.as_rule(), Rule::capability_data) { + unreachable!("{:#?}", pair); + } + + pair.into_inner().map(build_capability).collect() +} + fn build_status(pair: Pair) -> Status { match pair.as_rule() { Rule::resp_status => match pair.as_str().to_uppercase().as_str() { @@ -202,7 +216,7 @@ mod tests { use pest::Parser; #[test] -#[rustfmt::skip] + #[rustfmt::skip] fn test_capability() { assert_eq!(parse_capability("IMAP4rev1"), Ok(Capability::Imap4rev1)); assert_eq!(parse_capability("LOGINDISABLED"), Ok(Capability::Atom("LOGINDISABLED".to_owned()))); @@ -214,7 +228,7 @@ mod tests { } #[test] -#[rustfmt::skip] + #[rustfmt::skip] fn test_nil() { assert!(Rfc3501::parse(Rule::nil, "NIL").is_ok()); assert!(Rfc3501::parse(Rule::nil, "anything else").is_err()); @@ -292,5 +306,13 @@ mod tests { 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" 3028 92))"#, + // "\r\n", + // )), + // Ok(Response::Fetch(12, vec![])) + // ); } } diff --git a/imap/src/parser/rfc3501.pest b/imap/src/parser/rfc3501.pest index d72904f..04b9095 100644 --- a/imap/src/parser/rfc3501.pest +++ b/imap/src/parser/rfc3501.pest @@ -34,7 +34,7 @@ body_type_mpart = { body{1,} ~ sp ~ media_subtype ~ (sp ~ body_ext_mpart)? } body_type_msg = { media_message ~ sp ~ body_fields ~ sp ~ envelope ~ sp ~ body ~ sp ~ body_fld_lines } body_type_text = { media_text ~ sp ~ body_fields ~ sp ~ body_fld_lines } capability = ${ ^"AUTH=" ~ auth_type | atom } -capability_data = { ^"CAPABILITY" ~ (sp ~ capability)* ~ sp ~ "IMAP4rev1" ~ (sp ~ capability)* } +capability_data = { ^"CAPABILITY" ~ (sp ~ ("IMAP4rev1" ~ capability))* ~ sp ~ "IMAP4rev1" ~ (sp ~ capability)* } char8 = @{ '\x01'..'\xff' } continue_req = { "+" ~ sp ~ (resp_text | base64) ~ crlf } date_day_fixed = { (sp ~ digit) | digit{2} } diff --git a/src/mail/mod.rs b/src/mail/mod.rs index b589f72..77d45fd 100644 --- a/src/mail/mod.rs +++ b/src/mail/mod.rs @@ -9,7 +9,7 @@ use panorama_imap::{ use tokio::{sync::mpsc::UnboundedReceiver, task::JoinHandle}; use tokio_stream::wrappers::WatchStream; -use crate::config::{Config, ConfigWatcher, MailAccountConfig, TlsMethod}; +use crate::config::{Config, ConfigWatcher, ImapAuth, MailAccountConfig, TlsMethod}; /// Command sent to the mail thread by something else (i.e. UI) pub enum MailCommand { @@ -89,7 +89,13 @@ async fn imap_main(acct: MailAccountConfig) -> Result<()> { debug!("preparing to auth"); // check if the authentication method is supported - unauth.capabilities().await?; + let authed = match acct.imap.auth { + ImapAuth::Plain { username, password } => { + let ok = unauth.has_capability("AUTH=PLAIN").await?; + let res = unauth.execute(ImapCommand::Login { username, password }).await?; + debug!("res: {:?}", res); + } + }; // debug!("sending CAPABILITY"); // let result = unauth.capabilities().await?;