summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml4
-rw-r--r--config.toml2
-rw-r--r--shell.nix1
-rw-r--r--src/configuration.rs19
-rw-r--r--src/database_connection.rs22
-rw-r--r--src/loader.rs19
-rw-r--r--src/mail.rs36
-rw-r--r--src/mailstore.rs120
-rw-r--r--src/main.rs37
-rw-r--r--src/main_view.rs127
-rw-r--r--src/sidebar.rs3
11 files changed, 129 insertions, 261 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 74ff360..44c923d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,12 +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"
diff --git a/shell.nix b/shell.nix
index df41ddf..26c81fc 100644
--- a/shell.nix
+++ b/shell.nix
@@ -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/mail.rs b/src/mail.rs
new file mode 100644
index 0000000..1671721
--- /dev/null
+++ b/src/mail.rs
@@ -0,0 +1,36 @@
+use std::path::PathBuf;
+use anyhow::Result;
+use anyhow::Error;
+use mailparse::ParsedMail;
+
+#[derive(Clone, Debug)]
+pub struct Mail {
+ id: String,
+ buffer: Vec<u8>,
+ parsed: ParsedMail,
+}
+
+impl Mail {
+ pub fn read_from_path(id: String, pb: PathBuf) -> Result<Self> {
+ std::fs::read(pb)
+ .map_err(Error::from)
+ .and_then(|buffer| {
+ mailparse::parse_mail(buffer.clone())
+ .map(|parsed| (buffer, parsed))
+ .map_err(Error::from)
+ })
+ .map(|(buffer, parsed)| Mail { id, buffer, parsed })
+ }
+
+ pub fn parsed(&self) -> &ParsedMail {
+ &self.parsed
+ }
+
+ pub fn id(&self) -> &String {
+ &self.id
+ }
+
+}
+
+
+
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..c5f23bd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,40 +5,23 @@ use std::path::PathBuf;
use anyhow::Result;
use cursive::CursiveExt;
-mod mailstore;
-mod sidebar;
mod main_view;
-mod loader;
+mod mail;
+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 mainview = main_view::MainView::new();
-
- let mut layout = cursive::views::LinearLayout::horizontal();
- layout.add_child(sidebar);
- layout.add_child(mainview);
+ 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 mut siv = cursive::Cursive::default();
- siv.add_layer(layout);
+ siv.add_layer(main_view::MainView::new(config)?);
siv.add_global_callback('q', |s| s.quit());
siv.add_global_callback('~', cursive::Cursive::toggle_debug_console);
diff --git a/src/main_view.rs b/src/main_view.rs
index 35d5979..6368378 100644
--- a/src/main_view.rs
+++ b/src/main_view.rs
@@ -15,17 +15,15 @@ use cursive::views::LinearLayout;
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 cursive_tabs::TabPanel;
-use crate::mailstore::MailStore;
-use crate::mailstore::Mail;
-use crate::loader::Loader;
-use crate::loader::PostProcessor;
+use crate::configuration::Configuration;
pub const MAIN_VIEW_NAME: &'static str = "main_view";
+pub const MAIN_MAIL_LIST_NAME: &'static str = "main_mail_list";
pub struct MainView {
tabs: cursive_tabs::TabPanel<String>,
@@ -75,76 +73,36 @@ 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(String::from("main"), {
ResizedView::new(cursive::view::SizeConstraint::Full,
cursive::view::SizeConstraint::Full,
- TextView::new("No mailbox loaded"))
+ MainView::main_mail_list(&config)?)
});
- MainView { tabs }.with_name(MAIN_VIEW_NAME)
+ Ok(MainView { tabs }.with_name(MAIN_VIEW_NAME))
}
- pub fn maildir_loader(&mut self, pb: PathBuf) -> MaildirLoader {
- debug!("Creating Loader for: {}", pb.display());
- MaildirLoader(pb)
- }
-
- pub fn add_tab<T: View>(&mut self, id: String, view: T) {
- self.tabs.add_tab(id, view)
- }
-
-}
+ fn main_mail_list(config: &Configuration) -> Result<NamedView<TableView<MailListingData, MailListingColumn>>> {
+ let items = notmuch::Database::open(config.notmuch_database_path(), notmuch::DatabaseMode::ReadOnly)?
+ .create_query(config.notmuch_default_query())?
+ .search_messages()?
+ .map(|msg| {
+ Ok(MailListingData {
+ mail: crate::mail::Mail::read_from_path(msg.id().to_string(), msg.filename())?,
+ date: chrono::naive::NaiveDateTime::from_timestamp_opt(msg.date(), 0)
+ .ok_or_else(|| anyhow!("Failed to parse timestamp: {}", msg.date()))?
+ .to_string(),
-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()
+ 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(),
+ })
})
- .map_err(|e| e.to_string())
- }
-
- fn postprocessor(&self, load_name: String) -> Self::PostProcessor {
- MaildirLoaderPostProcessor { name: load_name }
- }
-}
-
-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();
+ .collect::<Result<Vec<_>>>()?;
let tab = TableView::<MailListingData, MailListingColumn>::new()
.column(MailListingColumn::Date, "Date", |c| c.width(20))
@@ -152,42 +110,25 @@ impl PostProcessor<Vec<MailListingData>> for MaildirLoaderPostProcessor {
.column(MailListingColumn::To, "To", |c| c)
.column(MailListingColumn::Subject, "Subject", |c| c)
.default_column(MailListingColumn::Date)
- .items(list)
+ .items(items)
.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
+ let mail_id = siv.call_on_name(&MAIN_MAIL_LIST_NAME, move |table: &mut ResizedView<TableView<MailListingData, MailListingColumn>>| {
+ table.get_inner_mut()
.borrow_item(row)
- .map(|mail_listing| {
- mail_listing.mail.parsed().get_body()
- })
+ .map(|data| data.mail.id().clone())
});
- 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);
- });
+ // use the mail ID to get the whole thread and open it as a table item
});
- 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))
- })
+ Ok(tab.with_name(MAIN_MAIL_LIST_NAME))
+ }
+
+ pub fn add_tab<T: View>(&mut self, id: String, view: T) {
+ self.tabs.add_tab(id, view)
}
+
}
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
@@ -200,7 +141,7 @@ pub enum MailListingColumn {
#[derive(Clone, Debug)]
pub struct MailListingData {
- mail: Mail,
+ mail: crate::mail::Mail,
date: String,
from: String,
to: String,
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