diff --git a/imap/src/client/inner.rs b/imap/src/client/inner.rs index 84b2fe2..96c58ff 100644 --- a/imap/src/client/inner.rs +++ b/imap/src/client/inner.rs @@ -303,9 +303,9 @@ where } else if let Some((tag, cmd, cmd_tx)) = curr_cmd.as_mut() { // we got a response from the server for this command, so send it over the // channel - debug!("sending {:?} to tag {}", resp, tag); + // debug!("sending {:?} to tag {}", resp, tag); let res = cmd_tx.send(resp); - debug!("res1: {:?}", res); + // debug!("res1: {:?}", res); } } } diff --git a/imap/src/codec.rs b/imap/src/codec.rs index 8a797d0..b03f547 100644 --- a/imap/src/codec.rs +++ b/imap/src/codec.rs @@ -13,7 +13,7 @@ impl Decoder for ImapCodec { fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { let s = std::str::from_utf8(src)?; - trace!("codec parsing {:?}", s); + // trace!("codec parsing {:?}", s); match parse_streamed_response(s) { Ok((resp, len)) => { src.advance(len); diff --git a/maildir/src/lib.rs b/maildir/src/lib.rs index e5f1491..1b85bfd 100644 --- a/maildir/src/lib.rs +++ b/maildir/src/lib.rs @@ -4,6 +4,9 @@ use anyhow::Result; use tempfile::NamedTempFile; use tokio::fs::{self, File, OpenOptions}; +/// A Maildir, as defined by [Daniel J. Bernstein][1]. +/// +/// [1]: https://cr.yp.to/proto/maildir.html pub struct Maildir { path: PathBuf, } diff --git a/src/mail/client.rs b/src/mail/client.rs index 7e370d0..564ebb2 100644 --- a/src/mail/client.rs +++ b/src/mail/client.rs @@ -23,9 +23,12 @@ use super::{MailCommand, MailEvent}; /// The main sequence of steps for the IMAP thread to follow pub async fn imap_main( + acct_name: impl AsRef, acct: MailAccountConfig, mail2ui_tx: UnboundedSender, ) -> Result<()> { + let acct_name = acct_name.as_ref().to_owned(); + // loop ensures that the connection is retried after it dies loop { let builder: ClientConfig = ClientBuilder::default() @@ -68,17 +71,20 @@ pub async fn imap_main( loop { let folder_list = authed.list().await?; debug!("mailbox list: {:?}", folder_list); - let _ = mail2ui_tx.send(MailEvent::FolderList(folder_list)); + let _ = mail2ui_tx.send(MailEvent::FolderList(acct_name.clone(), 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())); + let _ = mail2ui_tx.send(MailEvent::MessageUids( + acct_name.clone(), + 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); + let evt = MailEvent::UpdateUid(acct_name.clone(), uid, attrs); + // debug!("sent {:?}", evt); mail2ui_tx.send(evt); } @@ -110,13 +116,16 @@ pub async fn imap_main( 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())); + let _ = mail2ui_tx.send(MailEvent::MessageUids( + acct_name.clone(), + 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); + let evt = MailEvent::UpdateUid(acct_name.clone(), uid, attrs); + // debug!("sent {:?}", evt); mail2ui_tx.send(evt); } diff --git a/src/mail/event.rs b/src/mail/event.rs new file mode 100644 index 0000000..8183a9a --- /dev/null +++ b/src/mail/event.rs @@ -0,0 +1,31 @@ +use panorama_imap::response::{AttributeValue, Envelope}; + +/// Possible events returned from the server that should be sent to the UI +#[derive(Debug)] +#[non_exhaustive] +pub enum MailEvent { + /// Got the list of folders + FolderList(String, Vec), + + /// A list of the UIDs in the current mail view + MessageUids(String, Vec), + + /// Update the given UID with the given attribute list + UpdateUid(String, u32, Vec), + + /// New message came in with given UID + NewUid(String, u32), +} + +impl MailEvent { + /// Retrieves the account name that this event is associated with + pub fn acct_name(&self) -> &str { + use MailEvent::*; + match self { + FolderList(name, _) + | MessageUids(name, _) + | UpdateUid(name, _, _) + | NewUid(name, _) => name, + } + } +} diff --git a/src/mail/metadata.rs b/src/mail/metadata.rs index bf657ad..5523274 100644 --- a/src/mail/metadata.rs +++ b/src/mail/metadata.rs @@ -4,7 +4,7 @@ use chrono::{DateTime, Local}; use panorama_imap::response::*; /// A record that describes the metadata of an email as it appears in the UI list -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct EmailMetadata { /// UID if the message has one pub uid: Option, diff --git a/src/mail/mod.rs b/src/mail/mod.rs index 95b338a..f541ca2 100644 --- a/src/mail/mod.rs +++ b/src/mail/mod.rs @@ -1,6 +1,7 @@ //! Mail mod client; +mod event; mod metadata; use anyhow::Result; @@ -25,6 +26,7 @@ use tokio_stream::wrappers::WatchStream; use crate::config::{Config, ConfigWatcher, ImapAuth, MailAccountConfig, TlsMethod}; +pub use self::event::MailEvent; pub use self::metadata::EmailMetadata; /// Command sent to the mail thread by something else (i.e. UI) @@ -38,26 +40,6 @@ pub enum MailCommand { Raw(ImapCommand), } -/// Possible events returned from the server that should be sent to the UI -#[derive(Debug)] -#[non_exhaustive] -pub enum MailEvent { - /// Got the list of folders - FolderList(Vec), - - /// Got the current list of messages - MessageList(Vec), - - /// A list of the UIDs in the current mail view - MessageUids(Vec), - - /// Update the given UID with the given attribute list - UpdateUid(u32, Vec), - - /// New message came in with given UID - NewUid(u32), -} - /// Main entrypoint for the mail listener. pub async fn run_mail( mut config_watcher: ConfigWatcher, @@ -90,7 +72,7 @@ pub async fn run_mail( // this loop is to make sure accounts are restarted on error loop { - match client::imap_main(acct.clone(), mail2ui_tx.clone()).await { + match client::imap_main(&acct_name, acct.clone(), mail2ui_tx.clone()).await { Ok(_) => {} Err(err) => { error!("IMAP Error: {}", err); diff --git a/src/ui/mail_store.rs b/src/ui/mail_store.rs new file mode 100644 index 0000000..d67ef9f --- /dev/null +++ b/src/ui/mail_store.rs @@ -0,0 +1,90 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use parking_lot::RwLock; + +use crate::mail::{EmailMetadata, MailEvent}; + +/// UI's view of the currently-known mail-related state of all accounts. +#[derive(Clone, Debug, Default)] +pub struct MailStore { + accounts: Arc>>>>, +} + +impl MailStore { + pub fn handle_mail_event(&self, evt: MailEvent) { + let acct_name = evt.acct_name().to_owned(); + + { + let accounts = self.accounts.read(); + let contains_key = accounts.contains_key(&acct_name); + std::mem::drop(accounts); + + if !contains_key { + let mut accounts = self.accounts.write(); + accounts.insert( + acct_name.clone(), + Arc::new(RwLock::new(MailAccountState::default())), + ); + } + } + + let accounts = self.accounts.read(); + if let Some(lock) = accounts.get(&acct_name) { + let mut state = lock.write(); + state.update(evt); + } + } + + pub fn iter_accts(&self) -> Vec { + self.accounts.read().keys().cloned().collect() + } + + pub fn folders_of(&self, acct_name: impl AsRef) -> Option> { + let accounts = self.accounts.read(); + let lock = accounts.get(acct_name.as_ref())?; + let state = lock.read(); + Some(state.folders.clone()) + } + + pub fn messages_of(&self, acct_name: impl AsRef) -> Option> { + let accounts = self.accounts.read(); + let lock = accounts.get(acct_name.as_ref())?; + let state = lock.read(); + let mut msgs = Vec::new(); + for uid in state.message_uids.iter() { + if let Some(meta) = state.message_map.get(uid) { + msgs.push(meta.clone()); + } + } + Some(msgs) + } +} + +#[derive(Debug, Default)] +pub struct MailAccountState { + pub folders: Vec, + pub message_uids: Vec, + pub message_map: HashMap, +} + +impl MailAccountState { + pub fn update(&mut self, evt: MailEvent) { + match evt { + MailEvent::FolderList(_, new_folders) => self.folders = new_folders, + MailEvent::MessageUids(_, new_uids) => self.message_uids = new_uids, + + MailEvent::UpdateUid(_, uid, attrs) => { + let meta = EmailMetadata::from_attrs(attrs); + let uid = meta.uid.unwrap_or(uid); + self.message_map.insert(uid, meta); + } + MailEvent::NewUid(_, uid) => { + debug!("new msg!"); + self.message_uids.push(uid); + } + _ => {} + } + // debug!("mail store updated! {:?}", self); + } +} diff --git a/src/ui/mail_view.rs b/src/ui/mail_view.rs index 6bc1592..00ba7b2 100644 --- a/src/ui/mail_view.rs +++ b/src/ui/mail_view.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::sync::{ - atomic::{AtomicI8, Ordering}, + atomic::{AtomicI8, AtomicU32, Ordering}, Arc, }; @@ -19,15 +19,13 @@ use tui::{ use crate::mail::EmailMetadata; -use super::{FrameType, HandlesInput, InputResult, TermType, Window, UI}; +use super::{FrameType, HandlesInput, InputResult, MailStore, TermType, Window, UI}; -#[derive(Default, Debug)] +#[derive(Debug)] pub struct MailView { - pub folders: Vec, - pub message_uids: Vec, - pub message_map: HashMap, - pub messages: Vec, + pub mail_store: MailStore, pub message_list: TableState, + pub selected: Arc, pub change: Arc, } @@ -61,16 +59,23 @@ impl Window for MailView { .constraints([Constraint::Length(20), Constraint::Max(5000)]) .split(area); + let accts = self.mail_store.iter_accts(); + // folder list - let items = self - .folders - .iter() - .map(|s| ListItem::new(s.to_owned())) - .collect::>(); + let mut items = vec![]; + for acct in accts.iter() { + let result = self.mail_store.folders_of(acct); + if let Some(folders) = result { + items.push(ListItem::new(acct.to_owned())); + for folder in folders { + items.push(ListItem::new(format!(" {}", folder))); + } + } + } let dirlist = List::new(items) .block(Block::default().borders(Borders::NONE).title(Span::styled( - "ur mom", + "hellosu", Style::default().add_modifier(Modifier::BOLD), ))) .style(Style::default().fg(Color::White)) @@ -78,33 +83,57 @@ impl Window for MailView { .highlight_symbol(">>"); // message list table - let mut metas = self - .message_uids - .iter() - .filter_map(|id| self.message_map.get(id)) - .collect::>(); - metas.sort_by_key(|m| m.date); - let rows = metas - .iter() - .rev() - .map(|meta| { - 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), - ); + // let mut metas = self + // .message_uids + // .iter() + // .filter_map(|id| self.message_map.get(id)) + // .collect::>(); + // metas.sort_by_key(|m| m.date); + // let rows = metas + // .iter() + // .rev() + // .map(|meta| { + // 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), + // ); + // } + // row + // }) + // .collect::>(); + + let mut rows = vec![]; + for acct in accts.iter() { + let result = 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); } - row - }) - .collect::>(); + } + } let table = Table::new(rows) .style(Style::default().fg(Color::White)) .widths(&[ @@ -144,32 +173,41 @@ fn humanize_timestamp(date: DateTime) -> String { } impl MailView { - pub fn move_down(&mut self) { - if self.message_uids.is_empty() { - return; - } - let len = self.message_uids.len(); - if let Some(selected) = self.message_list.selected() { - if selected + 1 < len { - self.message_list.select(Some(selected + 1)); - } - } else { - self.message_list.select(Some(0)); + pub fn new(mail_store: MailStore) -> Self { + MailView { + mail_store, + message_list: TableState::default(), + selected: Arc::new(AtomicU32::default()), + change: Arc::new(AtomicI8::default()), } } + pub fn move_down(&mut self) { + // if self.message_uids.is_empty() { + // return; + // } + // let len = self.message_uids.len(); + // if let Some(selected) = self.message_list.selected() { + // if selected + 1 < len { + // self.message_list.select(Some(selected + 1)); + // } + // } else { + // self.message_list.select(Some(0)); + // } + } + pub fn move_up(&mut self) { - if self.message_uids.is_empty() { - return; - } - let len = self.message_uids.len(); - if let Some(selected) = self.message_list.selected() { - if selected >= 1 { - self.message_list.select(Some(selected - 1)); - } - } else { - self.message_list.select(Some(len - 1)); - } + // if self.message_uids.is_empty() { + // return; + // } + // let len = self.message_uids.len(); + // if let Some(selected) = self.message_list.selected() { + // if selected >= 1 { + // self.message_list.select(Some(selected - 1)); + // } + // } else { + // self.message_list.select(Some(len - 1)); + // } } pub fn update(&mut self) { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 039708f..4a062ab 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3,6 +3,7 @@ mod colon_prompt; mod input; mod keybinds; +mod mail_store; mod mail_view; mod messages; mod windows; @@ -41,6 +42,7 @@ use crate::mail::{EmailMetadata, MailEvent}; use self::colon_prompt::ColonPrompt; use self::input::{BaseInputHandler, HandlesInput, InputResult}; +use self::mail_store::MailStore; use self::mail_view::MailView; pub(crate) use self::messages::*; use self::windows::*; @@ -64,14 +66,17 @@ pub async fn run_ui2( let should_exit = Arc::new(AtomicBool::new(false)); + let mail_store = MailStore::default(); + let mut ui = UI { should_exit: should_exit.clone(), window_layout: WindowLayout::default(), windows: HashMap::new(), page_names: HashMap::new(), + mail_store: mail_store.clone(), }; - ui.open_window(MailView::default()); + ui.open_window(MailView::new(mail_store)); // let mut input_states: Vec> = vec![]; @@ -118,6 +123,7 @@ pub struct UI { window_layout: WindowLayout, windows: HashMap>, page_names: HashMap, + mail_store: MailStore, } impl UI { @@ -201,153 +207,6 @@ impl UI { } fn process_mail_event(&mut self, evt: MailEvent) { - debug!("received mail event: {:?}", evt); + self.mail_store.handle_mail_event(evt); } } - -/// Main entrypoint for the UI -pub async fn run_ui( - mut stdout: Stdout, - exit_tx: mpsc::Sender<()>, - mut mail2ui_rx: mpsc::UnboundedReceiver, -) -> Result<()> { - execute!(stdout, cursor::Hide, terminal::EnterAlternateScreen)?; - terminal::enable_raw_mode()?; - - let backend = CrosstermBackend::new(&mut stdout); - let mut term = Terminal::new(backend)?; - let mut mail_tab = MailView::default(); - - // state stack for handling inputs - let should_exit = Arc::new(AtomicBool::new(false)); - let mut input_states: Vec> = vec![Box::new(BaseInputHandler( - should_exit.clone(), - mail_tab.change.clone(), - ))]; - - let mut window_layout = WindowLayout::default(); - let mut page_names = HashMap::new(); - - // TODO: have this be configured thru the settings? - let (mail_id, mail_page) = window_layout.new_page(); - page_names.insert(mail_page, "Email"); - - while !should_exit.load(Ordering::Relaxed) { - term.draw(|f| { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(0) - .constraints([ - Constraint::Length(1), - Constraint::Max(5000), - Constraint::Length(1), - ]) - .split(f.size()); - - // this is the title bar - // let titles = vec!["email"].into_iter().map(Spans::from).collect(); - let titles = window_layout - .list_pages() - .iter() - .filter_map(|id| page_names.get(id)) - .map(|s| Spans::from(*s)) - .collect(); - let tabs = Tabs::new(titles); - f.render_widget(tabs, chunks[0]); - - // this is the main mail tab - // mail_tab.render(f, chunks[1]); - - // this is the status bar - if let Some(last_state) = input_states.last() { - let downcasted = last_state.downcast_ref::(); - match downcasted { - Some(colon_prompt) => { - let status = Block::default().title(vec![ - Span::styled(":", Style::default().fg(Color::Gray)), - Span::raw(&colon_prompt.value), - ]); - f.render_widget(status, chunks[2]); - f.set_cursor(colon_prompt.value.len() as u16 + 1, chunks[2].y); - } - None => { - let status = Paragraph::new("hellosu"); - f.render_widget(status, chunks[2]); - } - }; - } - })?; - - let event = if event::poll(FRAME_DURATION)? { - let event = event::read()?; - // table.update(&event); - - if let Event::Key(evt) = event { - // handle states in the state stack - // although this is written in a for loop, every case except one should break - let mut should_pop = false; - if let Some(input_state) = input_states.last_mut() { - match input_state.handle_key(&mut term, evt)? { - InputResult::Ok => {} - InputResult::Push(state) => { - input_states.push(state); - } - InputResult::Pop => { - should_pop = true; - } - } - } - - if should_pop { - input_states.pop(); - } - } - - Some(event) - } else { - None - }; - - select! { - mail_evt = mail2ui_rx.recv().fuse() => { - debug!("received mail event: {:?}", mail_evt); - // TODO: handle case that channel is closed later - let mail_evt = mail_evt.unwrap(); - - match mail_evt { - MailEvent::FolderList(new_folders) => mail_tab.folders = new_folders, - MailEvent::MessageList(new_messages) => mail_tab.messages = new_messages, - MailEvent::MessageUids(new_uids) => mail_tab.message_uids = new_uids, - - MailEvent::UpdateUid(uid, attrs) => { - let meta = EmailMetadata::from_attrs(attrs); - let uid = meta.uid.unwrap_or(uid); - mail_tab.message_map.insert(uid, meta); - } - MailEvent::NewUid(uid) => { - debug!("new msg!"); - mail_tab.message_uids.push(uid); - } - _ => {} - } - } - - // approx 60fps - _ = time::sleep(FRAME_DURATION).fuse() => {} - } - } - - mem::drop(term); - - execute!( - stdout, - style::ResetColor, - cursor::Show, - terminal::LeaveAlternateScreen - )?; - terminal::disable_raw_mode()?; - - exit_tx.send(()).await?; - debug!("sent exit"); - Ok(()) -} diff --git a/src/ui/windows.rs b/src/ui/windows.rs index 12666a6..ca7ff69 100644 --- a/src/ui/windows.rs +++ b/src/ui/windows.rs @@ -18,6 +18,9 @@ pub trait Window: HandlesInput { fn draw_inactive(&mut self, f: FrameType, area: Rect, ui: &UI) { self.draw(f, area, ui); } + + /// Update function + fn update(&mut self) {} } downcast_rs::impl_downcast!(Window);