message list is now fetched from the database

This commit is contained in:
Michael Zhang 2021-03-26 22:55:51 -05:00
parent 22f11544e0
commit 1bc6776615
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
6 changed files with 204 additions and 118 deletions

View file

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

View file

@ -3,7 +3,7 @@
mod client;
mod event;
mod metadata;
mod store;
pub mod store;
use anyhow::Result;
use futures::{

View file

@ -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 fut = future::try_join(
async {
let mut write = config2.write().await;
// drop old config
if let Some(old_config) = write.take() {
mem::drop(old_config);
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<Self> {
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<Vec<String>>,
pool: SqlitePool,
}
impl AccountRef {
pub async fn folders(&self) -> Vec<String> {
/// Gets the folders on this account
pub async fn get_folders(&self) -> Vec<String> {
self.folders.read().await.clone()
}
/// Sets the folders on this account
pub async fn set_folders(&self, folders: Vec<String>) {
*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<str>,
n: usize,
) -> Result<Vec<EmailMetadata>> {
let folder = folder.as_ref();
let messages: Vec<EmailMetadata> = 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<T>(res: Result<T, SqlxError>) -> Result<Option<T>> {

View file

@ -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<Arc<AccountRef>>,
pub current_folder: Option<String>,
pub message_list: TableState,
pub selected: Arc<AtomicU32>,
pub change: Arc<AtomicI8>,
@ -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<Vec<EmailMetadata>> = 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<Vec<EmailMetadata>> = 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<str>) {
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();
}
}
}

View file

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

View file

@ -16,7 +16,7 @@ pub trait Window: HandlesInput {
// async fn draw(&self, f: FrameType, area: Rect, ui: Rc<UI>);
/// Update function
fn update(&mut self) {}
async fn update(&mut self) {}
}
downcast_rs::impl_downcast!(Window);