diff --git a/Cargo.lock b/Cargo.lock index 97b405b..9176b09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -781,6 +781,7 @@ version = "0.0.1" dependencies = [ "anyhow", "assert_matches", + "async-trait", "derive_builder", "futures", "log", diff --git a/imap/Cargo.toml b/imap/Cargo.toml index caad1dc..a3a5166 100644 --- a/imap/Cargo.toml +++ b/imap/Cargo.toml @@ -13,6 +13,7 @@ maintenance = { status = "passively-maintained" } [dependencies] anyhow = "1.0.38" +async-trait = "0.1.42" derive_builder = "0.9.0" futures = "0.3.12" log = "0.4.14" diff --git a/imap/src/client/auth.rs b/imap/src/client/auth.rs new file mode 100644 index 0000000..eb3d519 --- /dev/null +++ b/imap/src/client/auth.rs @@ -0,0 +1,48 @@ +use anyhow::Result; + +use crate::command::Command; +use crate::response::{Response, 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; + + 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?; + + if !matches!( + result, + Response::Done { + status: Status::Ok, + .. + } + ) { + bail!("unable to login: {:?}", result); + } + + Ok(::convert_client(client)) + } +} diff --git a/imap/src/client/inner.rs b/imap/src/client/inner.rs index e62165c..b4e02f7 100644 --- a/imap/src/client/inner.rs +++ b/imap/src/client/inner.rs @@ -116,7 +116,7 @@ where /// Executes the CAPABILITY command pub async fn capabilities(&mut self, force: bool) -> Result<()> { { - let caps = &*self.caps.read(); + let caps = self.caps.read(); if caps.is_some() && !force { return Ok(()); } @@ -134,13 +134,10 @@ where .iter() .find(|resp| matches!(resp, Response::Capabilities(_))) { - let caps = &mut *self.caps.write(); + let mut caps = self.caps.write(); *caps = Some(new_caps.iter().cloned().collect()); } - // if let Response::Capabilities(caps) = resp { - // debug!("capabilities: {:?}", caps); - // } Ok(()) } @@ -183,7 +180,7 @@ where let cap = parse_capability(cap)?; self.capabilities(false).await?; - let caps = &*self.caps.read(); + let caps = self.caps.read(); // TODO: refresh caps let caps = caps.as_ref().unwrap(); @@ -276,6 +273,7 @@ where debug!("got a new line {:?}", next_line); let resp = parse_response(next_line)?; + // if this is the very first message, treat it as a greeting if let Some(greeting) = greeting.take() { let (greeting, waker) = &mut *greeting.write(); debug!("received greeting!"); @@ -285,17 +283,29 @@ where } } + // update capabilities list + // TODO: probably not really necessary here (done somewhere else)? + if let Response::Capabilities(new_caps) + | Response::Data { + status: Status::Ok, + code: Some(ResponseCode::Capabilities(new_caps)), + .. + } = &resp + { + let caps = &mut *caps.write(); + *caps = Some(new_caps.iter().cloned().collect()); + debug!("new caps: {:?}", caps); + } + match &resp { - // capabilities list - Response::Capabilities(new_caps) - | Response::Data { - status: Status::Ok, - code: Some(ResponseCode::Capabilities(new_caps)), - .. + Response::Data { + status: Status::Ok, .. } => { - let caps = &mut *caps.write(); - *caps = Some(new_caps.iter().cloned().collect()); - debug!("new caps: {:?}", caps); + let mut results = results.write(); + if let Some((_, _, intermediate, _)) = results.iter_mut().next() { + debug!("pushed to intermediate: {:?}", resp); + intermediate.push(resp); + } } // bye @@ -308,7 +318,7 @@ where Response::Done { tag, .. } => { if tag.starts_with(TAG_PREFIX) { - let id = tag.trim_start_matches(TAG_PREFIX).parse::()?; + // let id = tag.trim_start_matches(TAG_PREFIX).parse::()?; let mut results = results.write(); if let Some((_, opt, _, waker)) = results.iter_mut().next() { *opt = Some(resp); @@ -321,38 +331,6 @@ where _ => {} } - - // debug!("parsed as: {:?}", resp); - // let next_line = next_line.trim_end_matches('\n').trim_end_matches('\r'); - - // let mut parts = next_line.split(" "); - // let tag = parts.next().unwrap(); - // let rest = parts.collect::>().join(" "); - - // if tag == "*" { - // debug!("UNTAGGED {:?}", rest); - - // // TODO: verify that the greeting is actually an OK - // if let Some(greeting) = greeting.take() { - // let (greeting, waker) = &mut *greeting.write(); - // debug!("got greeting"); - // *greeting = true; - // if let Some(waker) = waker.take() { - // waker.wake(); - // } - // } - // } else if tag.starts_with(TAG_PREFIX) { - // let id = tag.trim_start_matches(TAG_PREFIX).parse::()?; - // debug!("set {} to {:?}", id, rest); - // let mut results = results.write(); - // if let Some((c, w)) = results.get_mut(&id) { - // // *c = Some(rest.to_string()); - // *c = Some(resp); - // if let Some(waker) = w.take() { - // waker.wake(); - // } - // } - // } } Either::Right((_, _)) => { diff --git a/imap/src/client/mod.rs b/imap/src/client/mod.rs index 409a056..fb6fd7d 100644 --- a/imap/src/client/mod.rs +++ b/imap/src/client/mod.rs @@ -33,6 +33,7 @@ //! # } //! ``` +pub mod auth; mod inner; use std::sync::Arc; @@ -89,22 +90,18 @@ impl ClientConfig { let inner = Client::new(conn, self); inner.wait_for_greeting().await; - return Ok(ClientUnauthenticated::Encrypted( - ClientUnauthenticatedEncrypted { inner }, - )); + return Ok(ClientUnauthenticated::Encrypted(inner)); } else { let inner = Client::new(conn, self); inner.wait_for_greeting().await; - return Ok(ClientUnauthenticated::Unencrypted( - ClientUnauthenticatedUnencrypted { inner }, - )); + return Ok(ClientUnauthenticated::Unencrypted(inner)); } } } pub enum ClientUnauthenticated { - Encrypted(ClientUnauthenticatedEncrypted), - Unencrypted(ClientUnauthenticatedUnencrypted), + Encrypted(Client>), + Unencrypted(Client), } impl ClientUnauthenticated { @@ -113,44 +110,46 @@ impl ClientUnauthenticated { // this is a no-op, we don't need to upgrade ClientUnauthenticated::Encrypted(_) => Ok(self), ClientUnauthenticated::Unencrypted(e) => { - let client = ClientUnauthenticatedEncrypted { - inner: e.inner.upgrade().await?, - }; - Ok(ClientUnauthenticated::Encrypted(client)) + Ok(ClientUnauthenticated::Encrypted(e.upgrade().await?)) } } } - /// TODO: Exposing low-level execute , shoudl remove later - pub async fn execute(&mut self, cmd: Command) -> Result<(Response, Vec)> { + /// Exposing low-level execute + async fn execute(&mut self, cmd: Command) -> Result<(Response, Vec)> { match self { - ClientUnauthenticated::Encrypted(e) => e.inner.execute(cmd).await, - ClientUnauthenticated::Unencrypted(e) => e.inner.execute(cmd).await, + 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.inner.has_capability(cap).await, - ClientUnauthenticated::Unencrypted(e) => e.inner.has_capability(cap).await, + ClientUnauthenticated::Encrypted(e) => e.has_capability(cap).await, + ClientUnauthenticated::Unencrypted(e) => e.has_capability(cap).await, } } } -pub struct ClientUnauthenticatedUnencrypted { - /// Connection to the remote server - inner: Client, +pub enum ClientAuthenticated { + Encrypted(Client>), + Unencrypted(Client), } -impl ClientUnauthenticatedUnencrypted { - pub async fn upgrade(&self) {} -} +impl ClientAuthenticated { + /// Exposing low-level execute + async fn execute(&mut self, cmd: Command) -> Result<(Response, Vec)> { + match self { + ClientAuthenticated::Encrypted(e) => e.execute(cmd).await, + ClientAuthenticated::Unencrypted(e) => e.execute(cmd).await, + } + } -/// An IMAP client that isn't authenticated. -pub struct ClientUnauthenticatedEncrypted { - /// Connection to the remote server - inner: Client>, + pub async fn list(&mut self) -> Result<()> { + let cmd = Command::List { reference: "".to_owned(), mailbox: "*".to_owned() }; + let resp = self.execute(cmd).await?; + debug!("list response: {:?}", resp); + Ok(()) + } } - -impl ClientUnauthenticatedEncrypted {} diff --git a/imap/src/command/mod.rs b/imap/src/command/mod.rs index b922226..8e5b0e5 100644 --- a/imap/src/command/mod.rs +++ b/imap/src/command/mod.rs @@ -6,14 +6,19 @@ pub enum Command { Capability, Starttls, Login { username: String, password: String }, + Select { mailbox: String }, + List { reference: String, mailbox: String }, } impl fmt::Display for Command { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Command::*; match self { - Command::Capability => write!(f, "CAPABILITY"), - Command::Starttls => write!(f, "STARTTLS"), - Command::Login { username, password } => write!(f, "LOGIN {} {}", username, password), + Capability => write!(f, "CAPABILITY"), + Starttls => write!(f, "STARTTLS"), + Login { username, password } => write!(f, "LOGIN {} {}", username, password), + Select { mailbox } => write!(f, "SELECT {}", mailbox), + List { reference, mailbox } => write!(f, "LIST {:?} {:?}", reference, mailbox), } } } diff --git a/imap/src/lib.rs b/imap/src/lib.rs index 475b4d8..19dbf35 100644 --- a/imap/src/lib.rs +++ b/imap/src/lib.rs @@ -1,6 +1,8 @@ #[macro_use] extern crate anyhow; #[macro_use] +extern crate async_trait; +#[macro_use] extern crate derive_builder; #[macro_use] extern crate futures; diff --git a/imap/src/parser/mod.rs b/imap/src/parser/mod.rs index 3483e9b..3e84ba4 100644 --- a/imap/src/parser/mod.rs +++ b/imap/src/parser/mod.rs @@ -104,9 +104,6 @@ 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() { @@ -155,7 +152,7 @@ fn build_status(pair: Pair) -> Status { } } -fn build_flag_list(pair: Pair) -> Vec { +fn build_flag_list(pair: Pair) -> Vec { if !matches!(pair.as_rule(), Rule::flag_list) { unreachable!("{:#?}", pair); } @@ -163,18 +160,18 @@ fn build_flag_list(pair: Pair) -> Vec { pair.into_inner().map(build_flag).collect() } -fn build_flag(pair: Pair) -> Flag { +fn build_flag(pair: Pair) -> MailboxFlag { 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()), + "\\Answered" => MailboxFlag::Answered, + "\\Flagged" => MailboxFlag::Flagged, + "\\Deleted" => MailboxFlag::Deleted, + "\\Seen" => MailboxFlag::Seen, + "\\Draft" => MailboxFlag::Draft, + s if s.starts_with("\\") => MailboxFlag::Ext(s.to_owned()), _ => unreachable!("{:#?}", pair.as_str()), } } @@ -195,10 +192,28 @@ fn build_mailbox_data(pair: Pair) -> MailboxData { MailboxData::Flags(flags) } Rule::mailbox_data_recent => MailboxData::Recent(build_number(pair)), + Rule::mailbox_data_list => { + let mut pairs = pair.into_inner(); + let pair = pairs.next().unwrap(); + let (flags, delimiter, name) = build_mailbox_list(pair); + MailboxData::List { flags, delimiter, name } + }, _ => unreachable!("{:#?}", pair), } } +fn build_mailbox_list(pair: Pair) -> (Vec, Option, String) { + todo!() +} + +fn build_mbx_list_flags(pair: Pair) -> Vec { + if !matches!(pair.as_rule(), Rule::mbx_list_flags) { + unreachable!("{:#?}", pair); + } + + todo!() +} + fn build_number(pair: Pair) -> T where T: FromStr, @@ -266,11 +281,11 @@ mod tests { 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, + MailboxFlag::Answered, + MailboxFlag::Flagged, + MailboxFlag::Deleted, + MailboxFlag::Seen, + MailboxFlag::Draft, ]))) ); diff --git a/imap/src/parser/rfc3501.pest b/imap/src/parser/rfc3501.pest index 04b9095..0db4591 100644 --- a/imap/src/parser/rfc3501.pest +++ b/imap/src/parser/rfc3501.pest @@ -64,11 +64,13 @@ 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 | mailbox_data_recent } +mailbox_data = { mailbox_data_flags | mailbox_data_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 } +mailbox_data_list = { ^"LIST" ~ sp ~ mailbox_list } +mailbox_list = { "(" ~ mbx_list_flags* ~ ")" ~ sp ~ mailbox_list_string ~ sp ~ mailbox } +mailbox_list_string = { dquote ~ quoted_char ~ dquote | nil } 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" } diff --git a/imap/src/response/mod.rs b/imap/src/response/mod.rs index c7adfa4..cfda6c8 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, @@ -88,7 +88,7 @@ pub enum MailboxData { } #[derive(Debug, Eq, PartialEq, Clone)] -pub enum Flag { +pub enum MailboxFlag { Answered, Flagged, Deleted, diff --git a/src/mail/mod.rs b/src/mail/mod.rs index 77d45fd..9c3c40c 100644 --- a/src/mail/mod.rs +++ b/src/mail/mod.rs @@ -3,7 +3,7 @@ use anyhow::Result; use futures::{future::FutureExt, stream::StreamExt}; use panorama_imap::{ - client::{ClientBuilder, ClientConfig}, + client::{ClientBuilder, ClientConfig, auth::{self, Auth}}, command::Command as ImapCommand, }; use tokio::{sync::mpsc::UnboundedReceiver, task::JoinHandle}; @@ -78,7 +78,7 @@ async fn imap_main(acct: MailAccountConfig) -> Result<()> { debug!("connecting to {}:{}", &acct.imap.server, acct.imap.port); let unauth = builder.open().await?; - let mut unauth = if matches!(acct.imap.tls, TlsMethod::Starttls) { + let unauth = if matches!(acct.imap.tls, TlsMethod::Starttls) { debug!("attempting to upgrade"); let client = unauth.upgrade().await?; debug!("upgrade successful"); @@ -89,18 +89,21 @@ async fn imap_main(acct: MailAccountConfig) -> Result<()> { debug!("preparing to auth"); // check if the authentication method is supported - let authed = match acct.imap.auth { + let mut 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); + let auth = auth::Plain {username, password}; + auth.perform_auth(unauth).await? } }; + debug!("authentication successful!"); + // debug!("sending CAPABILITY"); // let result = unauth.capabilities().await?; loop { + debug!("listing all emails..."); + authed.list().await?; tokio::time::sleep(std::time::Duration::from_secs(60)).await; debug!("heartbeat"); } diff --git a/src/main.rs b/src/main.rs index f6402c1..19eecf3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,8 @@ struct Opt { #[tokio::main(flavor = "multi_thread")] async fn main() -> Result<()> { + let now = chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"); + // parse command line arguments into options struct let opt = Opt::from_args(); @@ -33,8 +35,8 @@ async fn main() -> Result<()> { let mut logger = fern::Dispatch::new() .format(move |out, message, record| { out.finish(format_args!( - "[{}][{}] {}", - // chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), + "{}[{}][{}] {}", + now, record.target(), colors.color(record.level()), message