diff --git a/Cargo.lock b/Cargo.lock index fc0cbec..42bb40e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,9 +105,9 @@ checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" [[package]] name = "bumpalo" -version = "3.6.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" [[package]] name = "byteorder" @@ -123,9 +123,9 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" [[package]] name = "cc" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" [[package]] name = "cfg-if" @@ -155,7 +155,7 @@ dependencies = [ "ansi_term 0.11.0", "atty", "bitflags", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width", "vec_map", @@ -223,6 +223,66 @@ dependencies = [ "winapi", ] +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.9.3", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" +dependencies = [ + "darling", + "derive_builder_core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "fast_chemail" version = "0.9.6" @@ -232,6 +292,12 @@ dependencies = [ "ascii_utils", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foreign-types" version = "0.3.2" @@ -409,6 +475,12 @@ dependencies = [ "winutil", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "inotify" version = "0.9.2" @@ -520,9 +592,9 @@ checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] name = "mio" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e50ae3f04d169fcc9bde0b547d1c205219b7157e07ded9c5aff03e0637cb3ed7" +checksum = "dc250d6848c90d719ea2ce34546fb5df7af1d3fd189d10bf7bad80bfcebecd95" dependencies = [ "libc", "log", @@ -693,10 +765,23 @@ dependencies = [ name = "panorama-imap" version = "0.0.1" dependencies = [ + "anyhow", "assert_matches", + "derive_builder", + "futures", "nom 6.1.2", + "panorama-strings", + "parking_lot", + "tokio", + "tokio-rustls", + "tracing", + "webpki-roots", ] +[[package]] +name = "panorama-strings" +version = "0.1.0" + [[package]] name = "parking_lot" version = "0.11.1" @@ -1111,6 +1196,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + [[package]] name = "structopt" version = "0.3.21" @@ -1276,9 +1367,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d40a22fd029e33300d8d89a5cc8ffce18bb7c587662f54629e94c9de5487f3" +checksum = "f77d3842f76ca899ff2dbcf231c5c65813dea431301d6eb686279c15c4464f12" dependencies = [ "cfg-if", "pin-project-lite", @@ -1299,9 +1390,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f080ea7e4107844ef4766459426fa2d5c1ada2e47edba05dc7fa99d9629f47" +checksum = "a8a9bd1db7706f2373a190b0d067146caa39350c486f3d455b0e33b431f94c07" dependencies = [ "proc-macro2", "quote", @@ -1319,9 +1410,9 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e0f8c7178e13481ff6765bd169b33e8d554c5d2bbede5e32c356194be02b9b9" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" dependencies = [ "lazy_static", "log", @@ -1340,9 +1431,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1fa8f0c8f4c594e4fc9debc1990deab13238077271ba84dd853d54902ee3401" +checksum = "8ab8966ac3ca27126141f7999361cc97dd6fb4b71da04c02044fa9045d98bb96" dependencies = [ "ansi_term 0.12.1", "chrono", diff --git a/Cargo.toml b/Cargo.toml index ee9112c..e57011e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ readme = "README.md" license = "GPL-3.0-or-later" [workspace] -members = ["imap"] +members = ["imap", "strings"] [dependencies] # log = "0.4.14" diff --git a/imap/Cargo.toml b/imap/Cargo.toml index 38bb6a3..d78bea1 100644 --- a/imap/Cargo.toml +++ b/imap/Cargo.toml @@ -12,7 +12,16 @@ edition = "2018" maintenance = { status = "passively-maintained" } [dependencies] +anyhow = "1.0.38" +tokio = { version = "1.1.1", features = ["full"] } +futures = "0.3.12" nom = { version = "6.1.2", default-features = false, features = ["std"] } +derive_builder = "0.9.0" +tokio-rustls = "0.22.0" +webpki-roots = "0.21.0" +panorama-strings = { path = "../strings", version = "0" } +parking_lot = "0.11.1" +tracing = "0.1.23" [dev-dependencies] assert_matches = "1.3" diff --git a/imap/examples/parse_response.rs b/imap/examples/parse_response.rs index c506ff1..5751892 100644 --- a/imap/examples/parse_response.rs +++ b/imap/examples/parse_response.rs @@ -1,4 +1,4 @@ -use imap_proto::Response; +use panorama_imap::Response; use std::io::Write; fn main() -> std::io::Result<()> { diff --git a/imap/src/client/inner.rs b/imap/src/client/inner.rs new file mode 100644 index 0000000..4dcf258 --- /dev/null +++ b/imap/src/client/inner.rs @@ -0,0 +1,105 @@ +use std::collections::HashMap; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use anyhow::Result; +use futures::future::{Future, FutureExt}; +use panorama_strings::{StringEntry, StringStore}; +use parking_lot::RwLock; +use tokio::{ + io::{ + self, AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, + WriteHalf, + }, + task::JoinHandle, +}; + +use crate::command::Command; + +pub type BoxedFunc = Box; + +/// The private Client struct, that is shared by all of the exported structs in the state machine. +pub struct Client { + conn: WriteHalf, + symbols: StringStore, + + id: usize, + handlers: Arc>>, + + /// Cached capabilities that shouldn't change between + caps: Vec, + handle: JoinHandle>, +} + +impl Client +where + C: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + /// Creates a new client that wraps a connection + pub fn new(conn: C) -> Self { + let (read_half, write_half) = io::split(conn); + let listen_fut = tokio::spawn(listen(read_half)); + + Client { + conn: write_half, + symbols: StringStore::new(256), + id: 0, + handlers: Arc::new(RwLock::new(HashMap::new())), + caps: Vec::new(), + handle: listen_fut, + } + } + + /// Sends a command to the server and returns a handle to retrieve the result + pub async fn execute(&mut self, cmd: Command) -> Result<()> { + let id = self.id; + self.id += 1; + + { + let mut handlers = self.handlers.write(); + handlers.insert(id, false); + } + + let cmd_str = cmd.to_string(); + self.conn.write_all(cmd_str.as_bytes()).await?; + + ExecHandle(self, id).await; + Ok(()) + } + + /// Executes the CAPABILITY command + pub async fn supports(&mut self) { + let cmd = Command::Capability; + let result = self.execute(cmd).await; + debug!("poggers {:?}", result); + } +} + +pub struct ExecHandle<'a, C>(&'a Client, usize); + +impl<'a, C> Future for ExecHandle<'a, C> { + type Output = (); + fn poll(self: Pin<&mut Self>, _: &mut Context) -> Poll { + let state = { + let handlers = self.0.handlers.read(); + handlers.get(&self.1).cloned() + }; + + // TODO: handle the None case here + let state = state.unwrap(); + + match state { + true => Poll::Ready(()), + false => Poll::Pending, + } + } +} + +async fn listen(conn: impl AsyncRead + Unpin) -> Result<()> { + let mut reader = BufReader::new(conn); + loop { + let mut next_line = String::new(); + reader.read_line(&mut next_line).await?; + } +} diff --git a/imap/src/client/mod.rs b/imap/src/client/mod.rs new file mode 100644 index 0000000..178e5f9 --- /dev/null +++ b/imap/src/client/mod.rs @@ -0,0 +1,86 @@ +//! IMAP Client +//! === +//! +//! The IMAP client in this module is implemented as a state machine in the type system: methods +//! that are not supported in a particular state (ex. fetch in an unauthenticated state) cannot be +//! expressed in the type system entirely. + +mod inner; + +use std::sync::Arc; + +use anyhow::Result; +use tokio::{ + io::{self, AsyncRead, AsyncWrite, ReadHalf, WriteHalf}, + net::TcpStream, +}; +use tokio_rustls::{client::TlsStream, rustls::ClientConfig, webpki::DNSNameRef, TlsConnector}; + +use self::inner::Client; + +/// An IMAP client that hasn't been connected yet. +#[derive(Builder, Clone, Debug)] +pub struct ClientNotConnected { + /// The hostname of the IMAP server. If using TLS, must be an address + hostname: String, + + /// The port of the IMAP server. + port: u16, + + /// Whether or not the client is using an encrypted stream. + /// + /// To upgrade the connection later, use the upgrade method. + tls: bool, +} + +impl ClientNotConnected { + pub async fn connect(self) -> Result { + let hostname = self.hostname.as_ref(); + let port = self.port; + let conn = TcpStream::connect((hostname, port)).await?; + + if self.tls { + let mut tls_config = ClientConfig::new(); + tls_config + .root_store + .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + let tls_config = TlsConnector::from(Arc::new(tls_config)); + let dnsname = DNSNameRef::try_from_ascii_str(hostname).unwrap(); + let conn = tls_config.connect(dnsname, conn).await?; + + let inner = Client::new(conn); + return Ok(ClientUnauthenticated::Encrypted( + ClientEncryptedUnauthenticated { inner }, + )); + } + + let inner = Client::new(conn); + return Ok(ClientUnauthenticated::Unencrypted( + ClientUnencryptedUnauthenticated { inner }, + )); + } +} + +pub enum ClientUnauthenticated { + Encrypted(ClientEncryptedUnauthenticated), + Unencrypted(ClientUnencryptedUnauthenticated), +} + +impl ClientUnauthenticated {} + +pub struct ClientUnencryptedUnauthenticated { + /// Connection to the remote server + inner: Client, +} + +impl ClientUnencryptedUnauthenticated { + pub async fn upgrade(&self) {} +} + +/// An IMAP client that isn't authenticated. +pub struct ClientEncryptedUnauthenticated { + /// Connection to the remote server + inner: Client>, +} + +impl ClientEncryptedUnauthenticated {} diff --git a/imap/src/command/mod.rs b/imap/src/command/mod.rs new file mode 100644 index 0000000..44f5c2a --- /dev/null +++ b/imap/src/command/mod.rs @@ -0,0 +1,14 @@ +use std::fmt; + +/// Commands, without the tag part. +pub enum Command { + Capability, +} + +impl fmt::Display for Command { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Command::Capability => write!(f, "CAPABILITY"), + } + } +} diff --git a/imap/src/lib.rs b/imap/src/lib.rs index 8363c84..e8b873e 100644 --- a/imap/src/lib.rs +++ b/imap/src/lib.rs @@ -1,5 +1,13 @@ +#[macro_use] +extern crate derive_builder; +#[macro_use] +extern crate tracing; + pub mod builders; +pub mod client; +pub mod command; pub mod parser; +pub mod response; pub mod types; pub use crate::parser::ParseResult; diff --git a/imap/src/response/mod.rs b/imap/src/response/mod.rs new file mode 100644 index 0000000..12a31b8 --- /dev/null +++ b/imap/src/response/mod.rs @@ -0,0 +1 @@ +pub struct Response {} diff --git a/strings/Cargo.toml b/strings/Cargo.toml new file mode 100644 index 0000000..7f47599 --- /dev/null +++ b/strings/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "panorama-strings" +version = "0.1.0" +authors = ["Michael Zhang "] +edition = "2018" + +[dependencies] diff --git a/strings/src/lib.rs b/strings/src/lib.rs new file mode 100644 index 0000000..856f211 --- /dev/null +++ b/strings/src/lib.rs @@ -0,0 +1,87 @@ +use std::collections::HashMap; +use std::hash::Hash; +use std::marker::PhantomData; +use std::ops::Deref; + +pub type StringStore = Store<&'static str>; +pub type StringEntry = Entry<&'static str>; + +pub struct Store { + capacity: usize, + store: Vec>, + index: HashMap, + head: usize, + tail: usize, +} + +impl Store { + pub fn new(capacity: usize) -> Self { + Store { + capacity, + store: Vec::with_capacity(capacity), + index: HashMap::new(), + head: 0, + tail: 0, + } + } + + pub fn insert(&mut self, val: T) { + if self.index.contains_key(&val) { + return; + } + + let entry = Entry { + val, + prev: 0, + next: 0, + }; + + let new_head = if self.store.len() == self.store.capacity() { + let idx = self.pop_back(); + self.store[idx] = entry; + idx + } else { + self.store.push(entry); + self.store.len() - 1 + }; + + self.push_front(new_head); + } + + fn pop_back(&mut self) -> usize { + let old_tail = self.tail; + let new_tail = self.store[old_tail].prev; + self.tail = new_tail; + old_tail + } + + fn push_front(&mut self, idx: usize) { + if self.store.len() == 1 { + self.tail = idx; + } else { + self.store[self.head].prev = idx; + self.store[idx].next = idx; + } + self.head = idx; + } +} + +#[derive(Copy, Clone)] +pub struct Entry { + val: T, + prev: usize, + next: usize, +} + +impl Deref for Entry { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.val + } +} + +impl AsRef for Entry { + fn as_ref(&self) -> &T { + &self.val + } +}