From 9d31a13882cdd31a7a68914b29b96f9e2768477b Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Sun, 8 Aug 2021 23:30:08 -0500 Subject: [PATCH] switch &[u8] parsers to using Bytes wrapper --- Cargo.lock | 88 +++++-- imap/Cargo.toml | 4 + imap/bin/greenmail_test.rs | 14 +- imap/src/auth.rs | 59 ----- imap/src/{client.old.rs => client/client.rs} | 154 ++++++------- imap/src/client/inner.rs | 115 +++++++++- imap/src/client/macros.rs | 17 ++ imap/src/client/mod.rs | 22 +- imap/src/client/response_stream.rs | 48 ++++ imap/src/codec.rs | 36 +-- imap/src/lib.rs | 4 +- imap/src/proto/bytes.rs | 230 +++++++++++++++++++ imap/src/proto/command.rs | 58 ++++- imap/src/proto/macros.rs | 2 +- imap/src/proto/mod.rs | 6 +- imap/src/proto/parsers.rs | 96 +++++++- imap/src/proto/response.rs | 150 ++++++++---- imap/src/proto/rfc2234.rs | 8 +- imap/src/proto/rfc3501.rs | 161 ++++++++----- 19 files changed, 913 insertions(+), 359 deletions(-) delete mode 100644 imap/src/auth.rs rename imap/src/{client.old.rs => client/client.rs} (65%) create mode 100644 imap/src/client/macros.rs create mode 100644 imap/src/client/response_stream.rs create mode 100644 imap/src/proto/bytes.rs diff --git a/Cargo.lock b/Cargo.lock index 01e03a6..535e7fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,6 +79,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + [[package]] name = "darling" version = "0.12.4" @@ -277,9 +290,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.51" +version = "0.3.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +checksum = "ce791b7ca6638aae45be056e068fc756d871eb3b3b10b8efa62d1c9cec616752" dependencies = [ "wasm-bindgen", ] @@ -377,6 +390,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -401,6 +433,7 @@ dependencies = [ "async-trait", "bitflags", "bytes", + "chrono", "derive_builder", "futures", "log", @@ -486,9 +519,9 @@ checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" [[package]] name = "redox_syscall" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] @@ -554,9 +587,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" +checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" [[package]] name = "smallvec" @@ -599,6 +632,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + [[package]] name = "tokio" version = "1.9.0" @@ -674,10 +718,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" [[package]] -name = "wasm-bindgen" -version = "0.2.74" +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -685,9 +735,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.74" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f" dependencies = [ "bumpalo", "lazy_static", @@ -700,9 +750,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.74" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -710,9 +760,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.74" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f" dependencies = [ "proc-macro2", "quote", @@ -723,15 +773,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.74" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" +checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2" [[package]] name = "web-sys" -version = "0.3.51" +version = "0.3.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +checksum = "01c70a82d842c9979078c772d4a1344685045f1a5628f677c2b2eab4dd7d2696" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/imap/Cargo.toml b/imap/Cargo.toml index 2369b1f..be5abc7 100644 --- a/imap/Cargo.toml +++ b/imap/Cargo.toml @@ -20,11 +20,15 @@ path = "bin/greenmail_test.rs" default = ["rfc2177-idle"] rfc2177-idle = [] +[profile.release] +lto = true + [dependencies] anyhow = "1.0.42" async-trait = "0.1.51" bitflags = "1.2.1" bytes = "1.0.1" +chrono = "0.4.19" derive_builder = "0.10.2" futures = "0.3.16" log = "0.4.14" diff --git a/imap/bin/greenmail_test.rs b/imap/bin/greenmail_test.rs index 0a7bbed..bbfffd1 100644 --- a/imap/bin/greenmail_test.rs +++ b/imap/bin/greenmail_test.rs @@ -1,14 +1,14 @@ use anyhow::Result; -use panorama_imap::client::ConfigBuilder; +// use panorama_imap::client::ConfigBuilder; #[tokio::main] async fn main() -> Result<()> { - let _client = ConfigBuilder::default() - .hostname(String::from("localhost")) - .port(3993) - .tls(true) - .connect() - .await?; + // let _client = ConfigBuilder::default() + // .hostname(String::from("localhost")) + // .port(3993) + // .tls(true) + // .connect() + // .await?; Ok(()) } diff --git a/imap/src/auth.rs b/imap/src/auth.rs deleted file mode 100644 index 4647b42..0000000 --- a/imap/src/auth.rs +++ /dev/null @@ -1,59 +0,0 @@ -use anyhow::Result; - -use crate::command::Command; -use crate::response::{Response, ResponseDone, Status}; - -use super::{ClientAuthenticated, ClientUnauthenticated}; - -#[async_trait] -pub trait Auth { - /// Performs authentication, consuming the client - // TODO: return the unauthed client if failed? - async fn perform_auth(self, client: ClientUnauthenticated) -> Result; - - /// Converts the wrappers around the client once the authentication has happened. Should only - /// be called by the `perform_auth` function. - fn convert_client(client: ClientUnauthenticated) -> ClientAuthenticated { - match client { - ClientUnauthenticated::Encrypted(e) => ClientAuthenticated::Encrypted(e), - ClientUnauthenticated::Unencrypted(e) => ClientAuthenticated::Unencrypted(e), - } - } -} - -pub struct Plain { - pub username: String, - pub password: String, -} - -#[async_trait] -impl Auth for Plain { - async fn perform_auth(self, mut client: ClientUnauthenticated) -> Result { - let command = Command::Login { - username: self.username, - password: self.password, - }; - - let result = client.execute(command).await?; - let done = result.done().await?; - - assert!(done.is_some()); - let done = done.unwrap(); - - if done.status != Status::Ok { - bail!("unable to login: {:?}", done); - } - - // if !matches!( - // result, - // Response::Done(ResponseDone { - // status: Status::Ok, - // .. - // }) - // ) { - // bail!("unable to login: {:?}", result); - // } - - Ok(::convert_client(client)) - } -} diff --git a/imap/src/client.old.rs b/imap/src/client/client.rs similarity index 65% rename from imap/src/client.old.rs rename to imap/src/client/client.rs index 9cbc9a9..9881408 100644 --- a/imap/src/client.old.rs +++ b/imap/src/client/client.rs @@ -1,5 +1,6 @@ -use std::borrow::Cow; +use std::pin::Pin; use std::sync::Arc; +use std::task::{Context, Poll}; use anyhow::Result; use futures::{ @@ -11,39 +12,45 @@ use tokio_rustls::{ client::TlsStream, rustls::ClientConfig as RustlsConfig, webpki::DNSNameRef, TlsConnector, }; -pub use super::inner::{Client, ResponseStream}; +use crate::proto::{ + command::{ + Command, CommandFetch, CommandList, CommandSearch, CommandSelect, FetchItems, + SearchCriteria, + }, + response::{ + Condition, Flag, Mailbox, MailboxData, MailboxList, MessageAttribute, Response, + ResponseCode, Status, + }, +}; -/// Struct used to start building the config for a client. -/// -/// Call [`.build`][1] to _build_ the config, then run [`.open`][2] to actually start opening -/// the connection to the server. -/// -/// [1]: self::ClientConfigBuilder::build -/// [2]: self::ClientConfig::open -pub type ClientBuilder = ClientConfigBuilder; +use super::inner::Inner; +use super::response_stream::ResponseStream; /// An IMAP client that hasn't been connected yet. #[derive(Builder, Clone, Debug)] -pub struct ClientConfig { +#[builder(build_fn(private))] +pub struct Config { /// The hostname of the IMAP server. If using TLS, must be an address - hostname: String, + pub hostname: String, /// The port of the IMAP server. - port: u16, + pub port: u16, /// Whether or not the client is using an encrypted stream. /// /// To upgrade the connection later, use the upgrade method. - tls: bool, + pub tls: bool, } -impl ClientConfig { +impl ConfigBuilder { pub async fn open(self) -> Result { - let hostname = self.hostname.as_ref(); - let port = self.port; + let config = self.build()?; + + let hostname = config.hostname.as_ref(); + let port = config.port; let conn = TcpStream::connect((hostname, port)).await?; - if self.tls { + if config.tls { let mut tls_config = RustlsConfig::new(); tls_config .root_store @@ -52,11 +59,11 @@ impl ClientConfig { let dnsname = DNSNameRef::try_from_ascii_str(hostname).unwrap(); let conn = tls_config.connect(dnsname, conn).await?; - let mut inner = Client::new(conn, self); + let mut inner = Inner::new(conn, config).await?; inner.wait_for_greeting().await?; return Ok(ClientUnauthenticated::Encrypted(inner)); } else { - let mut inner = Client::new(conn, self); + let mut inner = Inner::new(conn, config).await?; inner.wait_for_greeting().await?; return Ok(ClientUnauthenticated::Unencrypted(inner)); } @@ -64,8 +71,8 @@ impl ClientConfig { } pub enum ClientUnauthenticated { - Encrypted(Client>), - Unencrypted(Client), + Encrypted(Inner>), + Unencrypted(Inner), } impl ClientUnauthenticated { @@ -79,36 +86,18 @@ impl ClientUnauthenticated { } } - /// Exposing low-level execute - async fn execute(&mut self, cmd: Command) -> Result { - match self { - ClientUnauthenticated::Encrypted(e) => e.execute(cmd).await, - ClientUnauthenticated::Unencrypted(e) => e.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.has_capability(cap).await, - ClientUnauthenticated::Unencrypted(e) => e.has_capability(cap).await, - } - } + client_expose!(async execute<'a>(cmd: Command<'a>) -> Result); + client_expose!(async has_capability(cap: impl AsRef) -> Result); } pub enum ClientAuthenticated { - Encrypted(Client>), - Unencrypted(Client), + Encrypted(Inner>), + Unencrypted(Inner), } impl ClientAuthenticated { - /// Exposing low-level execute - async fn execute(&mut self, cmd: Command) -> Result { - match self { - ClientAuthenticated::Encrypted(e) => e.execute(cmd).await, - ClientAuthenticated::Unencrypted(e) => e.execute(cmd).await, - } - } + client_expose!(async execute<'a>(cmd: Command<'a>) -> Result); + client_expose!(async has_capability(cap: impl AsRef) -> Result); fn sender(&self) -> mpsc::UnboundedSender { match self { @@ -117,28 +106,20 @@ impl ClientAuthenticated { } } - /// 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 { - ClientAuthenticated::Encrypted(e) => e.has_capability(cap).await, - ClientAuthenticated::Unencrypted(e) => e.has_capability(cap).await, - } - } - /// Runs the LIST command - pub async fn list(&mut self) -> Result> { - let cmd = Command::List { - reference: "".to_owned(), - mailbox: "*".to_owned(), - }; + pub async fn list(&mut self) -> Result> { + let cmd = Command::List(CommandList { + reference: "", + mailbox: "*", + }); let res = self.execute(cmd).await?; let (_, data) = res.wait().await?; let mut folders = Vec::new(); for resp in data { - if let Response::MailboxData(MailboxData::List { name, .. }) = resp { - folders.push(name.to_owned()); + if let Response::MailboxData(MailboxData::List(MailboxList { mailbox, .. })) = resp { + folders.push(mailbox); } } @@ -147,9 +128,9 @@ impl ClientAuthenticated { /// Runs the SELECT command pub async fn select(&mut self, mailbox: impl AsRef) -> Result { - let cmd = Command::Select { - mailbox: mailbox.as_ref().to_owned(), - }; + let cmd = Command::Select(CommandSelect { + mailbox: mailbox.as_ref(), + }); let stream = self.execute(cmd).await?; let (_, data) = stream.wait().await?; @@ -160,11 +141,14 @@ impl ClientAuthenticated { Response::MailboxData(MailboxData::Flags(flags)) => select.flags = flags, Response::MailboxData(MailboxData::Exists(exists)) => select.exists = Some(exists), Response::MailboxData(MailboxData::Recent(recent)) => select.recent = Some(recent), - Response::Data(ResponseData { - status: Status::Ok, - code: Some(code), - .. - }) => match code { + Response::Tagged( + _, + Condition { + status: Status::Ok, + code: Some(code), + .. + }, + ) => match code { ResponseCode::Unseen(value) => select.unseen = Some(value), ResponseCode::UidNext(value) => select.uid_next = Some(value), ResponseCode::UidValidity(value) => select.uid_validity = Some(value), @@ -179,9 +163,9 @@ impl ClientAuthenticated { /// Runs the SEARCH command pub async fn uid_search(&mut self) -> Result> { - let cmd = Command::UidSearch { + let cmd = Command::UidSearch(CommandSearch { criteria: SearchCriteria::All, - }; + }); let stream = self.execute(cmd).await?; let (_, data) = stream.wait().await?; for resp in data { @@ -197,12 +181,12 @@ impl ClientAuthenticated { &mut self, uids: &[u32], items: FetchItems, - ) -> Result)>> { - let cmd = Command::Fetch { - uids: uids.to_vec(), + ) -> Result)>> { + let cmd = Command::Fetch(CommandFetch { + ids: uids.to_vec(), items, - }; - debug!("fetch: {}", cmd); + }); + debug!("fetch: {:?}", cmd); let stream = self.execute(cmd).await?; // let (done, data) = stream.wait().await?; Ok(stream.filter_map(|resp| match resp { @@ -217,12 +201,12 @@ impl ClientAuthenticated { &mut self, uids: &[u32], items: FetchItems, - ) -> Result)>> { - let cmd = Command::UidFetch { - uids: uids.to_vec(), + ) -> Result)>> { + let cmd = Command::UidFetch(CommandFetch { + ids: uids.to_vec(), items, - }; - debug!("uid fetch: {}", cmd); + }); + debug!("uid fetch: {:?}", cmd); let stream = self.execute(cmd).await?; // let (done, data) = stream.wait().await?; Ok(stream.filter_map(|resp| match resp { @@ -244,8 +228,8 @@ impl ClientAuthenticated { } #[derive(Debug, Default)] -pub struct SelectResponse<'a> { - pub flags: Vec>, +pub struct SelectResponse { + pub flags: Vec, pub exists: Option, pub recent: Option, pub uid_next: Option, @@ -255,8 +239,8 @@ pub struct SelectResponse<'a> { /// A token that represents an idling connection. /// -/// Dropping this token indicates that the idling should be completed, and the DONE command will be -/// sent to the server as a result. +/// Dropping this token indicates that the idling should be completed, and the +/// DONE command will be sent to the server as a result. #[cfg(feature = "rfc2177-idle")] #[cfg_attr(docsrs, doc(cfg(feature = "rfc2177-idle")))] pub struct IdleToken { diff --git a/imap/src/client/inner.rs b/imap/src/client/inner.rs index 5e6e363..c34f6b1 100644 --- a/imap/src/client/inner.rs +++ b/imap/src/client/inner.rs @@ -1,63 +1,127 @@ +use std::sync::atomic::AtomicU32; + use anyhow::Result; -use futures::{future::FutureExt, stream::StreamExt}; +use futures::{ + future::{FutureExt, TryFutureExt}, + stream::StreamExt, +}; use tokio::{ - io::{split, AsyncRead, AsyncWrite, ReadHalf, WriteHalf}, - sync::oneshot, + io::{split, AsyncRead, AsyncWrite, AsyncWriteExt, ReadHalf, WriteHalf}, + sync::{mpsc, oneshot}, task::JoinHandle, }; use tokio_rustls::client::TlsStream; use tokio_util::codec::FramedRead; use crate::codec::ImapCodec; -use crate::proto::command::Command; +use crate::proto::{ + bytes::Bytes, command::Command, response::Response, rfc3501::capability as parse_capability, +}; +use super::client::Config; +use super::response_stream::ResponseStream; use super::upgrade::upgrade; type ExitSender = oneshot::Sender<()>; type ExitListener = oneshot::Receiver<()>; +type GreetingSender = oneshot::Sender<()>; +type GreetingWaiter = oneshot::Receiver<()>; pub struct Inner { + config: Config, + tag_number: AtomicU32, + read_exit: ExitSender, read_handle: JoinHandle>, - write_half: WriteHalf, + write_exit: ExitSender, + pub(crate) write_tx: mpsc::UnboundedSender, + write_handle: JoinHandle>, + + greeting_rx: Option, } impl Inner where C: AsyncRead + AsyncWrite + Unpin + Send + 'static, { - pub async fn open(c: C) -> Result { + pub async fn new(c: C, config: Config) -> Result { // break the stream of bytes into a reader and a writer // the read_half represents the server->client connection // the write_half represents the client->server connection let (read_half, write_half) = split(c); + // this channel is used to inform clients when we receive the + // initial greeting from the server + let (greeting_tx, greeting_rx) = oneshot::channel(); + // spawn the server->client loop let (read_exit, exit_rx) = oneshot::channel(); let read_handle = tokio::spawn(read_loop(read_half, exit_rx)); + // spawn the client->server loop + let (write_exit, exit_rx) = oneshot::channel(); + // TODO: maybe an arbitrary/configurable limit here would be better? + let (write_tx, write_rx) = mpsc::unbounded_channel(); + let write_handle = tokio::spawn(write_loop(write_half, exit_rx, write_rx)); + + let tag_number = AtomicU32::new(0); Ok(Inner { + config, + tag_number, read_exit, read_handle, - write_half, + write_exit, + write_tx, + write_handle, + greeting_rx: Some(greeting_rx), }) } - pub async fn execute<'a>(&mut self, _command: Command<'a>) {} + pub async fn execute<'a>(&mut self, _command: Command<'a>) -> Result { todo!() } + + pub async fn has_capability(&mut self, cap: impl AsRef) -> Result { + // TODO: cache capabilities if needed? + let cap = cap.as_ref().to_owned(); + let (_, cap) = parse_capability(Bytes::from(cap))?; + + let resp = self.execute(Command::Capability).await?; + let (_, data) = resp.wait().await?; + + for resp in data { + if let Response::Capabilities(caps) = resp { + return Ok(caps.contains(&cap)); + } + // debug!("cap: {:?}", resp); + } + + Ok(false) + } pub async fn upgrade(self) -> Result>> { // TODO: check that this capability exists?? + // TODO: issue the STARTTLS command to the server + // issue exit to the read loop and retrieve the read half let _ = self.read_exit.send(()); let read_half = self.read_handle.await?; - let write_half = self.write_half; + + // issue exit to the write loop and retrieve the write half + let _ = self.write_exit.send(()); + let write_half = self.write_handle.await?; // put the read half and write half back together let stream = read_half.unsplit(write_half); - let tls_stream = upgrade(stream, "hellosu").await?; + let tls_stream = upgrade(stream, &self.config.hostname).await?; - Inner::open(tls_stream).await + Inner::new(tls_stream, self.config).await + } + + pub async fn wait_for_greeting(&mut self) -> Result<()> { + if let Some(greeting_rx) = self.greeting_rx.take() { + greeting_rx.await?; + } + Ok(()) } } @@ -89,3 +153,32 @@ where framed.into_inner() } + +async fn write_loop( + mut stream: WriteHalf, + exit_rx: ExitListener, + mut write_rx: mpsc::UnboundedReceiver, +) -> WriteHalf +where + C: AsyncWrite, +{ + let mut exit_rx = exit_rx.map_err(|_| ()).shared(); + loop { + let write_fut = write_rx.recv().fuse(); + pin_mut!(write_fut); + + select! { + line = write_fut => { + if let Some(line) = line { + // TODO: handle errors here + stream.write_all(line.as_bytes()).await; + stream.flush().await; + trace!("C>>>S: {:?}", line); + } + } + _ = exit_rx => break, + } + } + + stream +} diff --git a/imap/src/client/macros.rs b/imap/src/client/macros.rs new file mode 100644 index 0000000..95e40d7 --- /dev/null +++ b/imap/src/client/macros.rs @@ -0,0 +1,17 @@ +macro_rules! client_expose { + ( + async + $fn_name:ident + $(< $($lifetime:lifetime)* >)? + ( $($arg_name:ident : $ty:ty),* $(,)? ) + $(-> $ret:ty)? + ) => { + #[allow(dead_code)] + async fn $fn_name $(< $($lifetime)* >)? (&mut self, $($arg_name : $ty,)*) $(-> $ret)? { + match self { + Self::Encrypted(inner) => inner.$fn_name($($arg_name,)*).await, + Self::Unencrypted(inner) => inner.$fn_name($($arg_name,)*).await, + } + } + }; +} diff --git a/imap/src/client/mod.rs b/imap/src/client/mod.rs index 4071b3e..baeb9fc 100644 --- a/imap/src/client/mod.rs +++ b/imap/src/client/mod.rs @@ -1,20 +1,8 @@ +#[macro_use] +mod macros; + pub mod auth; +pub mod client; pub mod inner; +pub mod response_stream; pub mod upgrade; - -use anyhow::Result; - -#[derive(Clone, Debug, Builder)] -#[builder(build_fn(skip))] -pub struct Config { - // (required for TLS) - hostname: String, - port: u16, - tls: bool, -} - -impl ConfigBuilder { - pub async fn connect(&self) -> Result { todo!() } -} - -pub struct Client; diff --git a/imap/src/client/response_stream.rs b/imap/src/client/response_stream.rs new file mode 100644 index 0000000..7288739 --- /dev/null +++ b/imap/src/client/response_stream.rs @@ -0,0 +1,48 @@ +use std::pin::Pin; +use std::task::{Context, Poll}; + +use anyhow::Result; +use futures::stream::Stream; +use tokio::sync::mpsc; + +use crate::proto::response::{Response, ResponseDone}; + +/// A series of responses that follow an +pub struct ResponseStream { + pub(crate) inner: mpsc::UnboundedReceiver, +} + +impl ResponseStream { + /// Retrieves just the DONE item in the stream, discarding the rest + pub async fn done(mut self) -> Result> { + while let Some(resp) = self.inner.recv().await { + if let Response::Done(done) = resp { + return Ok(Some(done)); + } + } + Ok(None) + } + + /// Waits for the entire stream to finish, returning the DONE status and the + /// stream + pub async fn wait(mut self) -> Result<(Option, Vec)> { + let mut done = None; + let mut vec = Vec::new(); + while let Some(resp) = self.inner.recv().await { + if let Response::Done(d) = resp { + done = Some(d); + break; + } else { + vec.push(resp); + } + } + Ok((done, vec)) + } +} + +impl Stream for ResponseStream { + type Item = Response; + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + self.inner.poll_recv(cx) + } +} diff --git a/imap/src/codec.rs b/imap/src/codec.rs index 2a15c6d..4d62f8a 100644 --- a/imap/src/codec.rs +++ b/imap/src/codec.rs @@ -1,13 +1,9 @@ use std::io; -use bytes::{Bytes, BytesMut}; - +use bytes::BytesMut; use tokio_util::codec::{Decoder, Encoder}; -use crate::proto::{ - command::Command, - response::{Response, ResponseDone, Tag}, -}; +use crate::proto::{command::Command, response::Response}; #[derive(Default)] pub struct ImapCodec { @@ -15,7 +11,7 @@ pub struct ImapCodec { } impl<'a> Decoder for ImapCodec { - type Item = OwnedResponse; + type Item = Response; type Error = io::Error; fn decode(&mut self, buf: &mut BytesMut) -> Result, io::Error> { if self.decode_need_message_bytes > buf.len() { @@ -64,29 +60,3 @@ impl<'a> Encoder<&'a Command<'a>> for ImapCodec { // Ok(()) } } - -#[derive(Debug)] -pub struct OwnedResponse { - raw: Bytes, - // This reference is really scoped to the lifetime of the `raw` - // member, but unfortunately Rust does not allow that yet. It - // is transmuted to `'static` by the `Decoder`, instead, and - // references returned to callers of `ResponseData` are limited - // to the lifetime of the `ResponseData` struct. - // - // `raw` is never mutated during the lifetime of `ResponseData`, - // and `Response` does not not implement any specific drop glue. - response: Response<'static>, -} - -impl OwnedResponse { - pub fn _request_id(&self) -> Option<&Tag> { - match self.response { - Response::Done(ResponseDone { ref tag, .. }) => Some(tag), - _ => None, - } - } - - #[allow(clippy::needless_lifetimes)] - pub fn _parsed<'a>(&'a self) -> &'a Response<'a> { &self.response } -} diff --git a/imap/src/lib.rs b/imap/src/lib.rs index 10ca898..4245a1c 100644 --- a/imap/src/lib.rs +++ b/imap/src/lib.rs @@ -1,8 +1,8 @@ -// #[macro_use] +#[macro_use] extern crate anyhow; #[macro_use] extern crate async_trait; -// #[macro_use] +#[macro_use] extern crate log; #[macro_use] extern crate futures; diff --git a/imap/src/proto/bytes.rs b/imap/src/proto/bytes.rs new file mode 100644 index 0000000..132f676 --- /dev/null +++ b/imap/src/proto/bytes.rs @@ -0,0 +1,230 @@ +use std::ops::{Deref, RangeBounds}; + +use nom::{ + error::{ErrorKind, ParseError}, + CompareResult, Err, IResult, InputLength, Needed, +}; + +/// Glue code between nom and Bytes so they work together +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Bytes(bytes::Bytes); + +impl Bytes { + pub fn len(&self) -> usize { self.0.len() } +} + +impl From for Bytes { + fn from(b: bytes::Bytes) -> Self { Bytes(b) } +} + +impl From<&'static [u8]> for Bytes { + fn from(slice: &'static [u8]) -> Self { Bytes(bytes::Bytes::from(slice)) } +} + +impl From for Bytes { + fn from(slice: String) -> Self { Bytes(bytes::Bytes::from(slice)) } +} + +pub trait ShitNeededForParsing: Sized { + type Item; + type Sliced; + fn slice>(&self, range: R) -> Self::Sliced; + + fn first(&self) -> Option; + fn slice_index(&self, count: usize) -> Result; + + // InputTake + fn take(&self, count: usize) -> Self; + fn take_split(&self, count: usize) -> (Self, Self); + + // InputTakeAtPosition + fn split_at_position>(&self, predicate: P) -> IResult + where + P: Fn(Self::Item) -> bool; + fn split_at_position1>( + &self, + predicate: P, + e: ErrorKind, + ) -> IResult + where + P: Fn(Self::Item) -> bool; + fn split_at_position_complete>( + &self, + predicate: P, + ) -> IResult + where + P: Fn(Self::Item) -> bool; + fn split_at_position1_complete>( + &self, + predicate: P, + e: ErrorKind, + ) -> IResult + where + P: Fn(Self::Item) -> bool; +} + +impl ShitNeededForParsing for Bytes { + type Item = u8; + + type Sliced = Bytes; + fn slice>(&self, range: R) -> Self::Sliced { Self(self.0.slice(range)) } + + fn first(&self) -> Option { self.0.first().copied() } + fn slice_index(&self, count: usize) -> Result { + if self.len() >= count { + Ok(count) + } else { + Err(Needed::new(count - self.len())) + } + } + + // InputTake + fn take(&self, count: usize) -> Self { self.slice(..count) } + fn take_split(&self, count: usize) -> (Self, Self) { + let mut prefix = self.clone(); + let suffix = Self(prefix.0.split_off(count)); + (suffix, prefix) + } + + // InputTakeAtPosition + fn split_at_position>(&self, predicate: P) -> IResult + where + P: Fn(Self::Item) -> bool, + { + match (0..self.len()).find(|b| predicate(self[*b])) { + Some(i) => Ok((self.slice(i..), self.slice(..i))), + None => Err(Err::Incomplete(Needed::new(1))), + } + } + + fn split_at_position1>( + &self, + predicate: P, + e: ErrorKind, + ) -> IResult + where + P: Fn(Self::Item) -> bool, + { + match (0..self.len()).find(|b| predicate(self[*b])) { + Some(0) => Err(Err::Error(E::from_error_kind(self.clone(), e))), + Some(i) => Ok((self.slice(i..), self.slice(..i))), + None => Err(Err::Incomplete(Needed::new(1))), + } + } + + fn split_at_position_complete>( + &self, + predicate: P, + ) -> IResult + where + P: Fn(Self::Item) -> bool, + { + match (0..self.len()).find(|b| predicate(self[*b])) { + Some(i) => Ok((self.slice(i..), self.slice(..i))), + None => Ok(self.take_split(self.input_len())), + } + } + + fn split_at_position1_complete>( + &self, + predicate: P, + e: ErrorKind, + ) -> IResult + where + P: Fn(Self::Item) -> bool, + { + match (0..self.len()).find(|b| predicate(self[*b])) { + Some(0) => Err(Err::Error(E::from_error_kind(self.clone(), e))), + Some(i) => Ok((self.slice(i..), self.slice(..i))), + None => { + if self.is_empty() { + Err(Err::Error(E::from_error_kind(self.clone(), e))) + } else { + Ok(self.take_split(self.input_len())) + } + } + } + } +} + +pub trait ShitCompare { + fn compare(&self, t: T) -> CompareResult; + fn compare_no_case(&self, t: T) -> CompareResult; +} + +impl ShitCompare<&[u8]> for Bytes { + fn compare(&self, other: &[u8]) -> CompareResult { + match self.iter().zip(other.iter()).any(|(a, b)| a != b) { + true => CompareResult::Error, + false if self.len() < other.len() => CompareResult::Incomplete, + false => CompareResult::Ok, + } + } + fn compare_no_case(&self, other: &[u8]) -> CompareResult { + match self + .iter() + .zip(other.iter()) + .any(|(a, b)| (a & 0x20) != (b & 0x20)) + { + true => CompareResult::Error, + false if self.len() < other.len() => CompareResult::Incomplete, + false => CompareResult::Ok, + } + } +} + +macro_rules! array_impls { + ($($N:expr)+) => { $( + impl ShitCompare<[u8; $N]> for Bytes { + #[inline(always)] + fn compare(&self, t: [u8; $N]) -> CompareResult { + self.compare(&t[..]) + } + + #[inline(always)] + fn compare_no_case(&self, t: [u8;$N]) -> CompareResult { + self.compare_no_case(&t[..]) + } + } + + impl ShitCompare<&[u8; $N]> for Bytes { + #[inline(always)] + fn compare(&self, t: &[u8; $N]) -> CompareResult { + self.compare(&t[..]) + } + + #[inline(always)] + fn compare_no_case(&self, t: &[u8;$N]) -> CompareResult { + self.compare_no_case(&t[..]) + } + } + + impl From<&'static [u8; $N]> for Bytes { + fn from(slice: &'static [u8; $N]) -> Self { Bytes(bytes::Bytes::from(&slice[..])) } + } + )* } +} + +array_impls! { + 0 1 2 3 4 5 6 7 8 9 + 10 11 12 13 14 15 16 17 18 19 + 20 21 22 23 24 25 26 27 28 29 + 30 31 32 +} + +impl Bytes { + pub fn inner(self) -> bytes::Bytes { self.0 } +} + +impl Deref for Bytes { + type Target = [u8]; + fn deref(&self) -> &Self::Target { &*self.0 } +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { &*self.0 } +} + +impl InputLength for Bytes { + fn input_len(&self) -> usize { self.0.len() } +} diff --git a/imap/src/proto/command.rs b/imap/src/proto/command.rs index 6d3c253..0718701 100644 --- a/imap/src/proto/command.rs +++ b/imap/src/proto/command.rs @@ -1,3 +1,4 @@ +#[derive(Debug)] pub enum Command<'a> { // Any state Capability, @@ -10,14 +11,14 @@ pub enum Command<'a> { Authenticate, // Authenticated - Select, + Select(CommandSelect<'a>), Examine, Create, Delete, Rename, Subscribe, Unsubscribe, - List, + List(CommandList<'a>), Lsub, Status, Append, @@ -27,17 +28,64 @@ pub enum Command<'a> { Close, Expunge, Search, - Fetch, - Store, Copy, - Uid, + Fetch(CommandFetch), + Store, + UidCopy, + UidFetch(CommandFetch), + UidStore, + UidSearch(CommandSearch), // Extensions #[cfg(feature = "rfc2177-idle")] Idle, } +#[derive(Debug)] +pub struct CommandFetch { + pub ids: Vec, + pub items: FetchItems, +} + +#[derive(Debug)] +pub struct CommandList<'a> { + pub reference: &'a str, + pub mailbox: &'a str, +} + +#[derive(Debug)] pub struct CommandLogin<'a> { pub username: &'a str, pub password: &'a str, } + +#[derive(Debug)] +pub struct CommandSearch { + pub criteria: SearchCriteria, +} + +#[derive(Debug)] +pub struct CommandSelect<'a> { + pub mailbox: &'a str, +} + +// + +#[derive(Clone, Debug)] +pub enum FetchItems { + All, + Fast, + Full, + BodyPeek, + Items(Vec), + /// item set that panorama uses, TODO: remove when FetchItems has a builder + PanoramaAll, +} + +#[derive(Clone, Debug)] +pub enum FetchAttr {} + +#[derive(Clone, Debug)] +pub enum SearchCriteria { + All, +} diff --git a/imap/src/proto/macros.rs b/imap/src/proto/macros.rs index ddfc4bc..9806e33 100644 --- a/imap/src/proto/macros.rs +++ b/imap/src/proto/macros.rs @@ -1,6 +1,6 @@ macro_rules! rule { ($vis:vis $name:ident : $ret:ty => $expr:expr) => { - $vis fn $name(i: &[u8]) -> nom::IResult<&[u8], $ret> { + $vis fn $name(i: crate::proto::bytes::Bytes) -> nom::IResult { $expr(i) } }; diff --git a/imap/src/proto/mod.rs b/imap/src/proto/mod.rs index 5f629da..d32f969 100644 --- a/imap/src/proto/mod.rs +++ b/imap/src/proto/mod.rs @@ -1,15 +1,17 @@ #![allow(non_snake_case, dead_code)] +// utils #[macro_use] mod macros; +pub mod bytes; +#[macro_use] +pub mod parsers; // data types pub mod command; pub mod response; // parsers -#[macro_use] -pub mod parsers; pub mod rfc2234; pub mod rfc3501; diff --git a/imap/src/proto/parsers.rs b/imap/src/proto/parsers.rs index c7bea8e..88743b2 100644 --- a/imap/src/proto/parsers.rs +++ b/imap/src/proto/parsers.rs @@ -1,13 +1,17 @@ -use std::ops::RangeFrom; - +use anyhow::Result; use nom::{ error::{Error, ErrorKind, ParseError}, - Err, IResult, InputIter, Needed, Parser, Slice, + CompareResult, Err, IResult, InputLength, Needed, Parser, ToUsize, }; -/// `sep_list!(t, d)` represents `t *(d t)` and automatically collapses it into `Vec`. +use super::bytes::{ShitCompare, ShitNeededForParsing}; +use super::rfc2234::is_digit; + +/// `sep_list!(t, d)` represents `t *(d t)` and automatically collapses it into +/// `Vec`. /// -/// Also `sep_list!(?t, d)` represents `[t *(d t)]` and will default to an empty vec. +/// Also `sep_list!(?t, d)` represents `[t *(d t)]` and will default to an empty +/// vec. /// /// If `d` is not passed then it defaults to `SP`. macro_rules! sep_list { @@ -49,6 +53,31 @@ macro_rules! sep_list { }; } +macro_rules! opt_nil { + ($t:expr) => { + alt((map($t, Some), map(crate::proto::rfc3501::nil, |_| None))) + }; +} + +macro_rules! paren { + ($t:expr) => { + delimited(byte(b'('), $t, byte(b')')) + }; +} + +pub fn parse_u32(s: impl AsRef<[u8]>) -> Result { + let mut total = 0u32; + let s = s.as_ref(); + for digit in s.iter().rev() { + total *= 10; + if !is_digit(*digit) { + bail!("invalid digit {}", digit) + } + total += (*digit - b'\x30') as u32; + } + Ok(total) +} + /// Always fails, used as a no-op. pub fn never(i: I) -> IResult { Err(Err::Error(Error::new(i, ErrorKind::Not))) } @@ -64,15 +93,64 @@ where } } -/// Same as nom satisfy, but operates on `&[u8]` instead of `&str`. +/// Same as nom's streaming take, but operates on Bytes +pub fn take(count: C) -> impl Fn(I) -> IResult +where + I: ShitNeededForParsing + InputLength, + E: ParseError, + C: ToUsize, +{ + let c = count.to_usize(); + move |i: I| match i.slice_index(c) { + Err(i) => Err(Err::Incomplete(i)), + Ok(index) => Ok(i.take_split(index)), + } +} + +/// Same as nom's streaming take_while1, but operates on Bytes +pub fn take_while1(cond: F) -> impl Fn(I) -> IResult +where + I: ShitNeededForParsing, + E: ParseError, + F: Fn(T) -> bool, +{ + move |i: I| { + let e: ErrorKind = ErrorKind::TakeWhile1; + i.split_at_position1(|c| !cond(c), e) + } +} + +/// Tag (case-)Insensitive: Same as nom's tag_no_case, but operates on Bytes +pub fn tagi(tag: T) -> impl Fn(I) -> IResult +where + I: ShitNeededForParsing + InputLength + ShitCompare, + T: InputLength + Clone, + E: ParseError, +{ + let tag_len = tag.input_len(); + move |i: I| match i.compare_no_case(tag.clone()) { + CompareResult::Ok => { + let fst = i.slice(0..tag_len); + let snd = i.slice(tag_len..); + Ok((snd, fst)) + } + CompareResult::Incomplete => Err(Err::Incomplete(Needed::new(tag_len - i.input_len()))), + CompareResult::Error => { + let e: ErrorKind = ErrorKind::Tag; + Err(Err::Error(E::from_error_kind(i, e))) + } + } +} + +/// Same as nom's satisfy, but operates on `Bytes` instead of `&str`. pub fn satisfy(f: F) -> impl Fn(I) -> IResult where - I: Slice> + InputIter, + I: ShitNeededForParsing, F: Fn(T) -> bool, E: ParseError, T: Copy, { - move |i: I| match i.iter_elements().next().map(|t| (f(t), t)) { + move |i: I| match i.first().map(|t| (f(t), t)) { Some((true, ft)) => Ok((i.slice(1..), ft)), Some((false, _)) => Err(Err::Error(E::from_error_kind(i, ErrorKind::Satisfy))), None => Err(Err::Incomplete(Needed::Unknown)), @@ -82,7 +160,7 @@ where /// Match a single byte exactly. pub fn byte>(b: u8) -> impl Fn(I) -> IResult where - I: Slice> + InputIter, + I: ShitNeededForParsing, { satisfy(move |c| c == b) } diff --git a/imap/src/proto/response.rs b/imap/src/proto/response.rs index 69130c7..c320950 100644 --- a/imap/src/proto/response.rs +++ b/imap/src/proto/response.rs @@ -1,53 +1,84 @@ -use std::borrow::Cow; +use std::ops::RangeInclusive; -pub type Atom<'a> = Cow<'a, [u8]>; -pub type CowU8<'a> = Cow<'a, [u8]>; +use chrono::{DateTime, FixedOffset}; + +use super::bytes::Bytes; + +pub type Atom = Bytes; #[derive(Clone, Debug)] -pub struct Tag<'a>(pub CowU8<'a>); +pub struct Tag(pub Bytes); #[derive(Debug)] #[non_exhaustive] -pub enum Response<'a> { - Capabilities(Vec>), - Continue(ResponseText<'a>), - Condition(Condition<'a>), - Done(ResponseDone<'a>), - MailboxData(MailboxData<'a>), - Fetch(u32, Vec>), +pub enum Response { + Capabilities(Vec), + Continue(ResponseText), + Condition(Condition), + Done(ResponseDone), + MailboxData(MailboxData), + Fetch(u32, Vec), Expunge(u32), - Fatal(Condition<'a>), - Tagged(Tag<'a>, Condition<'a>), + Fatal(Condition), + Tagged(Tag, Condition), } #[derive(Debug)] -pub struct ResponseText<'a> { - pub code: Option>, - pub info: CowU8<'a>, +pub struct ResponseText { + pub code: Option, + pub info: Bytes, } #[derive(Debug)] -pub enum MessageAttribute<'a> { - Flags(Vec>), +pub enum MessageAttribute { + BodySection, + BodyStructure, Envelope(Envelope), + Flags(Vec), + InternalDate(DateTime), + ModSeq(u64), // RFC 4551, section 3.3.2 + Rfc822(Option), + Rfc822Header(Option), + Rfc822Size(u32), + Rfc822Text(Option), + Uid(u32), } #[derive(Debug)] -pub struct Envelope {} - -#[derive(Debug)] -pub struct ResponseDone<'a> { - pub tag: Tag<'a>, - pub status: Status, - pub code: Option>, - pub info: Option>, +pub struct Envelope { + pub date: Option, + pub subject: Option, + pub from: Option>, + pub sender: Option>, + pub reply_to: Option>, + pub to: Option>, + pub cc: Option>, + pub bcc: Option>, + pub in_reply_to: Option, + pub message_id: Option, } #[derive(Debug)] -pub struct Condition<'a> { +pub struct Address { + pub name: Option, + pub adl: Option, + pub mailbox: Option, + pub host: Option, +} + +#[derive(Debug)] +pub struct ResponseDone { + pub tag: Tag, pub status: Status, - pub code: Option>, - pub info: CowU8<'a>, + pub code: Option, + pub info: Option, +} + +#[derive(Debug)] +pub struct Condition { + pub status: Status, + pub code: Option, + pub info: Bytes, } #[derive(Debug)] @@ -61,54 +92,79 @@ pub enum Status { #[derive(Debug)] #[non_exhaustive] -pub enum ResponseCode<'a> { +pub enum ResponseCode { Alert, - Capabilities(Vec>), + BadCharset(Option>), + Capabilities(Vec), + HighestModSeq(u64), // RFC 4551, section 3.1.1 + Parse, + PermanentFlags(Vec), + ReadOnly, + ReadWrite, + TryCreate, + UidNext(u32), + UidValidity(u32), + Unseen(u32), + AppendUid(u32, Vec), + CopyUid(u32, Vec, Vec), + UidNotSticky, + Other(Bytes, Option), } #[derive(Debug)] -pub enum Capability<'a> { +pub enum UidSetMember { + UidRange(RangeInclusive), + Uid(u32), +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Capability { Imap4rev1, - Auth(Atom<'a>), - Atom(Atom<'a>), + Auth(Atom), + Atom(Atom), } #[derive(Debug)] -pub enum MailboxData<'a> { - Flags(Vec>), - List(MailboxList<'a>), +pub enum MailboxData { + Flags(Vec), + List(MailboxList), + Lsub, + Search(Vec), + Status, + Exists(u32), + Recent(u32), } #[derive(Debug)] -pub enum Mailbox<'a> { +pub enum Mailbox { Inbox, - Name(CowU8<'a>), + Name(Bytes), } #[derive(Debug)] -pub enum Flag<'a> { +pub enum Flag { Answered, Flagged, Deleted, Seen, Draft, Recent, - Keyword(Atom<'a>), - Extension(Atom<'a>), + Keyword(Atom), + Extension(Atom), } #[derive(Debug)] -pub struct MailboxList<'a> { - pub flags: Vec>, +pub struct MailboxList { + pub flags: Vec, pub delimiter: Option, - pub mailbox: Mailbox<'a>, + pub mailbox: Mailbox, } #[derive(Debug)] -pub enum MailboxListFlag<'a> { +pub enum MailboxListFlag { NoInferiors, NoSelect, Marked, Unmarked, - Extension(Atom<'a>), + Extension(Atom), } diff --git a/imap/src/proto/rfc2234.rs b/imap/src/proto/rfc2234.rs index b6133b4..4cc8be8 100644 --- a/imap/src/proto/rfc2234.rs +++ b/imap/src/proto/rfc2234.rs @@ -1,6 +1,6 @@ //! Grammar from -use nom::{branch::alt, character::streaming::anychar, multi::many0, sequence::pair}; +use nom::{branch::alt, multi::many0, sequence::pair}; use super::parsers::{byte, satisfy, skip}; @@ -16,10 +16,10 @@ rule!(pub CR : u8 => satisfy(is_cr)); rule!(pub CRLF : (u8, u8) => pair(CR, LF)); -pub fn is_ctl(c: u8) -> bool { c <= b'\x1f' || c == b'\x7f' } +pub(crate) fn is_ctl(c: u8) -> bool { c <= b'\x1f' || c == b'\x7f' } rule!(pub CTL : u8 => satisfy(is_ctl)); -pub fn is_digit(c: u8) -> bool { c >= b'\x30' && c <= b'\x39' } +pub(crate) fn is_digit(c: u8) -> bool { c >= b'\x30' && c <= b'\x39' } rule!(pub DIGIT : u8 => satisfy(is_digit)); pub(crate) fn is_dquote(c: u8) -> bool { c == b'\x22' } @@ -34,7 +34,7 @@ rule!(pub LF : u8 => satisfy(is_lf)); rule!(pub LWSP : () => skip(many0(alt((skip(WSP), skip(pair(CRLF, WSP))))))); -rule!(pub OCTET : char => anychar); +// rule!(pub OCTET : char => anychar); pub(crate) fn is_sp(c: u8) -> bool { c == b'\x20' } rule!(pub SP : u8 => satisfy(is_sp)); diff --git a/imap/src/proto/rfc3501.rs b/imap/src/proto/rfc3501.rs index b6d4f76..8495fe0 100644 --- a/imap/src/proto/rfc3501.rs +++ b/imap/src/proto/rfc3501.rs @@ -1,30 +1,42 @@ //! Grammar from -use std::borrow::Cow; - use nom::{ branch::alt, - bytes::streaming::{tag_no_case, take, take_while1}, - character::streaming::char, combinator::{map, map_res, opt, verify}, - multi::many0, + multi::{many0, many1}, sequence::{delimited, pair, preceded, separated_pair, terminated, tuple}, IResult, }; -use super::parsers::{byte, never, satisfy}; +use super::bytes::Bytes; +use super::parsers::{byte, never, parse_u32, satisfy, tagi, take, take_while1}; use super::response::{ - Atom, Capability, Condition, CowU8, Envelope, Flag, Mailbox, MailboxData, MailboxList, + Address, Atom, Capability, Condition, Envelope, Flag, Mailbox, MailboxData, MailboxList, MailboxListFlag, MessageAttribute, Response, ResponseCode, ResponseText, Status, Tag, }; use super::rfc2234::{is_char, is_cr, is_ctl, is_digit, is_dquote, is_lf, is_sp, CRLF, DQUOTE, SP}; -rule!(pub astring : CowU8 => alt((map(take_while1(is_astring_char), Cow::from), string))); +rule!(pub address : Address => map(paren!(tuple(( + terminated(addr_name, SP), + terminated(addr_adl, SP), + terminated(addr_mailbox, SP), + addr_host +))), |(name, adl, mailbox, host)| Address { name, adl, mailbox, host })); + +rule!(pub addr_adl : Option => nstring); + +rule!(pub addr_host : Option => nstring); + +rule!(pub addr_mailbox : Option => nstring); + +rule!(pub addr_name : Option => nstring); + +rule!(pub astring : Bytes => alt((take_while1(is_astring_char), string))); pub(crate) fn is_astring_char(c: u8) -> bool { is_atom_char(c) || is_resp_specials(c) } rule!(pub ASTRING_CHAR : u8 => alt((ATOM_CHAR, resp_specials))); -rule!(pub atom : CowU8 => map(take_while1(is_atom_char), Cow::from)); +rule!(pub atom : Bytes => take_while1(is_atom_char)); pub(crate) fn is_atom_char(c: u8) -> bool { is_char(c) && !is_atom_specials(c) } rule!(pub ATOM_CHAR : u8 => satisfy(is_atom_char)); @@ -44,39 +56,73 @@ rule!(pub atom_specials : u8 => satisfy(is_atom_specials)); rule!(pub auth_type : Atom => atom); rule!(pub capability : Capability => alt(( - map(preceded(tag_no_case("AUTH="), auth_type), |s| Capability::Auth(Cow::from(s))), - map(atom, |s| Capability::Atom(Cow::from(s))), + map(preceded(tagi(b"AUTH="), auth_type), Capability::Auth), + map(atom, Capability::Atom), ))); -rule!(pub capability_data : Vec => preceded(tag_no_case("CAPABILITY"), { +rule!(pub capability_data : Vec => preceded(tagi(b"CAPABILITY"), { map(separated_pair( many0(preceded(SP, capability)), - pair(SP, tag_no_case("IMAP4rev1")), + pair(SP, tagi(b"IMAP4rev1")), many0(preceded(SP, capability)) ), |(mut a, b)| { a.extend(b); a }) })); +pub(crate) fn is_char8(c: u8) -> bool { c != b'\0' } + rule!(pub continue_req : Response => delimited(pair(byte(b'+'), SP), // TODO: handle base64 case? map(resp_text, Response::Continue), CRLF)); -rule!(pub envelope : Envelope => map(byte(b'('), |_| Envelope {})); +rule!(pub envelope : Envelope => map(paren!(tuple(( + terminated(env_date, SP), + terminated(env_subject, SP), + terminated(env_from, SP), + terminated(env_sender, SP), + terminated(env_reply_to, SP), + terminated(env_to, SP), + terminated(env_cc, SP), + terminated(env_bcc, SP), + terminated(env_in_reply_to, SP), + env_message_id, +))), |(date, subject, from, sender, reply_to, to, cc, bcc, in_reply_to, message_id)| + Envelope { date, subject, from, sender, reply_to, to, cc, bcc, in_reply_to, message_id })); + +rule!(pub env_bcc : Option> => opt_nil!(paren!(many1(address)))); + +rule!(pub env_cc : Option> => opt_nil!(paren!(many1(address)))); + +rule!(pub env_date : Option => nstring); + +rule!(pub env_from : Option> => opt_nil!(paren!(many1(address)))); + +rule!(pub env_in_reply_to : Option => nstring); + +rule!(pub env_message_id : Option => nstring); + +rule!(pub env_reply_to : Option> => opt_nil!(paren!(many1(address)))); + +rule!(pub env_sender : Option> => opt_nil!(paren!(many1(address)))); + +rule!(pub env_subject : Option => nstring); + +rule!(pub env_to : Option> => opt_nil!(paren!(many1(address)))); rule!(pub flag : Flag => alt(( - map(tag_no_case("\\Answered"), |_| Flag::Answered), - map(tag_no_case("\\Flagged"), |_| Flag::Flagged), - map(tag_no_case("\\Deleted"), |_| Flag::Deleted), - map(tag_no_case("\\Seen"), |_| Flag::Seen), - map(tag_no_case("\\Draft"), |_| Flag::Draft), + map(tagi(b"\\Answered"), |_| Flag::Answered), + map(tagi(b"\\Flagged"), |_| Flag::Flagged), + map(tagi(b"\\Deleted"), |_| Flag::Deleted), + map(tagi(b"\\Seen"), |_| Flag::Seen), + map(tagi(b"\\Draft"), |_| Flag::Draft), map(flag_extension, Flag::Extension), ))); rule!(pub flag_extension : Atom => preceded(byte(b'\\'), atom)); -rule!(pub flag_fetch : Flag => alt((flag, map(tag_no_case("\\Recent"), |_| Flag::Recent)))); +rule!(pub flag_fetch : Flag => alt((flag, map(tagi(b"\\Recent"), |_| Flag::Recent)))); -rule!(pub flag_list : Vec => delimited(byte(b'('), sep_list!(?flag), byte(b')'))); +rule!(pub flag_list : Vec => paren!(sep_list!(?flag))); pub(crate) fn is_list_wildcards(c: u8) -> bool { c == b'%' || c == b'*' } rule!(pub list_wildcards : u8 => satisfy(is_list_wildcards)); @@ -86,33 +132,36 @@ rule!(pub list_wildcards : u8 => satisfy(is_list_wildcards)); // TODO: Future work, could possibly initialize writing to file if the length is // determined to exceed a certain threshold so we don't have insane amounts of // data in memory -pub fn literal(i: &[u8]) -> IResult<&[u8], CowU8> { - let mut length_of = terminated(delimited(char('{'), number, char('}')), CRLF); +pub fn literal(i: Bytes) -> IResult { + let mut length_of = terminated(delimited(byte(b'{'), number, byte(b'}')), CRLF); let (i, length) = length_of(i)?; - println!("length is: {:?}", (i, length)); - map(take(length), Cow::from)(i) + println!("length is: {:?}", length); + map(take(length), Bytes::from)(i) } #[test] fn test_literal() { assert_eq!( - literal(b"{13}\r\nHello, world!").unwrap().1.as_ref(), + literal(Bytes::from(b"{13}\r\nHello, world!")) + .unwrap() + .1 + .as_ref(), b"Hello, world!" ); } rule!(pub mailbox : Mailbox => alt(( - map(tag_no_case("INBOX"), |_| Mailbox::Inbox), + map(tagi(b"INBOX"), |_| Mailbox::Inbox), map(astring, Mailbox::Name), ))); rule!(pub mailbox_data : MailboxData => alt(( - map(preceded(pair(tag_no_case("FLAGS"), SP), flag_list), MailboxData::Flags), - map(preceded(pair(tag_no_case("LIST"), SP), mailbox_list), MailboxData::List), + map(preceded(pair(tagi(b"FLAGS"), SP), flag_list), MailboxData::Flags), + map(preceded(pair(tagi(b"LIST"), SP), mailbox_list), MailboxData::List), ))); rule!(pub mailbox_list : MailboxList => map(separated_pair( - delimited(byte(b'('), map(opt(mbx_list_flags), |opt| opt.unwrap_or_else(Vec::new)), byte(b')')), + paren!(map(opt(mbx_list_flags), |opt| opt.unwrap_or_else(Vec::new))), SP, separated_pair( alt(( map(delimited(DQUOTE, QUOTED_CHAR, DQUOTE), Some), @@ -132,50 +181,46 @@ rule!(pub mbx_list_flags : Vec => alt(( ))); rule!(pub mbx_list_oflag : MailboxListFlag => alt(( - map(tag_no_case("\\Noinferiors"), |_| MailboxListFlag::NoInferiors), + map(tagi(b"\\Noinferiors"), |_| MailboxListFlag::NoInferiors), map(flag_extension, MailboxListFlag::Extension), ))); rule!(pub mbx_list_sflag : MailboxListFlag => alt(( - map(tag_no_case("\\NoSelect"), |_| MailboxListFlag::NoSelect), - map(tag_no_case("\\Marked"), |_| MailboxListFlag::Marked), - map(tag_no_case("\\Unmarked"), |_| MailboxListFlag::Unmarked), + map(tagi(b"\\NoSelect"), |_| MailboxListFlag::NoSelect), + map(tagi(b"\\Marked"), |_| MailboxListFlag::Marked), + map(tagi(b"\\Unmarked"), |_| MailboxListFlag::Unmarked), ))); rule!(pub message_data : Response => alt(( - map(terminated(nz_number, pair(SP, tag_no_case("EXPUNGE"))), Response::Expunge), - map(separated_pair(nz_number, SP, preceded(pair(tag_no_case("FETCH"), SP), msg_att)), + map(terminated(nz_number, pair(SP, tagi(b"EXPUNGE"))), Response::Expunge), + map(separated_pair(nz_number, SP, preceded(pair(tagi(b"FETCH"), SP), msg_att)), |(n, attrs)| Response::Fetch(n, attrs)), ))); -rule!(pub msg_att : Vec => delimited(byte(b'('), - sep_list!(alt((msg_att_dynamic, msg_att_static))), -byte(b')'))); +rule!(pub msg_att : Vec => paren!(sep_list!(alt((msg_att_dynamic, msg_att_static))))); rule!(pub msg_att_dynamic : MessageAttribute => alt(( - map(preceded(pair(tag_no_case("FLAGS"), SP), - delimited(byte(b'('), sep_list!(?flag_fetch), byte(b')'))), MessageAttribute::Flags), + map(preceded(pair(tagi(b"FLAGS"), SP), + paren!(sep_list!(?flag_fetch))), MessageAttribute::Flags), never, ))); rule!(pub msg_att_static : MessageAttribute => alt(( - map(preceded(pair(tag_no_case("ENVELOPE"), SP), envelope), MessageAttribute::Envelope), - map(preceded(pair(tag_no_case("ENVELOPE"), SP), envelope), MessageAttribute::Envelope), + map(preceded(pair(tagi(b"ENVELOPE"), SP), envelope), MessageAttribute::Envelope), + map(preceded(pair(tagi(b"ENVELOPE"), SP), envelope), MessageAttribute::Envelope), ))); -rule!(pub nil : &[u8] => tag_no_case("NIL")); +rule!(pub nil : Bytes => tagi(b"NIL")); -rule!(pub nstring : Option => alt((map(string, Some), map(nil, |_| None)))); +rule!(pub nstring : Option => opt_nil!(string)); -pub(crate) fn number(i: &[u8]) -> IResult<&[u8], u32> { - map_res(map_res(take_while1(is_digit), std::str::from_utf8), |s| { - s.parse::() - })(i) +pub(crate) fn number(i: Bytes) -> IResult { + map_res(take_while1(is_digit), parse_u32)(i) } rule!(pub nz_number : u32 => verify(number, |n| *n != 0)); -rule!(pub quoted : CowU8 => delimited(DQUOTE, map(take_while1(is_quoted_char), Cow::from), DQUOTE)); +rule!(pub quoted : Bytes => delimited(DQUOTE, take_while1(is_quoted_char), DQUOTE)); fn is_quoted_char(c: u8) -> bool { is_char(c) && !is_quoted_specials(c) } rule!(pub QUOTED_CHAR : u8 => alt((satisfy(is_quoted_char), preceded(byte(b'\\'), quoted_specials)))); @@ -202,15 +247,15 @@ rule!(pub response_fatal : Response => delimited(pair(byte(b'*'), SP), rule!(pub response_tagged : Response => map(terminated(separated_pair(tag, SP, resp_cond_state), CRLF), |(tag, cond)| Response::Tagged(tag, cond))); -rule!(pub resp_cond_bye : Condition => preceded(pair(tag_no_case("BYE"), SP), +rule!(pub resp_cond_bye : Condition => preceded(pair(tagi(b"BYE"), SP), map(resp_text, |ResponseText { code, info }| Condition { status: Status::Bye, code, info }))); rule!(pub resp_cond_state : Condition => map( separated_pair( alt(( - map(tag_no_case("OK"), |_| Status::Ok), - map(tag_no_case("NO"), |_| Status::No), - map(tag_no_case("BAD"), |_| Status::Bad), + map(tagi(b"OK"), |_| Status::Ok), + map(tagi(b"NO"), |_| Status::No), + map(tagi(b"BAD"), |_| Status::Bad), )), SP, resp_text, @@ -227,16 +272,16 @@ rule!(pub resp_text : ResponseText => map(pair( ), |(code, info)| ResponseText { code, info })); rule!(pub resp_text_code : ResponseCode => alt(( - map(tag_no_case("ALERT"), |_| ResponseCode::Alert), + map(tagi(b"ALERT"), |_| ResponseCode::Alert), map(capability_data, ResponseCode::Capabilities), ))); -rule!(pub string : CowU8 => alt((quoted, literal))); +rule!(pub string : Bytes => alt((quoted, literal))); pub(crate) fn is_tag_char(c: u8) -> bool { is_astring_char(c) && c != b'+' } -rule!(pub tag : Tag => map(take_while1(is_tag_char), |s: &[u8]| Tag(s.into()))); +rule!(pub tag : Tag => map(take_while1(is_tag_char), Tag)); -rule!(pub text : CowU8 => map(take_while1(is_text_char), Cow::from)); +rule!(pub text : Bytes => map(take_while1(is_text_char), Bytes::from)); pub(crate) fn is_text_char(c: u8) -> bool { is_char(c) && !is_cr(c) && !is_lf(c) } rule!(pub TEXT_CHAR : u8 => satisfy(is_text_char));