diff options
author | Matthias Beyer <mail@beyermatthias.de> | 2020-07-23 15:33:26 +0200 |
---|---|---|
committer | Matthias Beyer <mail@beyermatthias.de> | 2020-07-23 15:33:26 +0200 |
commit | 8242b5ffb29f3c20ee33221fd3a07e96a087cb27 (patch) | |
tree | 61532484ce7a9fb951ea435eb8cfe6de5f810415 | |
parent | 7c77ae52c546596ac2be057efeefd5939e0c0d26 (diff) | |
parent | cc1a2e381cce09e7ed7039cc14c7e9823cbd0529 (diff) |
Merge branch 'notmuch'
This switches to an implementation that only relies on notmuch as a mail
backend.
Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | config.toml | 2 | ||||
-rw-r--r-- | shell.nix | 1 | ||||
-rw-r--r-- | src/configuration.rs | 19 | ||||
-rw-r--r-- | src/database_connection.rs | 22 | ||||
-rw-r--r-- | src/loader.rs | 19 | ||||
-rw-r--r-- | src/maillist_view.rs | 152 | ||||
-rw-r--r-- | src/mailstore.rs | 120 | ||||
-rw-r--r-- | src/main.rs | 43 | ||||
-rw-r--r-- | src/main_view.rs | 206 | ||||
-rw-r--r-- | src/sidebar.rs | 3 |
11 files changed, 279 insertions, 313 deletions
@@ -10,11 +10,14 @@ walkdir = "2" anyhow = "1" log = "0.4" env_logger = "0.7" -maildir.git = "https://git.sr.ht/~matthiasbeyer/maildir" mailparse.git = "https://git.sr.ht/~matthiasbeyer/mailparse" cursive-tabs = "0.5" cursive_table_view.git = "https://git.sr.ht/~matthiasbeyer/cursive_table_view" cursive-async-view = "0.4" +notmuch = "0.6" +config = "0.10" +serde = { version = "1.0", features = ["derive"] } +chrono = "0.4" cursive = "0.15" diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..99d004c --- /dev/null +++ b/config.toml @@ -0,0 +1,2 @@ +notmuch_database_path = "/tmp/mails" +notmuch_default_query = "tag:inbox and tag:new" @@ -21,6 +21,7 @@ pkgs.mkShell { openssl pkgconfig ncurses + notmuch ]; LIBCLANG_PATH = "${pkgs.llvmPackages.libclang}/lib"; } diff --git a/src/configuration.rs b/src/configuration.rs new file mode 100644 index 0000000..165b503 --- /dev/null +++ b/src/configuration.rs @@ -0,0 +1,19 @@ +use std::path::PathBuf; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Configuration { + notmuch_database_path: PathBuf, + notmuch_default_query: String, +} + +impl Configuration { + pub fn notmuch_database_path(&self) -> &PathBuf { + &self.notmuch_database_path + } + + pub fn notmuch_default_query(&self) -> &String { + &self.notmuch_default_query + } +} + diff --git a/src/database_connection.rs b/src/database_connection.rs new file mode 100644 index 0000000..21124cc --- /dev/null +++ b/src/database_connection.rs @@ -0,0 +1,22 @@ +#[derive(Debug)] +pub struct DatabaseConnection(notmuch::Database); + +impl DatabaseConnection { + + pub fn readonly(p: &PathBuf) -> Result<DatabaseConnection> { + .map(DatabaseConnection) + } + + pub fn for_thread<'d, 'q, F, T>(&self, thread_id: &str, f: F) -> Result<T> + where F: FnOnce<Threads<'d, 'q>> -> Result<T>, + T: Sized + { + let query = format!("thread:{}", thread_id); + + self.0 + .create_query(query)? + .search_threads() + .and_then(func) + } + +} diff --git a/src/loader.rs b/src/loader.rs deleted file mode 100644 index 9aae9c8..0000000 --- a/src/loader.rs +++ /dev/null @@ -1,19 +0,0 @@ -/// Helper trait to load something and then postprocess it -/// -/// the load() function might fail, but the PostProcessor::postprocess() function should never fail. -pub trait Loader { - type Output: Sized; - type Error: Sized; - type PostProcessedOutput: Sized; - type PostProcessor: PostProcessor<Self::Output, Output = Self::PostProcessedOutput>; - - fn load(self) -> Result<Self::Output, Self::Error>; - fn postprocessor(&self, load_name: String) -> Self::PostProcessor; -} - -pub trait PostProcessor<Object: Sized> { - type Output: Sized; - - fn postprocess(&self, o: Object) -> Self::Output; -} - diff --git a/src/maillist_view.rs b/src/maillist_view.rs new file mode 100644 index 0000000..97afff7 --- /dev/null +++ b/src/maillist_view.rs @@ -0,0 +1,152 @@ +use std::path::PathBuf; + +use anyhow::Result; +use cursive::Cursive; +use cursive::Printer; +use cursive::Rect; +use cursive::View; +use cursive::XY; +use cursive::direction::Direction; +use cursive::view::Selector; +use cursive::event::Event; +use cursive::event::EventResult; +use cursive::views::ResizedView; +use cursive_table_view::TableView; +use cursive_table_view::TableViewItem; + +pub struct MaillistView(TableView<MailListingData, MailListingColumn>); + +impl MaillistView { + pub fn create_for(database_path: &PathBuf, query: &str, name: String) -> Result<Self> { + debug!("Getting '{}' from '{}'", query, database_path.display()); + + let items = notmuch::Database::open(database_path, notmuch::DatabaseMode::ReadOnly)? + .create_query(query)? + .search_messages()? + .map(|msg| { + Ok(MailListingData { + mail_id: msg.id().to_string(), + tags: msg.tags().collect(), + date: chrono::naive::NaiveDateTime::from_timestamp_opt(msg.date(), 0) + .ok_or_else(|| anyhow!("Failed to parse timestamp: {}", msg.date()))? + .to_string(), + + from: msg.header("From")?.ok_or_else(|| anyhow!("Failed to get From for {}", msg.id()))?.to_string(), + to: msg.header("To")?.map(|c| c.to_string()).unwrap_or_else(|| String::from("")), + subject: msg.header("Subject")?.ok_or_else(|| anyhow!("Failed to get Subject for {}", msg.id()))?.to_string(), + }) + }) + .collect::<Result<Vec<_>>>()?; + + debug!("Found {} entries", items.len()); + let tab = TableView::<MailListingData, MailListingColumn>::new() + .column(MailListingColumn::Date, "Date", |c| c.width(20)) + .column(MailListingColumn::Tags, "Tags", |c| c.width(20)) + .column(MailListingColumn::From, "From", |c| c) + .column(MailListingColumn::To, "To", |c| c) + .column(MailListingColumn::Subject, "Subject", |c| c) + .default_column(MailListingColumn::Date) + .items(items) + .selected_item(0) + .on_submit(move |siv: &mut Cursive, row: usize, _: usize| { + let mail_id = siv.call_on_name(&name, move |table: &mut ResizedView<TableView<MailListingData, MailListingColumn>>| { + table.get_inner_mut() + .borrow_item(row) + .map(|data| data.mail_id.clone()) + }); + + // use the mail ID to get the whole thread and open it as a table item + }); + + Ok(MaillistView(tab)) + } +} + +impl View for MaillistView { + fn draw(&self, printer: &Printer) { + self.0.draw(printer) + } + + fn layout(&mut self, xy: XY<usize>) { + self.0.layout(xy) + } + + fn needs_relayout(&self) -> bool { + self.0.needs_relayout() + } + + fn required_size(&mut self, constraint: XY<usize>) -> XY<usize> { + self.0.required_size(constraint) + } + + fn on_event(&mut self, e: Event) -> EventResult { + self.0.on_event(e) + } + + fn call_on_any<'a>(&mut self, s: &Selector, tpl: &'a mut (dyn FnMut(&mut (dyn View + 'static)) + 'a)) { + self.0.call_on_any(s, tpl); + } + + fn focus_view(&mut self, s: &Selector) -> Result<(), ()> { + self.0.focus_view(s) + } + + fn take_focus(&mut self, source: Direction) -> bool { + self.0.take_focus(source) + } + + fn important_area(&self, view_size: XY<usize>) -> Rect { + self.0.important_area(view_size) + } + + fn type_name(&self) -> &'static str { + self.0.type_name() + } + +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub enum MailListingColumn { + Date, + Tags, + From, + To, + Subject, +} + +#[derive(Clone, Debug)] +pub struct MailListingData { + mail_id: String, + tags: Vec<String>, + date: String, + from: String, + to: String, + subject: String, +} + +impl TableViewItem<MailListingColumn> for MailListingData { + + fn to_column(&self, column: MailListingColumn) -> String { + match column { + MailListingColumn::Date => self.date.clone(), + MailListingColumn::Tags => self.tags.join(", "), + MailListingColumn::From => self.from.clone(), + MailListingColumn::To => self.to.clone(), + MailListingColumn::Subject => self.subject.clone(), + } + } + + fn cmp(&self, other: &Self, column: MailListingColumn) -> std::cmp::Ordering + where Self: Sized + { + match column { + MailListingColumn::Date => self.date.cmp(&other.date), + MailListingColumn::Tags => self.tags.cmp(&other.tags), + MailListingColumn::From => self.from.cmp(&other.from), + MailListingColumn::To => self.to.cmp(&other.to), + MailListingColumn::Subject => self.subject.cmp(&other.subject), + } + } + +} + diff --git a/src/mailstore.rs b/src/mailstore.rs deleted file mode 100644 index 49f911f..0000000 --- a/src/mailstore.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::ops::Deref; -use std::path::PathBuf; -use std::iter::FromIterator; -use anyhow::Result; -use maildir::Maildir; -use maildir::MailEntry; -use mailparse::ParsedMail; - -pub struct MailStore { - new: Vec<Mail>, - cur: Vec<Mail>, -} - -impl MailStore { - pub fn build_from_path(path: PathBuf) -> MailStoreBuilder { - let md = Maildir::from(path); - MailStoreBuilder { - cur: Box::new(md.list_cur().map(|m| Mail::cur_from(m?))), - new: Box::new(md.list_new().map(|m| Mail::new_from(m?))), - } - } - - pub fn new_mail(&self) -> &Vec<Mail> { - &self.new - } - - pub fn cur_mail(&self) -> &Vec<Mail> { - &self.cur - } -} - - -impl FromIterator<Mail> for MailStore { - fn from_iter<T>(iter: T) -> Self - where T: IntoIterator<Item = Mail> - { - let mut new = vec![]; - let mut cur = vec![]; - for mail in iter { - if mail.is_new() { - new.push(mail) - } else if mail.is_cur() { - cur.push(mail) - } else { - unreachable!("Not implemented yet, should not be reachable in current implementation") - } - } - - MailStore { new, cur } - } -} - -pub struct MailStoreBuilder { - cur: Box<dyn Iterator<Item = Result<Mail>>>, - new: Box<dyn Iterator<Item = Result<Mail>>>, -} - -impl Iterator for MailStoreBuilder { - type Item = Result<Mail>; - - fn next(&mut self) -> Option<Self::Item> { - if let Some(next) = self.cur.next() { - return Some(next) - } - - self.new.next() - } -} - -#[derive(Clone, Debug)] -pub struct Mail { - entry: MailEntry, - parsed: ParsedMail, - mailtype: MailType, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -enum MailType { - Cur, - New, -} - -impl Mail { - fn cur_from(mut entry: MailEntry) -> Result<Self> { - Ok(Mail { - parsed: entry.parsed()?, - entry, - mailtype: MailType::Cur, - }) - } - - fn new_from(mut entry: MailEntry) -> Result<Self> { - Ok(Mail { - parsed: entry.parsed()?, - entry, - mailtype: MailType::New, - }) - } - - pub fn parsed(&self) -> &ParsedMail { - &self.parsed - } - - pub fn is_new(&self) -> bool { - self.mailtype == MailType::New - } - - pub fn is_cur(&self) -> bool { - self.mailtype == MailType::Cur - } -} - -impl Deref for Mail { - type Target = MailEntry; - - fn deref(&self) -> &Self::Target { - &self.entry - } -} - diff --git a/src/main.rs b/src/main.rs index 2eb311f..6f7000e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,45 +1,32 @@ #[macro_use] extern crate anyhow; #[macro_use] extern crate log; -use std::path::PathBuf; use anyhow::Result; use cursive::CursiveExt; +use cursive::event::{Event, EventTrigger}; -mod mailstore; -mod sidebar; mod main_view; -mod loader; +mod maillist_view; +mod configuration; + +use configuration::Configuration; fn main() -> Result<()> { cursive::logger::init(); - let maildir_path = std::env::args() - .skip(1) - .next() - .map(PathBuf::from) - .ok_or_else(|| anyhow!("No path to maildir passed"))?; - - let pathes = walkdir::WalkDir::new(maildir_path.clone()) - .max_depth(1) - .follow_links(false) - .same_file_system(true) - .into_iter() - .filter_map(Result::ok) - .filter(|de| de.file_type().is_dir()) - .map(|de| de.path().to_path_buf()) - .collect(); - - let sidebar = sidebar::Sidebar::new(pathes); + let mut config = config::Config::default(); + config + .merge(config::File::with_name("config"))? + .merge(config::Environment::with_prefix("MUAR"))?; + let config = config.try_into::<Configuration>()?; - let mainview = main_view::MainView::new(); + let mut siv = cursive::Cursive::default(); - let mut layout = cursive::views::LinearLayout::horizontal(); - layout.add_child(sidebar); - layout.add_child(mainview); + let event = Event::Char('q'); + let trigger: EventTrigger = event.clone().into(); + siv.set_on_post_event(trigger, |s| s.quit()); - let mut siv = cursive::Cursive::default(); - siv.add_layer(layout); - siv.add_global_callback('q', |s| s.quit()); + siv.add_fullscreen_layer(main_view::MainView::new(config)?); siv.add_global_callback('~', cursive::Cursive::toggle_debug_console); debug!("Starting cursive"); diff --git a/src/main_view.rs b/src/main_view.rs index 35d5979..748c1ea 100644 --- a/src/main_view.rs +++ b/src/main_view.rs @@ -1,4 +1,3 @@ -use std::path::PathBuf; use anyhow::Result; use cursive::Cursive; use cursive::Printer; @@ -6,28 +5,24 @@ use cursive::Rect; use cursive::View; use cursive::XY; use cursive::direction::Direction; +use cursive::event::Callback; use cursive::event::Event; use cursive::event::EventResult; use cursive::view::Nameable; use cursive::view::Selector; -use cursive::views::ListView; -use cursive::views::LinearLayout; +use cursive::views::Dialog; +use cursive::views::EditView; use cursive::views::NamedView; use cursive::views::ResizedView; -use cursive::views::TextView; -use mailparse::MailHeaderMap; -use cursive_table_view::TableView; -use cursive_table_view::TableViewItem; -use cursive_table_view::TableColumn; -use crate::mailstore::MailStore; -use crate::mailstore::Mail; -use crate::loader::Loader; -use crate::loader::PostProcessor; +use crate::configuration::Configuration; +use crate::maillist_view::MaillistView; pub const MAIN_VIEW_NAME: &'static str = "main_view"; +pub const MAIN_MAIL_LIST_NAME: &'static str = "main_mail_list"; pub struct MainView { + config: Configuration, tabs: cursive_tabs::TabPanel<String>, } @@ -49,7 +44,30 @@ impl View for MainView { } fn on_event(&mut self, e: Event) -> EventResult { - self.tabs.on_event(e) + match e { + Event::Char('q') => { + if self.tabs.tab_order().len() == 1 { + EventResult::Ignored + } else { + if let Some(key) = self.tabs.active_tab().cloned() { + if let Err(e) = self.tabs.remove_tab(&key) { + error!("{:?}", e); // TODO do more than just logging + } + debug!("Remove tab"); + EventResult::Consumed(None) + } else { + debug!("No tab to remove found."); + EventResult::Ignored + } + } + }, + + Event::Char('o') => { + EventResult::Consumed(Some(Callback::from_fn(MainView::add_notmuch_query_layer))) + }, + + other => self.tabs.on_event(other), + } } fn call_on_any<'a>(&mut self, s: &Selector, tpl: &'a mut (dyn FnMut(&mut (dyn View + 'static)) + 'a)) { @@ -75,159 +93,59 @@ impl View for MainView { } impl MainView { - pub fn new() -> NamedView<Self> { + pub fn new(config: Configuration) -> Result<NamedView<Self>> { let tabs = cursive_tabs::TabPanel::default() .with_bar_alignment(cursive_tabs::Align::Start) .with_bar_placement(cursive_tabs::Placement::HorizontalTop) - .with_tab(String::from("nobox"), { + .with_tab(config.notmuch_default_query().clone(), { ResizedView::new(cursive::view::SizeConstraint::Full, cursive::view::SizeConstraint::Full, - TextView::new("No mailbox loaded")) + MaillistView::create_for(config.notmuch_database_path(), config.notmuch_default_query(), MAIN_MAIL_LIST_NAME.to_string())? + .with_name(MAIN_MAIL_LIST_NAME)) }); - MainView { tabs }.with_name(MAIN_VIEW_NAME) - } - - pub fn maildir_loader(&mut self, pb: PathBuf) -> MaildirLoader { - debug!("Creating Loader for: {}", pb.display()); - MaildirLoader(pb) + Ok(MainView { config, tabs }.with_name(MAIN_VIEW_NAME)) } pub fn add_tab<T: View>(&mut self, id: String, view: T) { self.tabs.add_tab(id, view) } -} - -pub struct MaildirLoader(PathBuf); - -impl Loader for MaildirLoader { - type Output = Vec<MailListingData>; - type Error = String; - type PostProcessedOutput = LinearLayout; - type PostProcessor = MaildirLoaderPostProcessor; - - fn load(self) -> Result<Self::Output, Self::Error> { - debug!("Loading: {}", self.0.display()); - MailStore::build_from_path(self.0).collect::<Result<MailStore>>() - .map(|store| { - store.cur_mail() - .iter() - .chain(store.new_mail().iter()) - .map(|mail| { - debug!("Loaded: {:?}", mail.parsed()); - let date = mail.parsed().headers.get_first_value("Date").unwrap_or_else(|| String::from("No date")); - let from = mail.parsed().headers.get_first_value("From").unwrap_or_else(|| String::from("No From")); - let to = mail.parsed().headers.get_first_value("To").unwrap_or_else(|| String::from("No To")); - let subject = mail.parsed().headers.get_first_value("Subject").unwrap_or_else(|| String::from("No Subject")); - - // FIXME: do not clone Mail object - MailListingData { mail: mail.clone(), date, from, to, subject } - }) - .collect() - }) - .map_err(|e| e.to_string()) - } - - fn postprocessor(&self, load_name: String) -> Self::PostProcessor { - MaildirLoaderPostProcessor { name: load_name } + pub fn config(&self) -> &Configuration { + &self.config } -} -pub struct MaildirLoaderPostProcessor { - name: String -} - -impl PostProcessor<Vec<MailListingData>> for MaildirLoaderPostProcessor { - type Output = LinearLayout; - - fn postprocess(&self, list: Vec<MailListingData>) -> Self::Output { - use cursive::view::SizeConstraint; - - let name = self.name.clone(); - - let tab = TableView::<MailListingData, MailListingColumn>::new() - .column(MailListingColumn::Date, "Date", |c| c.width(20)) - .column(MailListingColumn::From, "From", |c| c) - .column(MailListingColumn::To, "To", |c| c) - .column(MailListingColumn::Subject, "Subject", |c| c) - .default_column(MailListingColumn::Date) - .items(list) - .selected_item(0) - .on_submit(move |siv: &mut Cursive, row: usize, index: usize| { - let mail_body = siv.call_on_name(&format!("{}-mail-list", name), move |table: &mut ResizedView<TableView<MailListingData, MailListingColumn>>| { - table.get_inner_mut() // TODO: Bug in cursive_table_view::TableView::borrow_item() implementation - .borrow_item(row) - .map(|mail_listing| { - mail_listing.mail.parsed().get_body() - }) - }); - - siv.call_on_name(&format!("{}-mail-view", name), move |mail_view: &mut ResizedView<TextView>| { - let body = match mail_body.flatten() { - Some(Ok(body)) => body, - Some(Err(e)) => format!("Failed to parse mail body: {:?}", e), - None => format!("No mail body found"), - }; - - mail_view.get_inner_mut().set_content(body); - }); + fn add_notmuch_query_layer(siv: &mut Cursive) { + let edit_view = EditView::new() + .on_submit(move |siv: &mut Cursive, query: &str| { + let database_path = siv.call_on_name(MAIN_VIEW_NAME, move |main_view: &mut MainView| { + main_view.config().notmuch_database_path().clone() }); - LinearLayout::vertical() - .child({ - ResizedView::new(SizeConstraint::Full, - SizeConstraint::Full, - tab) - .with_name(format!("{}-mail-list", self.name)) - }) - .child({ - ResizedView::new(SizeConstraint::Full, - SizeConstraint::AtMost(50), - TextView::new("Not a loaded mail yet")) - .with_name(format!("{}-mail-view", self.name)) - }) - } -} + let database_path = database_path.unwrap(); // TODO: Fixme -#[derive(Copy, Clone, PartialEq, Eq, Hash)] -pub enum MailListingColumn { - Date, - From, - To, - Subject, -} + let tab_name = format!("{}-view", query); + let tab = MaillistView::create_for(&database_path, query, query.to_string()) + .unwrap() // TODO: FIXME + .with_name(tab_name); -#[derive(Clone, Debug)] -pub struct MailListingData { - mail: Mail, - date: String, - from: String, - to: String, - subject: String, -} + let tab = ResizedView::new(cursive::view::SizeConstraint::Full, + cursive::view::SizeConstraint::Full, + tab); -impl TableViewItem<MailListingColumn> for MailListingData { + siv.call_on_name(MAIN_VIEW_NAME, move |main_view: &mut MainView| { + main_view.add_tab(query.to_string(), tab); + }); - fn to_column(&self, column: MailListingColumn) -> String { - match column { - MailListingColumn::Date => self.date.clone(), - MailListingColumn::From => self.from.clone(), - MailListingColumn::To => self.to.clone(), - MailListingColumn::Subject => self.subject.clone(), - } - } + siv.pop_layer(); + }) + .with_name("query"); - fn cmp(&self, other: &Self, column: MailListingColumn) -> std::cmp::Ordering - where Self: Sized - { - match column { - MailListingColumn::Date => self.date.cmp(&other.date), - MailListingColumn::From => self.from.cmp(&other.from), - MailListingColumn::To => self.to.cmp(&other.to), - MailListingColumn::Subject => self.subject.cmp(&other.subject), - } + siv.add_layer({ + ResizedView::new(cursive::view::SizeConstraint::AtLeast(80), + cursive::view::SizeConstraint::Free, + Dialog::around(edit_view).title("Query")) + }) } } - diff --git a/src/sidebar.rs b/src/sidebar.rs index 9da58ff..4573eb6 100644 --- a/src/sidebar.rs +++ b/src/sidebar.rs @@ -31,7 +31,8 @@ pub struct Sidebar { } impl Sidebar { - pub fn new(pathes: Vec<PathBuf>) -> Self { + pub fn new(config: Configuration) -> Self { + let mut tv = cursive_tree_view::TreeView::default(); pathes |