diff --git a/imap/src/lib.rs b/imap/src/lib.rs index 2697a91..34ee11f 100644 --- a/imap/src/lib.rs +++ b/imap/src/lib.rs @@ -1,10 +1,10 @@ //! Panorama/IMAP //! === //! -//! This is a library that implements the IMAP protocol according to RFC 3501 and several +//! This is a library that implements parts of the IMAP protocol according to RFC 3501 and several //! extensions. Although its primary purpose is to be used in panorama, it should be usable for -//! general-purpose IMAP usage. See the [client][crate::client] module for more information on how -//! to get started with a client quickly. +//! general-purpose IMAP client development. See the [client][crate::client] module for more +//! information on how to get started with a client quickly. //! //! RFCs: //! diff --git a/notes.md b/notes.md index f9d4f5c..92d2d2b 100644 --- a/notes.md +++ b/notes.md @@ -36,7 +36,4 @@ imap routine list of shit to do --- -- [x] starttls impl -- [ ] auth impl - - [ ] auth plain impl -- [ ] fetch impl +- profile the client to see what needs to be improved? diff --git a/src/config.rs b/src/config.rs index c24a8ce..cc0dbd0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,7 @@ //! One of the primary goals of panorama is to be able to always hot-reload configuration files. use std::fs::{self, File}; +use std::collections::HashMap; use std::io::Read; use std::path::{Path, PathBuf}; @@ -26,7 +27,7 @@ pub struct Config { /// Mail accounts #[serde(rename = "mail")] - pub mail_accounts: Vec, + pub mail_accounts: HashMap, } /// Configuration for a single mail account diff --git a/src/mail/client.rs b/src/mail/client.rs new file mode 100644 index 0000000..7e370d0 --- /dev/null +++ b/src/mail/client.rs @@ -0,0 +1,147 @@ +use anyhow::Result; +use futures::{ + future::FutureExt, + stream::{Stream, StreamExt}, +}; +use notify_rust::{Notification, Timeout}; +use panorama_imap::{ + client::{ + auth::{self, Auth}, + ClientBuilder, ClientConfig, + }, + command::Command as ImapCommand, + response::{AttributeValue, Envelope, MailboxData, Response}, +}; +use tokio::{ + sync::mpsc::{UnboundedReceiver, UnboundedSender}, + task::JoinHandle, +}; + +use crate::config::{Config, ConfigWatcher, ImapAuth, MailAccountConfig, TlsMethod}; + +use super::{MailCommand, MailEvent}; + +/// The main sequence of steps for the IMAP thread to follow +pub async fn imap_main( + acct: MailAccountConfig, + mail2ui_tx: UnboundedSender, +) -> Result<()> { + // loop ensures that the connection is retried after it dies + loop { + let builder: ClientConfig = ClientBuilder::default() + .hostname(acct.imap.server.clone()) + .port(acct.imap.port) + .tls(matches!(acct.imap.tls, TlsMethod::On)) + .build() + .map_err(|err| anyhow!("err: {}", err))?; + + debug!("connecting to {}:{}", &acct.imap.server, acct.imap.port); + let unauth = builder.open().await?; + + let unauth = if matches!(acct.imap.tls, TlsMethod::Starttls) { + debug!("attempting to upgrade"); + let client = unauth.upgrade().await?; + debug!("upgrade successful"); + client + } else { + unauth + }; + + debug!("preparing to auth"); + // check if the authentication method is supported + let mut authed = match &acct.imap.auth { + ImapAuth::Plain { username, password } => { + let auth = auth::Plain { + username: username.clone(), + password: password.clone(), + }; + auth.perform_auth(unauth).await? + } + }; + + debug!("authentication successful!"); + + // let's just select INBOX for now, maybe have a config for default mailbox later? + debug!("selecting the INBOX mailbox"); + authed.select("INBOX").await?; + + loop { + let folder_list = authed.list().await?; + debug!("mailbox list: {:?}", folder_list); + let _ = mail2ui_tx.send(MailEvent::FolderList(folder_list)); + + let message_uids = authed.uid_search().await?; + let message_uids = message_uids.into_iter().take(30).collect::>(); + let _ = mail2ui_tx.send(MailEvent::MessageUids(message_uids.clone())); + + // TODO: make this happen concurrently with the main loop? + let mut message_list = authed.uid_fetch(&message_uids).await.unwrap(); + while let Some((uid, attrs)) = message_list.next().await { + let evt = MailEvent::UpdateUid(uid, attrs); + debug!("sent {:?}", evt); + mail2ui_tx.send(evt); + } + + // check if IDLE is supported + let supports_idle = authed.has_capability("IDLE").await?; + if supports_idle { + let mut idle_stream = authed.idle().await?; + + loop { + let evt = match idle_stream.next().await { + Some(v) => v, + None => break, + }; + debug!("got an event: {:?}", evt); + + match evt { + Response::MailboxData(MailboxData::Exists(uid)) => { + debug!("NEW MESSAGE WITH UID {:?}, droping everything", uid); + // send DONE to stop the idle + std::mem::drop(idle_stream); + + let handle = Notification::new() + .summary("New Email") + .body("holy Shit,") + .icon("firefox") + .timeout(Timeout::Milliseconds(6000)) + .show()?; + + let message_uids = authed.uid_search().await?; + let message_uids = + message_uids.into_iter().take(20).collect::>(); + let _ = mail2ui_tx.send(MailEvent::MessageUids(message_uids.clone())); + + // TODO: make this happen concurrently with the main loop? + let mut message_list = authed.uid_fetch(&message_uids).await.unwrap(); + while let Some((uid, attrs)) = message_list.next().await { + let evt = MailEvent::UpdateUid(uid, attrs); + debug!("sent {:?}", evt); + mail2ui_tx.send(evt); + } + + idle_stream = authed.idle().await?; + } + _ => {} + } + } + } else { + loop { + tokio::time::sleep(std::time::Duration::from_secs(20)).await; + debug!("heartbeat"); + } + } + + if false { + break; + } + } + + // wait a bit so we're not hitting the server really fast if the fail happens + // early on + // + // TODO: some kind of smart exponential backoff that considers some time + // threshold to be a failing case? + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } +} diff --git a/src/mail/mod.rs b/src/mail/mod.rs index 37ab10c..95b338a 100644 --- a/src/mail/mod.rs +++ b/src/mail/mod.rs @@ -1,5 +1,6 @@ //! Mail +mod client; mod metadata; use anyhow::Result; @@ -82,14 +83,14 @@ pub async fn run_mail( conn.abort(); } - for acct in config.mail_accounts.into_iter() { + for (acct_name, acct) in config.mail_accounts.into_iter() { let mail2ui_tx = mail2ui_tx.clone(); let handle = tokio::spawn(async move { // debug!("opening imap connection for {:?}", acct); // this loop is to make sure accounts are restarted on error loop { - match imap_main(acct.clone(), mail2ui_tx.clone()).await { + match client::imap_main(acct.clone(), mail2ui_tx.clone()).await { Ok(_) => {} Err(err) => { error!("IMAP Error: {}", err); @@ -112,125 +113,3 @@ pub async fn run_mail( Ok(()) } - -/// The main sequence of steps for the IMAP thread to follow -async fn imap_main(acct: MailAccountConfig, mail2ui_tx: UnboundedSender) -> Result<()> { - // loop ensures that the connection is retried after it dies - loop { - let builder: ClientConfig = ClientBuilder::default() - .hostname(acct.imap.server.clone()) - .port(acct.imap.port) - .tls(matches!(acct.imap.tls, TlsMethod::On)) - .build() - .map_err(|err| anyhow!("err: {}", err))?; - - debug!("connecting to {}:{}", &acct.imap.server, acct.imap.port); - let unauth = builder.open().await?; - - let unauth = if matches!(acct.imap.tls, TlsMethod::Starttls) { - debug!("attempting to upgrade"); - let client = unauth.upgrade().await?; - debug!("upgrade successful"); - client - } else { - unauth - }; - - debug!("preparing to auth"); - // check if the authentication method is supported - let mut authed = match &acct.imap.auth { - ImapAuth::Plain { username, password } => { - let auth = auth::Plain { - username: username.clone(), - password: password.clone(), - }; - auth.perform_auth(unauth).await? - } - }; - - debug!("authentication successful!"); - - // let's just select INBOX for now, maybe have a config for default mailbox later? - debug!("selecting the INBOX mailbox"); - authed.select("INBOX").await?; - - loop { - let folder_list = authed.list().await?; - debug!("mailbox list: {:?}", folder_list); - let _ = mail2ui_tx.send(MailEvent::FolderList(folder_list)); - - let message_uids = authed.uid_search().await?; - let message_uids = message_uids.into_iter().take(30).collect::>(); - let _ = mail2ui_tx.send(MailEvent::MessageUids(message_uids.clone())); - - // TODO: make this happen concurrently with the main loop? - let mut message_list = authed.uid_fetch(&message_uids).await.unwrap(); - while let Some((uid, attrs)) = message_list.next().await { - let evt = MailEvent::UpdateUid(uid, attrs); - debug!("sent {:?}", evt); - mail2ui_tx.send(evt); - } - - // check if IDLE is supported - let supports_idle = authed.has_capability("IDLE").await?; - if supports_idle { - let mut idle_stream = authed.idle().await?; - - loop { - let evt = match idle_stream.next().await { - Some(v) => v, - None => break, - }; - debug!("got an event: {:?}", evt); - - match evt { - Response::MailboxData(MailboxData::Exists(uid)) => { - debug!("NEW MESSAGE WITH UID {:?}, droping everything", uid); - // send DONE to stop the idle - std::mem::drop(idle_stream); - - let handle = Notification::new() - .summary("New Email") - .body("holy Shit,") - .icon("firefox") - .timeout(Timeout::Milliseconds(6000)) - .show()?; - - let message_uids = authed.uid_search().await?; - let message_uids = - message_uids.into_iter().take(20).collect::>(); - let _ = mail2ui_tx.send(MailEvent::MessageUids(message_uids.clone())); - - // TODO: make this happen concurrently with the main loop? - let mut message_list = authed.uid_fetch(&message_uids).await.unwrap(); - while let Some((uid, attrs)) = message_list.next().await { - let evt = MailEvent::UpdateUid(uid, attrs); - debug!("sent {:?}", evt); - mail2ui_tx.send(evt); - } - - idle_stream = authed.idle().await?; - } - _ => {} - } - } - } else { - loop { - tokio::time::sleep(std::time::Duration::from_secs(20)).await; - debug!("heartbeat"); - } - } - - if false { - break; - } - } - - // wait a bit so we're not hitting the server really fast if the fail happens - // early on - // - // TODO: some kind of smart exponential backoff that considers some time - // threshold to be a failing case? - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - } -} diff --git a/src/main.rs b/src/main.rs index 8e76558..b44f23b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use std::thread; use anyhow::Result; use fern::colors::{Color, ColoredLevelConfig}; -use futures::future::{FutureExt, TryFutureExt}; +use futures::future::{TryFutureExt}; use panorama::{ config::spawn_config_watcher_system, mail::{self, MailEvent}, @@ -54,13 +54,13 @@ async fn run(opt: Opt) -> Result<()> { let (exit_tx, mut exit_rx) = mpsc::channel::<()>(1); // send messages from the UI thread to the mail thread - let (ui2mail_tx, ui2mail_rx) = mpsc::unbounded_channel(); + let (_ui2mail_tx, ui2mail_rx) = mpsc::unbounded_channel(); // send messages from the mail thread to the UI thread let (mail2ui_tx, mail2ui_rx) = mpsc::unbounded_channel(); // send messages from the UI thread to the vm thread - let (ui2vm_tx, ui2vm_rx) = mpsc::unbounded_channel(); + let (ui2vm_tx, _ui2vm_rx) = mpsc::unbounded_channel(); tokio::spawn(async move { let config_update = config_update.clone(); @@ -86,7 +86,7 @@ async fn run(opt: Opt) -> Result<()> { fn run_ui( exit_tx: mpsc::Sender<()>, mail2ui_rx: mpsc::UnboundedReceiver, - ui2vm_tx: mpsc::UnboundedSender<()>, + _ui2vm_tx: mpsc::UnboundedSender<()>, ) { let stdout = std::io::stdout(); diff --git a/src/ui/mail_view.rs b/src/ui/mail_view.rs index cfc6488..38b520a 100644 --- a/src/ui/mail_view.rs +++ b/src/ui/mail_view.rs @@ -4,9 +4,11 @@ use std::sync::{ Arc, }; +use anyhow::Result; use chrono::{DateTime, Datelike, Duration, Local}; use chrono_humanize::HumanTime; use panorama_imap::response::Envelope; +use crossterm::event::{KeyEvent, KeyCode}; use tui::{ buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, @@ -17,7 +19,7 @@ use tui::{ use crate::mail::EmailMetadata; -use super::{FrameType, HandlesInput, Window, UI}; +use super::{FrameType, HandlesInput, TermType, InputResult, Window, UI}; #[derive(Default, Debug)] pub struct MailView { @@ -29,7 +31,23 @@ pub struct MailView { pub change: Arc, } -impl HandlesInput for MailView {} +impl HandlesInput for MailView { + fn handle_key(&mut self, term: TermType, evt: KeyEvent) -> Result { + let KeyEvent { code, .. } = evt; + match code { + // KeyCode::Char('q') => self.0.store(true, Ordering::Relaxed), + // KeyCode::Char('j') => self.1.store(1, Ordering::Relaxed), + // KeyCode::Char('k') => self.1.store(-1, Ordering::Relaxed), + KeyCode::Char(':') => { + // let colon_prompt = Box::new(ColonPrompt::init(term)); + // return Ok(InputResult::Push(colon_prompt)); + } + _ => {} + } + + Ok(InputResult::Ok) + } +} impl Window for MailView { fn name(&self) -> String { @@ -87,7 +105,6 @@ impl Window for MailView { row }) .collect::>(); - let table = Table::new(rows) .style(Style::default().fg(Color::White)) .widths(&[ @@ -108,6 +125,11 @@ impl Window for MailView { } } +/// Turn a timestamp into a format that a human might read when viewing it in a table. +/// +/// This means, for dates within the past 24 hours, report it in a relative format. +/// +/// For dates sent this year, omit the year entirely. fn humanize_timestamp(date: DateTime) -> String { let now = Local::now(); let diff = now - date;