This commit is contained in:
Michael Zhang 2021-02-19 23:03:33 -06:00
parent f5d3a89641
commit 921e4b02d1
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
11 changed files with 425 additions and 17 deletions

121
Cargo.lock generated
View file

@ -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",

View file

@ -9,7 +9,7 @@ readme = "README.md"
license = "GPL-3.0-or-later"
[workspace]
members = ["imap"]
members = ["imap", "strings"]
[dependencies]
# log = "0.4.14"

View file

@ -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"

View file

@ -1,4 +1,4 @@
use imap_proto::Response;
use panorama_imap::Response;
use std::io::Write;
fn main() -> std::io::Result<()> {

105
imap/src/client/inner.rs Normal file
View file

@ -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<dyn Fn()>;
/// The private Client struct, that is shared by all of the exported structs in the state machine.
pub struct Client<C> {
conn: WriteHalf<C>,
symbols: StringStore,
id: usize,
handlers: Arc<RwLock<HashMap<usize, bool>>>,
/// Cached capabilities that shouldn't change between
caps: Vec<StringEntry>,
handle: JoinHandle<Result<()>>,
}
impl<C> Client<C>
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<C>, usize);
impl<'a, C> Future for ExecHandle<'a, C> {
type Output = ();
fn poll(self: Pin<&mut Self>, _: &mut Context) -> Poll<Self::Output> {
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?;
}
}

86
imap/src/client/mod.rs Normal file
View file

@ -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<ClientUnauthenticated> {
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<TcpStream>,
}
impl ClientUnencryptedUnauthenticated {
pub async fn upgrade(&self) {}
}
/// An IMAP client that isn't authenticated.
pub struct ClientEncryptedUnauthenticated {
/// Connection to the remote server
inner: Client<TlsStream<TcpStream>>,
}
impl ClientEncryptedUnauthenticated {}

14
imap/src/command/mod.rs Normal file
View file

@ -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"),
}
}
}

View file

@ -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;

1
imap/src/response/mod.rs Normal file
View file

@ -0,0 +1 @@
pub struct Response {}

7
strings/Cargo.toml Normal file
View file

@ -0,0 +1,7 @@
[package]
name = "panorama-strings"
version = "0.1.0"
authors = ["Michael Zhang <mail@mzhang.io>"]
edition = "2018"
[dependencies]

87
strings/src/lib.rs Normal file
View file

@ -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<T> {
capacity: usize,
store: Vec<Entry<T>>,
index: HashMap<T, usize>,
head: usize,
tail: usize,
}
impl<T: Hash + Eq> Store<T> {
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<T> {
val: T,
prev: usize,
next: usize,
}
impl<T: Copy + Clone> Deref for Entry<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.val
}
}
impl<T> AsRef<T> for Entry<T> {
fn as_ref(&self) -> &T {
&self.val
}
}