message list is now fetched from the database
This commit is contained in:
parent
22f11544e0
commit
1bc6776615
6 changed files with 204 additions and 118 deletions
|
@ -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
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
mod client;
|
||||
mod event;
|
||||
mod metadata;
|
||||
mod store;
|
||||
pub mod store;
|
||||
|
||||
use anyhow::Result;
|
||||
use futures::{
|
||||
|
|
|
@ -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<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>> {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue