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 = [
"anyhow",
"assert_matches",
"async-trait",
"derive_builder",
"futures",
"log",

View file

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

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
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::<usize>()?;
// let id = tag.trim_start_matches(TAG_PREFIX).parse::<usize>()?;
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::<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((_, _)) => {

View file

@ -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<TlsStream<TcpStream>>),
Unencrypted(Client<TcpStream>),
}
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<Response>)> {
/// Exposing low-level execute
async fn execute(&mut self, cmd: Command) -> Result<(Response, Vec<Response>)> {
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<str>) -> Result<bool> {
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<TcpStream>,
pub enum ClientAuthenticated {
Encrypted(Client<TlsStream<TcpStream>>),
Unencrypted(Client<TcpStream>),
}
impl ClientUnauthenticatedUnencrypted {
pub async fn upgrade(&self) {}
}
impl ClientAuthenticated {
/// 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 struct ClientUnauthenticatedEncrypted {
/// Connection to the remote server
inner: Client<TlsStream<TcpStream>>,
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 {}

View file

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

View file

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

View file

@ -104,9 +104,6 @@ fn build_resp_code(pair: Pair<Rule>) -> Option<ResponseCode> {
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<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) {
unreachable!("{:#?}", pair);
}
@ -163,18 +160,18 @@ fn build_flag_list(pair: Pair<Rule>) -> Vec<Flag> {
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) {
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<Rule>) -> 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<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
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,
])))
);

View file

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

View file

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

View file

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

View file

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