diff --git a/.gitignore b/.gitignore index f9aa6ce..7ae2572 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /.env /output.log +/config.toml diff --git a/Cargo.lock b/Cargo.lock index c370ff9..5892636 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -736,9 +736,11 @@ dependencies = [ "notify", "pin-project", "rustls-connector", + "serde", "tokio", "tokio-rustls", "tokio-util", + "toml", "webpki-roots", "xdg", ] @@ -1033,6 +1035,9 @@ name = "serde" version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_derive" @@ -1205,6 +1210,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + [[package]] name = "unicode-xid" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 4cb1e07..ac57e5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,5 @@ webpki-roots = "0.21.0" xdg = "2.2.0" imap = { path = "imap" } +toml = "0.5.8" +serde = { version = "1.0.123", features = ["derive"] } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..f79dccf --- /dev/null +++ b/src/config.rs @@ -0,0 +1,8 @@ +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Config { + pub server: String, + pub port: u16, + + pub username: String, + pub password: String, +} diff --git a/src/mail.rs b/src/mail.rs index c91eceb..5cf8c50 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -14,41 +14,49 @@ use imap::{ parser::parse_response, types::{Capability, RequestId, Response, ResponseCode, State, Status}, }; -use tokio::{net::TcpStream, sync::mpsc}; +use tokio::{ + net::TcpStream, + sync::mpsc::{self, UnboundedReceiver}, +}; use tokio_rustls::{rustls::ClientConfig, webpki::DNSNameRef, TlsConnector}; use tokio_util::codec::{Decoder, LinesCodec, LinesCodecError}; +use crate::config::Config; + pub enum MailCommand { + Refresh, Raw(Command), } -pub async fn run_mail(server: impl AsRef, port: u16) -> Result<()> { - let server = server.as_ref(); +pub async fn run_mail(config: Config, cmd_in: UnboundedReceiver) -> Result<()> { + let server = config.server.as_str(); + let port = config.port; + let client = TcpStream::connect((server, port)).await?; let codec = LinesCodec::new(); let framed = codec.framed(client); let mut state = State::NotAuthenticated; let (sink, stream) = framed.split::(); - let result = listen_loop(&mut state, sink, stream).await?; + let result = listen_loop(config.clone(), &mut state, sink, stream, false).await?; if let LoopExit::NegotiateTls(stream, sink) = result { debug!("negotiating tls"); - let mut config = ClientConfig::new(); - config + let mut tls_config = ClientConfig::new(); + tls_config .root_store .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); - let config = TlsConnector::from(Arc::new(config)); + let tls_config = TlsConnector::from(Arc::new(tls_config)); let dnsname = DNSNameRef::try_from_ascii_str(server).unwrap(); // reconstruct the original stream let stream = stream.reunite(sink)?.into_inner(); // let stream = TcpStream::connect((server, port)).await?; - let stream = config.connect(dnsname, stream).await?; + let stream = tls_config.connect(dnsname, stream).await?; let codec = LinesCodec::new(); let framed = codec.framed(stream); let (sink, stream) = framed.split::(); - listen_loop(&mut state, sink, stream).await?; + listen_loop(config.clone(), &mut state, sink, stream, true).await?; } Ok(()) @@ -59,7 +67,13 @@ enum LoopExit { Closed, } -async fn listen_loop(st: &mut State, sink: S2, mut stream: S) -> Result> +async fn listen_loop( + config: Config, + st: &mut State, + sink: S2, + mut stream: S, + with_ssl: bool, +) -> Result> where S: Stream> + Unpin, S2: Sink + Unpin, @@ -68,6 +82,14 @@ where let (tx, mut rx) = mpsc::unbounded_channel::<()>(); let mut cmd_mgr = CommandManager::new(sink); + if with_ssl { + let cmd = Command { + args: b"CAPABILITY".to_vec(), + next_state: Some(State::Authenticated), + }; + cmd_mgr.send(cmd, |_| {}).await?; + } + loop { let fut1 = stream.next(); let fut2 = rx.recv(); @@ -95,23 +117,39 @@ where code: Some(ResponseCode::Capabilities(caps)), .. } => { - let mut has_starttls = false; - for cap in caps { - if let Capability::Atom("STARTTLS") = cap { - has_starttls = true; + if !with_ssl { + // prepare to do TLS negotiation + let mut has_starttls = false; + for cap in caps { + if let Capability::Atom("STARTTLS") = cap { + has_starttls = true; + } + } + if has_starttls { + let cmd = Command { + args: b"STARTTLS".to_vec(), + next_state: None, + }; + let tx = tx.clone(); + cmd_mgr + .send(cmd, move |_| { + tx.send(()).unwrap(); + }) + .await?; } } - if has_starttls { + } + + Response::Capabilities(caps) => { + if with_ssl { + // send authentication information let cmd = Command { - args: b"STARTTLS".to_vec(), - next_state: None, + args: format!("LOGIN {} {}", config.username, config.password) + .as_bytes() + .to_vec(), + next_state: Some(State::Authenticated), }; - let tx = tx.clone(); - cmd_mgr - .send(cmd, move |_| { - tx.send(()).unwrap(); - }) - .await?; + cmd_mgr.send(cmd, |_| {}).await?; } } @@ -169,6 +207,8 @@ where let cmd_str = std::str::from_utf8(&cmd.args)?; let full_str = format!("{} {}", tag_str, cmd_str); self.in_flight.insert(tag_str.clone(), cb); + + debug!(">>> {:?}", full_str); self.sink .send(full_str) .await diff --git a/src/main.rs b/src/main.rs index 6756524..2f32862 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,13 +4,21 @@ extern crate anyhow; extern crate crossterm; #[macro_use] extern crate log; +#[macro_use] +extern crate serde; +mod config; mod mail; mod ui; +use std::fs::File; +use std::io::Read; + use anyhow::Result; use futures::future::TryFutureExt; -use tokio::sync::oneshot; +use tokio::sync::{mpsc, oneshot}; + +use crate::config::Config; type ExitSender = oneshot::Sender<()>; @@ -18,9 +26,17 @@ type ExitSender = oneshot::Sender<()>; async fn main() -> Result<()> { setup_logger()?; - let (exit_tx, exit_rx) = oneshot::channel::<()>(); + let config: Config = { + let mut config_file = File::open("config.toml")?; + let mut contents = Vec::new(); + config_file.read_to_end(&mut contents)?; + toml::from_slice(&contents)? + }; - tokio::spawn(mail::run_mail("mzhang.io", 143).unwrap_or_else(report_err)); + let (exit_tx, exit_rx) = oneshot::channel::<()>(); + let (mail_tx, mail_rx) = mpsc::unbounded_channel(); + + tokio::spawn(mail::run_mail(config.clone(), mail_rx).unwrap_or_else(report_err)); let mut stdout = std::io::stdout(); tokio::spawn(ui::run_ui(stdout, exit_tx).unwrap_or_else(report_err));