authentication works for the most part

This commit is contained in:
Michael Zhang 2021-02-22 22:01:39 -06:00
parent 2b97aca995
commit 8be5d65435
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
12 changed files with 165 additions and 109 deletions

1
Cargo.lock generated
View file

@ -781,6 +781,7 @@ version = "0.0.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assert_matches", "assert_matches",
"async-trait",
"derive_builder", "derive_builder",
"futures", "futures",
"log", "log",

View file

@ -13,6 +13,7 @@ maintenance = { status = "passively-maintained" }
[dependencies] [dependencies]
anyhow = "1.0.38" anyhow = "1.0.38"
async-trait = "0.1.42"
derive_builder = "0.9.0" derive_builder = "0.9.0"
futures = "0.3.12" futures = "0.3.12"
log = "0.4.14" log = "0.4.14"

48
imap/src/client/auth.rs Normal file
View file

@ -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<ClientAuthenticated>;
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<ClientAuthenticated> {
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(<Self as Auth>::convert_client(client))
}
}

View file

@ -116,7 +116,7 @@ where
/// Executes the CAPABILITY command /// Executes the CAPABILITY command
pub async fn capabilities(&mut self, force: bool) -> Result<()> { pub async fn capabilities(&mut self, force: bool) -> Result<()> {
{ {
let caps = &*self.caps.read(); let caps = self.caps.read();
if caps.is_some() && !force { if caps.is_some() && !force {
return Ok(()); return Ok(());
} }
@ -134,13 +134,10 @@ where
.iter() .iter()
.find(|resp| matches!(resp, Response::Capabilities(_))) .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()); *caps = Some(new_caps.iter().cloned().collect());
} }
// if let Response::Capabilities(caps) = resp {
// debug!("capabilities: {:?}", caps);
// }
Ok(()) Ok(())
} }
@ -183,7 +180,7 @@ where
let cap = parse_capability(cap)?; let cap = parse_capability(cap)?;
self.capabilities(false).await?; self.capabilities(false).await?;
let caps = &*self.caps.read(); let caps = self.caps.read();
// TODO: refresh caps // TODO: refresh caps
let caps = caps.as_ref().unwrap(); let caps = caps.as_ref().unwrap();
@ -276,6 +273,7 @@ where
debug!("got a new line {:?}", next_line); debug!("got a new line {:?}", next_line);
let resp = parse_response(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() { if let Some(greeting) = greeting.take() {
let (greeting, waker) = &mut *greeting.write(); let (greeting, waker) = &mut *greeting.write();
debug!("received greeting!"); debug!("received greeting!");
@ -285,19 +283,31 @@ where
} }
} }
match &resp { // update capabilities list
// capabilities list // TODO: probably not really necessary here (done somewhere else)?
Response::Capabilities(new_caps) if let Response::Capabilities(new_caps)
| Response::Data { | Response::Data {
status: Status::Ok, status: Status::Ok,
code: Some(ResponseCode::Capabilities(new_caps)), code: Some(ResponseCode::Capabilities(new_caps)),
.. ..
} => { } = &resp
{
let caps = &mut *caps.write(); let caps = &mut *caps.write();
*caps = Some(new_caps.iter().cloned().collect()); *caps = Some(new_caps.iter().cloned().collect());
debug!("new caps: {:?}", caps); debug!("new caps: {:?}", caps);
} }
match &resp {
Response::Data {
status: Status::Ok, ..
} => {
let mut results = results.write();
if let Some((_, _, intermediate, _)) = results.iter_mut().next() {
debug!("pushed to intermediate: {:?}", resp);
intermediate.push(resp);
}
}
// bye // bye
Response::Data { Response::Data {
status: Status::Bye, status: Status::Bye,
@ -308,7 +318,7 @@ where
Response::Done { tag, .. } => { Response::Done { tag, .. } => {
if tag.starts_with(TAG_PREFIX) { if tag.starts_with(TAG_PREFIX) {
let id = tag.trim_start_matches(TAG_PREFIX).parse::<usize>()?; // let id = tag.trim_start_matches(TAG_PREFIX).parse::<usize>()?;
let mut results = results.write(); let mut results = results.write();
if let Some((_, opt, _, waker)) = results.iter_mut().next() { if let Some((_, opt, _, waker)) = results.iter_mut().next() {
*opt = Some(resp); *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::<Vec<_>>().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::<usize>()?;
// 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((_, _)) => { Either::Right((_, _)) => {

View file

@ -33,6 +33,7 @@
//! # } //! # }
//! ``` //! ```
pub mod auth;
mod inner; mod inner;
use std::sync::Arc; use std::sync::Arc;
@ -89,22 +90,18 @@ impl ClientConfig {
let inner = Client::new(conn, self); let inner = Client::new(conn, self);
inner.wait_for_greeting().await; inner.wait_for_greeting().await;
return Ok(ClientUnauthenticated::Encrypted( return Ok(ClientUnauthenticated::Encrypted(inner));
ClientUnauthenticatedEncrypted { inner },
));
} else { } else {
let inner = Client::new(conn, self); let inner = Client::new(conn, self);
inner.wait_for_greeting().await; inner.wait_for_greeting().await;
return Ok(ClientUnauthenticated::Unencrypted( return Ok(ClientUnauthenticated::Unencrypted(inner));
ClientUnauthenticatedUnencrypted { inner },
));
} }
} }
} }
pub enum ClientUnauthenticated { pub enum ClientUnauthenticated {
Encrypted(ClientUnauthenticatedEncrypted), Encrypted(Client<TlsStream<TcpStream>>),
Unencrypted(ClientUnauthenticatedUnencrypted), Unencrypted(Client<TcpStream>),
} }
impl ClientUnauthenticated { impl ClientUnauthenticated {
@ -113,44 +110,46 @@ impl ClientUnauthenticated {
// this is a no-op, we don't need to upgrade // this is a no-op, we don't need to upgrade
ClientUnauthenticated::Encrypted(_) => Ok(self), ClientUnauthenticated::Encrypted(_) => Ok(self),
ClientUnauthenticated::Unencrypted(e) => { ClientUnauthenticated::Unencrypted(e) => {
let client = ClientUnauthenticatedEncrypted { Ok(ClientUnauthenticated::Encrypted(e.upgrade().await?))
inner: e.inner.upgrade().await?,
};
Ok(ClientUnauthenticated::Encrypted(client))
} }
} }
} }
/// TODO: Exposing low-level execute , shoudl remove later /// Exposing low-level execute
pub async fn execute(&mut self, cmd: Command) -> Result<(Response, Vec<Response>)> { async fn execute(&mut self, cmd: Command) -> Result<(Response, Vec<Response>)> {
match self { match self {
ClientUnauthenticated::Encrypted(e) => e.inner.execute(cmd).await, ClientUnauthenticated::Encrypted(e) => e.execute(cmd).await,
ClientUnauthenticated::Unencrypted(e) => e.inner.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. /// 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<str>) -> Result<bool> { pub async fn has_capability(&mut self, cap: impl AsRef<str>) -> Result<bool> {
match self { match self {
ClientUnauthenticated::Encrypted(e) => e.inner.has_capability(cap).await, ClientUnauthenticated::Encrypted(e) => e.has_capability(cap).await,
ClientUnauthenticated::Unencrypted(e) => e.inner.has_capability(cap).await, ClientUnauthenticated::Unencrypted(e) => e.has_capability(cap).await,
} }
} }
} }
pub struct ClientUnauthenticatedUnencrypted { pub enum ClientAuthenticated {
/// Connection to the remote server Encrypted(Client<TlsStream<TcpStream>>),
inner: Client<TcpStream>, Unencrypted(Client<TcpStream>),
} }
impl ClientUnauthenticatedUnencrypted { impl ClientAuthenticated {
pub async fn upgrade(&self) {} /// Exposing low-level execute
async fn execute(&mut self, cmd: Command) -> Result<(Response, Vec<Response>)> {
match self {
ClientAuthenticated::Encrypted(e) => e.execute(cmd).await,
ClientAuthenticated::Unencrypted(e) => e.execute(cmd).await,
}
} }
/// An IMAP client that isn't authenticated. pub async fn list(&mut self) -> Result<()> {
pub struct ClientUnauthenticatedEncrypted { let cmd = Command::List { reference: "".to_owned(), mailbox: "*".to_owned() };
/// Connection to the remote server let resp = self.execute(cmd).await?;
inner: Client<TlsStream<TcpStream>>, debug!("list response: {:?}", resp);
Ok(())
}
} }
impl ClientUnauthenticatedEncrypted {}

View file

@ -6,14 +6,19 @@ pub enum Command {
Capability, Capability,
Starttls, Starttls,
Login { username: String, password: String }, Login { username: String, password: String },
Select { mailbox: String },
List { reference: String, mailbox: String },
} }
impl fmt::Display for Command { impl fmt::Display for Command {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use Command::*;
match self { match self {
Command::Capability => write!(f, "CAPABILITY"), Capability => write!(f, "CAPABILITY"),
Command::Starttls => write!(f, "STARTTLS"), Starttls => write!(f, "STARTTLS"),
Command::Login { username, password } => write!(f, "LOGIN {} {}", username, password), Login { username, password } => write!(f, "LOGIN {} {}", username, password),
Select { mailbox } => write!(f, "SELECT {}", mailbox),
List { reference, mailbox } => write!(f, "LIST {:?} {:?}", reference, mailbox),
} }
} }
} }

View file

@ -1,6 +1,8 @@
#[macro_use] #[macro_use]
extern crate anyhow; extern crate anyhow;
#[macro_use] #[macro_use]
extern crate async_trait;
#[macro_use]
extern crate derive_builder; extern crate derive_builder;
#[macro_use] #[macro_use]
extern crate futures; extern crate futures;

View file

@ -104,9 +104,6 @@ fn build_resp_code(pair: Pair<Rule>) -> Option<ResponseCode> {
unreachable!("{:#?}", pair); unreachable!("{:#?}", pair);
} }
// panic!("pair: {:#?}", pair);
debug!("pair: {:#?}", pair);
let mut pairs = pair.into_inner(); let mut pairs = pair.into_inner();
let pair = pairs.next()?; let pair = pairs.next()?;
Some(match pair.as_rule() { Some(match pair.as_rule() {
@ -155,7 +152,7 @@ fn build_status(pair: Pair<Rule>) -> Status {
} }
} }
fn build_flag_list(pair: Pair<Rule>) -> Vec<Flag> { fn build_flag_list(pair: Pair<Rule>) -> Vec<MailboxFlag> {
if !matches!(pair.as_rule(), Rule::flag_list) { if !matches!(pair.as_rule(), Rule::flag_list) {
unreachable!("{:#?}", pair); unreachable!("{:#?}", pair);
} }
@ -163,18 +160,18 @@ fn build_flag_list(pair: Pair<Rule>) -> Vec<Flag> {
pair.into_inner().map(build_flag).collect() pair.into_inner().map(build_flag).collect()
} }
fn build_flag(pair: Pair<Rule>) -> Flag { fn build_flag(pair: Pair<Rule>) -> MailboxFlag {
if !matches!(pair.as_rule(), Rule::flag) { if !matches!(pair.as_rule(), Rule::flag) {
unreachable!("{:#?}", pair); unreachable!("{:#?}", pair);
} }
match pair.as_str() { match pair.as_str() {
"\\Answered" => Flag::Answered, "\\Answered" => MailboxFlag::Answered,
"\\Flagged" => Flag::Flagged, "\\Flagged" => MailboxFlag::Flagged,
"\\Deleted" => Flag::Deleted, "\\Deleted" => MailboxFlag::Deleted,
"\\Seen" => Flag::Seen, "\\Seen" => MailboxFlag::Seen,
"\\Draft" => Flag::Draft, "\\Draft" => MailboxFlag::Draft,
s if s.starts_with("\\") => Flag::Ext(s.to_owned()), s if s.starts_with("\\") => MailboxFlag::Ext(s.to_owned()),
_ => unreachable!("{:#?}", pair.as_str()), _ => unreachable!("{:#?}", pair.as_str()),
} }
} }
@ -195,10 +192,28 @@ fn build_mailbox_data(pair: Pair<Rule>) -> MailboxData {
MailboxData::Flags(flags) MailboxData::Flags(flags)
} }
Rule::mailbox_data_recent => MailboxData::Recent(build_number(pair)), 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), _ => unreachable!("{:#?}", pair),
} }
} }
fn build_mailbox_list(pair: Pair<Rule>) -> (Vec<String>, Option<String>, String) {
todo!()
}
fn build_mbx_list_flags(pair: Pair<Rule>) -> Vec<String> {
if !matches!(pair.as_rule(), Rule::mbx_list_flags) {
unreachable!("{:#?}", pair);
}
todo!()
}
fn build_number<T>(pair: Pair<Rule>) -> T fn build_number<T>(pair: Pair<Rule>) -> T
where where
T: FromStr, T: FromStr,
@ -266,11 +281,11 @@ mod tests {
assert_eq!( assert_eq!(
parse_response("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n"), parse_response("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n"),
Ok(Response::MailboxData(MailboxData::Flags(vec![ Ok(Response::MailboxData(MailboxData::Flags(vec![
Flag::Answered, MailboxFlag::Answered,
Flag::Flagged, MailboxFlag::Flagged,
Flag::Deleted, MailboxFlag::Deleted,
Flag::Seen, MailboxFlag::Seen,
Flag::Draft, MailboxFlag::Draft,
]))) ])))
); );

View file

@ -64,11 +64,13 @@ header_list = { "(" ~ header_fld_name ~ (sp ~ header_fld_name)* ~ ")" }
list_wildcards = @{ "%" | "*" } list_wildcards = @{ "%" | "*" }
literal = @{ "{" ~ number ~ "}" ~ crlf ~ char8* } literal = @{ "{" ~ number ~ "}" ~ crlf ~ char8* }
mailbox = { ^"INBOX" | astring } 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_exists = { number ~ sp ~ ^"EXISTS" }
mailbox_data_flags = { ^"FLAGS" ~ sp ~ flag_list } mailbox_data_flags = { ^"FLAGS" ~ sp ~ flag_list }
mailbox_data_recent = { number ~ sp ~ ^"RECENT" } 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_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_oflag = { "\\NoInferiors" | flag_extension }
mbx_list_sflag = { "\\NoSelect" | "\\Marked" | "\\Unmarked" } mbx_list_sflag = { "\\NoSelect" | "\\Marked" | "\\Unmarked" }

View file

@ -65,7 +65,7 @@ pub enum AttributeValue {}
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum MailboxData { pub enum MailboxData {
Exists(u32), Exists(u32),
Flags(Vec<Flag>), Flags(Vec<MailboxFlag>),
List { List {
flags: Vec<String>, flags: Vec<String>,
delimiter: Option<String>, delimiter: Option<String>,
@ -88,7 +88,7 @@ pub enum MailboxData {
} }
#[derive(Debug, Eq, PartialEq, Clone)] #[derive(Debug, Eq, PartialEq, Clone)]
pub enum Flag { pub enum MailboxFlag {
Answered, Answered,
Flagged, Flagged,
Deleted, Deleted,

View file

@ -3,7 +3,7 @@
use anyhow::Result; use anyhow::Result;
use futures::{future::FutureExt, stream::StreamExt}; use futures::{future::FutureExt, stream::StreamExt};
use panorama_imap::{ use panorama_imap::{
client::{ClientBuilder, ClientConfig}, client::{ClientBuilder, ClientConfig, auth::{self, Auth}},
command::Command as ImapCommand, command::Command as ImapCommand,
}; };
use tokio::{sync::mpsc::UnboundedReceiver, task::JoinHandle}; 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); debug!("connecting to {}:{}", &acct.imap.server, acct.imap.port);
let unauth = builder.open().await?; 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"); debug!("attempting to upgrade");
let client = unauth.upgrade().await?; let client = unauth.upgrade().await?;
debug!("upgrade successful"); debug!("upgrade successful");
@ -89,18 +89,21 @@ async fn imap_main(acct: MailAccountConfig) -> Result<()> {
debug!("preparing to auth"); debug!("preparing to auth");
// check if the authentication method is supported // check if the authentication method is supported
let authed = match acct.imap.auth { let mut authed = match acct.imap.auth {
ImapAuth::Plain { username, password } => { ImapAuth::Plain { username, password } => {
let ok = unauth.has_capability("AUTH=PLAIN").await?; let auth = auth::Plain {username, password};
let res = unauth.execute(ImapCommand::Login { username, password }).await?; auth.perform_auth(unauth).await?
debug!("res: {:?}", res);
} }
}; };
debug!("authentication successful!");
// debug!("sending CAPABILITY"); // debug!("sending CAPABILITY");
// let result = unauth.capabilities().await?; // let result = unauth.capabilities().await?;
loop { loop {
debug!("listing all emails...");
authed.list().await?;
tokio::time::sleep(std::time::Duration::from_secs(60)).await; tokio::time::sleep(std::time::Duration::from_secs(60)).await;
debug!("heartbeat"); debug!("heartbeat");
} }

View file

@ -22,6 +22,8 @@ struct Opt {
#[tokio::main(flavor = "multi_thread")] #[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let now = chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]");
// parse command line arguments into options struct // parse command line arguments into options struct
let opt = Opt::from_args(); let opt = Opt::from_args();
@ -33,8 +35,8 @@ async fn main() -> Result<()> {
let mut logger = fern::Dispatch::new() let mut logger = fern::Dispatch::new()
.format(move |out, message, record| { .format(move |out, message, record| {
out.finish(format_args!( out.finish(format_args!(
"[{}][{}] {}", "{}[{}][{}] {}",
// chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), now,
record.target(), record.target(),
colors.color(record.level()), colors.color(record.level()),
message message