diff --git a/src/mail/client.rs b/src/mail/client.rs index 3811f09..eef9288 100644 --- a/src/mail/client.rs +++ b/src/mail/client.rs @@ -67,6 +67,10 @@ pub async fn sync_main( debug!("authentication successful!"); let folder_list = authed.list().await?; + let _ = mail2ui_tx.send(MailEvent::FolderList( + acct_name.clone(), + folder_list.clone(), + )); debug!("mailbox list: {:?}", folder_list); for folder in folder_list.iter() { @@ -100,7 +104,6 @@ pub async fn sync_main( } } - let _ = mail2ui_tx.send(MailEvent::FolderList(acct_name.clone(), folder_list)); tokio::time::sleep(std::time::Duration::from_secs(50)).await; // TODO: remove this later diff --git a/src/mail/mod.rs b/src/mail/mod.rs index edce37b..fe1fad5 100644 --- a/src/mail/mod.rs +++ b/src/mail/mod.rs @@ -3,7 +3,7 @@ mod client; mod event; mod metadata; -mod store; +pub mod store; use anyhow::Result; use futures::{ diff --git a/src/mail/store.rs b/src/mail/store.rs index 8ed9ca6..c1c75c0 100644 --- a/src/mail/store.rs +++ b/src/mail/store.rs @@ -5,7 +5,11 @@ use std::mem; use std::path::{Path, PathBuf}; use std::sync::Arc; -use anyhow::{Context, Result}; +use anyhow::{Context, Error, Result}; +use futures::{ + future::{self, FutureExt, TryFutureExt}, + stream::{StreamExt, TryStreamExt}, +}; use panorama_imap::response::AttributeValue; use sha2::{Digest, Sha256}; use sqlx::{ @@ -21,7 +25,7 @@ use tokio::{ use crate::config::{Config, ConfigWatcher}; -use super::MailEvent; +use super::{EmailMetadata, MailEvent}; static MIGRATOR: Migrator = sqlx::migrate!(); @@ -44,6 +48,7 @@ struct MailStoreInner { } #[derive(Clone, Debug)] +/// Probably an event about new emails? i forgot pub struct EmailUpdateInfo {} impl MailStore { @@ -52,79 +57,44 @@ impl MailStore { let config = Arc::new(RwLock::new(None)); let config2 = config.clone(); + let inner = Arc::new(RwLock::new(None)); + let inner2 = inner.clone(); + let listener = async move { while let Ok(()) = config_watcher.changed().await { let new_config = config_watcher.borrow().clone(); - let mut write = config2.write().await; - - // drop old config - if let Some(old_config) = write.take() { - mem::drop(old_config); + let fut = future::try_join( + async { + let mut write = config2.write().await; + write.replace(new_config.clone()); + Ok::<_, Error>(()) + }, + async { + let new_inner = + MailStoreInner::init_with_config(new_config.clone()).await?; + let mut write = inner2.write().await; + write.replace(new_inner); + Ok(()) + }, + ); + match fut.await { + Ok(_) => {} + Err(e) => { + error!("during mail loop: {}", e); + panic!(); + } } - - *write = Some(new_config); } }; let handle = tokio::spawn(listener); MailStore { config, - inner: Arc::new(RwLock::new(None)), + inner, handle: Arc::new(handle), } } - async fn init_with_config(&self, config: Config) -> Result<()> { - let data_dir = config.data_dir.to_string_lossy(); - let data_dir = PathBuf::from(shellexpand::tilde(data_dir.as_ref()).as_ref()); - - let mail_dir = data_dir.join("mail"); - if !mail_dir.exists() { - fs::create_dir_all(&mail_dir).await?; - } - info!("using mail dir: {:?}", mail_dir); - - // create database parent - let db_path = data_dir.join("panorama.db"); - let db_parent = db_path.parent(); - if let Some(path) = db_parent { - fs::create_dir_all(path).await?; - } - - let db_path_str = db_path.to_string_lossy(); - let db_path = format!("sqlite:{}", db_path_str); - info!("using database path: {}", db_path_str); - - // create the database file if it doesn't already exist -_ - - if !Sqlite::database_exists(&db_path_str).await? { - Sqlite::create_database(&db_path_str).await?; - } - - let pool = SqlitePool::connect(&db_path_str).await?; - MIGRATOR.run(&pool).await?; - debug!("run migrations : {:?}", MIGRATOR); - - let accounts = config - .mail_accounts - .keys() - .map(|acct| { - let folders = RwLock::new(Vec::new()); - (acct.to_owned(), Arc::new(AccountRef { folders })) - }) - .collect(); - - // let (new_email_tx, new_email_rx) = broadcast::channel(100); - { - let mut write = self.inner.write().await; - *write = Some(MailStoreInner { - mail_dir, - pool, - accounts, - }); - } - Ok(()) - } - /// Given a UID and optional message-id try to identify a particular message pub async fn try_identify_email( &self, @@ -158,12 +128,6 @@ impl MailStore { if let Some(existing) = existing { let rowid = existing.0; - debug!( - "folder: {:?} uid: {:?} rowid: {:?}", - folder.as_ref(), - uid, - rowid, - ); return Ok(Some(rowid)); } @@ -282,8 +246,21 @@ impl MailStore { } /// Event handerl - pub fn handle_mail_event(&self, evt: MailEvent) { + pub async fn handle_mail_event(&self, evt: MailEvent) -> Result<()> { debug!("TODO: handle {:?}", evt); + match evt { + MailEvent::FolderList(acct, folders) => { + let inner = self.inner.write().await; + let acct_ref = match inner.as_ref().and_then(|inner| inner.accounts.get(&acct)) { + Some(inner) => inner.clone(), + None => return Ok(()), + }; + mem::drop(inner); + acct_ref.set_folders(folders).await; + } + _ => {} + } + Ok(()) } /// Return a map of the accounts that are currently being tracked as well as a reference to the @@ -299,15 +276,103 @@ impl MailStore { } } +impl MailStoreInner { + async fn init_with_config(config: Config) -> Result { + let data_dir = config.data_dir.to_string_lossy(); + let data_dir = PathBuf::from(shellexpand::tilde(data_dir.as_ref()).as_ref()); + + let mail_dir = data_dir.join("mail"); + if !mail_dir.exists() { + fs::create_dir_all(&mail_dir).await?; + } + info!("using mail dir: {:?}", mail_dir); + + // create database parent + let db_path = data_dir.join("panorama.db"); + let db_parent = db_path.parent(); + if let Some(path) = db_parent { + fs::create_dir_all(path).await?; + } + + let db_path_str = db_path.to_string_lossy(); + let db_path = format!("sqlite:{}", db_path_str); + info!("using database path: {}", db_path_str); + + // create the database file if it doesn't already exist -_ - + if !Sqlite::database_exists(&db_path_str).await? { + Sqlite::create_database(&db_path_str).await?; + } + + let pool = SqlitePool::connect(&db_path_str).await?; + MIGRATOR.run(&pool).await?; + debug!("run migrations : {:?}", MIGRATOR); + + let accounts = config + .mail_accounts + .keys() + .map(|acct| { + let folders = RwLock::new(Vec::new()); + ( + acct.to_owned(), + Arc::new(AccountRef { + folders, + pool: pool.clone(), + }), + ) + }) + .collect(); + + Ok(MailStoreInner { + mail_dir, + pool, + accounts, + }) + } +} + #[derive(Debug)] +/// Holds a reference to an account pub struct AccountRef { folders: RwLock>, + pool: SqlitePool, } impl AccountRef { - pub async fn folders(&self) -> Vec { + /// Gets the folders on this account + pub async fn get_folders(&self) -> Vec { self.folders.read().await.clone() } + + /// Sets the folders on this account + pub async fn set_folders(&self, folders: Vec) { + *self.folders.write().await = folders; + } + + /// Gets the n latest messages in the given folder + pub async fn get_newest_n_messages( + &self, + folder: impl AsRef, + n: usize, + ) -> Result> { + let folder = folder.as_ref(); + let messages: Vec = sqlx::query_as( + r#" + SELECT internaldate, subject FROM mail + WHERE folder = ? + ORDER BY internaldate DESC + "#, + ) + .bind(folder) + .fetch(&self.pool) + .map_ok(|(date, subject): (String, String)| EmailMetadata { + subject, + ..EmailMetadata::default() + }) + .try_collect() + .await?; + debug!("found {} messages", messages.len()); + Ok(messages) + } } fn into_opt(res: Result) -> Result> { diff --git a/src/ui/mail_view.rs b/src/ui/mail_view.rs index 58f366d..0235bea 100644 --- a/src/ui/mail_view.rs +++ b/src/ui/mail_view.rs @@ -19,14 +19,18 @@ use panorama_tui::{ widgets::*, }, }; +use tokio::task::JoinHandle; -use crate::mail::EmailMetadata; +use crate::mail::{store::AccountRef, EmailMetadata}; use super::{FrameType, HandlesInput, InputResult, MailStore, TermType, Window, UI}; #[derive(Debug)] +/// A singular UI view of a list of mail pub struct MailView { pub mail_store: MailStore, + pub current_account: Option>, + pub current_folder: Option, pub message_list: TableState, pub selected: Arc, pub change: Arc, @@ -68,8 +72,7 @@ impl Window for MailView { // folder list let mut items = vec![]; for (acct_name, acct_ref) in accts.iter() { - let folders = acct_ref.folders().await; - + let folders = acct_ref.get_folders().await; items.push(ListItem::new(acct_name.to_owned())); for folder in folders { items.push(ListItem::new(format!(" {}", folder))); @@ -86,29 +89,32 @@ impl Window for MailView { .highlight_symbol(">>"); let mut rows = vec![]; - // for acct in accts.iter() { - // // TODO: messages - // let result: Option> = None; // self.mail_store.messages_of(acct); - // if let Some(messages) = result { - // for meta in messages { - // let mut row = Row::new(vec![ - // String::from(if meta.unread { "\u{2b24}" } else { "" }), - // meta.uid.map(|u| u.to_string()).unwrap_or_default(), - // meta.date.map(|d| humanize_timestamp(d)).unwrap_or_default(), - // meta.from.clone(), - // meta.subject.clone(), - // ]); - // if meta.unread { - // row = row.style( - // Style::default() - // .fg(Color::LightCyan) - // .add_modifier(Modifier::BOLD), - // ); - // } - // rows.push(row); - // } - // } - // } + if let Some(acct_ref) = self.current_account.as_ref() { + let messages = acct_ref.get_newest_n_messages("INBOX", chunks[1].height as usize); + } + + for (acct_name, acct_ref) in accts.iter() { + let result: Option> = None; // self.mail_store.messages_of(acct); + if let Some(messages) = result { + for meta in messages { + let mut row = Row::new(vec![ + String::from(if meta.unread { "\u{2b24}" } else { "" }), + meta.uid.map(|u| u.to_string()).unwrap_or_default(), + meta.date.map(|d| humanize_timestamp(d)).unwrap_or_default(), + meta.from.clone(), + meta.subject.clone(), + ]); + if meta.unread { + row = row.style( + Style::default() + .fg(Color::LightCyan) + .add_modifier(Modifier::BOLD), + ); + } + rows.push(row); + } + } + } let table = Table::new(rows) .style(Style::default().fg(Color::White)) @@ -128,6 +134,24 @@ impl Window for MailView { f.render_widget(dirlist, chunks[0]); f.render_widget(table, chunks[1]); } + + async fn update(&mut self) { + // make the change + if self + .change + .compare_exchange(-1, 0, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + self.move_up(); + } + if self + .change + .compare_exchange(1, 0, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + self.move_down(); + } + } } /// Turn a timestamp into a format that a human might read when viewing it in a table. @@ -152,12 +176,22 @@ impl MailView { pub fn new(mail_store: MailStore) -> Self { MailView { mail_store, + current_account: None, + current_folder: None, message_list: TableState::default(), selected: Arc::new(AtomicU32::default()), change: Arc::new(AtomicI8::default()), } } + pub async fn set_current_account(&mut self, name: impl AsRef) { + let name = name.as_ref(); + let accounts = self.mail_store.list_accounts().await; + if let Some(acct_ref) = accounts.get(name) { + self.current_account = Some(acct_ref.clone()); + } + } + pub fn move_down(&mut self) { // if self.message_uids.is_empty() { // return; @@ -186,21 +220,4 @@ impl MailView { // } } - pub fn update(&mut self) { - // make the change - if self - .change - .compare_exchange(-1, 0, Ordering::Relaxed, Ordering::Relaxed) - .is_ok() - { - self.move_up(); - } - if self - .change - .compare_exchange(1, 0, Ordering::Relaxed, Ordering::Relaxed) - .is_ok() - { - self.move_down(); - } - } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d341892..b1689c8 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -109,7 +109,7 @@ pub async fn run_ui2(params: UiParams) -> Result<()> { select! { // got an event from the mail thread evt = mail2ui_rx.recv().fuse() => if let Some(evt) = evt { - ui.process_mail_event(evt); + ui.process_mail_event(evt).await?; }, // got an event from the ui thread @@ -229,7 +229,8 @@ impl UI { Ok(()) } - fn process_mail_event(&mut self, evt: MailEvent) { - self.mail_store.handle_mail_event(evt); + async fn process_mail_event(&mut self, evt: MailEvent) -> Result<()> { + self.mail_store.handle_mail_event(evt).await?; + Ok(()) } } diff --git a/src/ui/windows.rs b/src/ui/windows.rs index 00b0b44..00d8041 100644 --- a/src/ui/windows.rs +++ b/src/ui/windows.rs @@ -16,7 +16,7 @@ pub trait Window: HandlesInput { // async fn draw(&self, f: FrameType, area: Rect, ui: Rc); /// Update function - fn update(&mut self) {} + async fn update(&mut self) {} } downcast_rs::impl_downcast!(Window);