diff options
author | Matthias Beyer <mail@beyermatthias.de> | 2020-02-28 11:46:05 +0100 |
---|---|---|
committer | Matthias Beyer <mail@beyermatthias.de> | 2020-06-01 13:56:45 +0200 |
commit | 23f25cf563db94d83bed4076b8ddcf6acb3ded7c (patch) | |
tree | 3f51a40b365fa33254bcd2cfceda437e1ab02773 | |
parent | 35a8bfdaa60e1bc449648171d7f18b253f206a26 (diff) |
libimagmail: Rewrite backend to use notmuch
This commit rewrites libimagmail to use notmuch as backend for mail
operations.
Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
-rw-r--r-- | doc/src/05100-lib-mails.md | 16 | ||||
-rw-r--r-- | lib/domain/libimagmail/Cargo.toml | 7 | ||||
-rw-r--r-- | lib/domain/libimagmail/src/config.rs | 131 | ||||
-rw-r--r-- | lib/domain/libimagmail/src/fetch.rs | 117 | ||||
-rw-r--r-- | lib/domain/libimagmail/src/iter.rs | 83 | ||||
-rw-r--r-- | lib/domain/libimagmail/src/lib.rs | 17 | ||||
-rw-r--r-- | lib/domain/libimagmail/src/mail.rs | 525 | ||||
-rw-r--r-- | lib/domain/libimagmail/src/mailflags.rs | 93 | ||||
-rw-r--r-- | lib/domain/libimagmail/src/mailtree.rs | 245 | ||||
-rw-r--r-- | lib/domain/libimagmail/src/notmuch/connection.rs | 109 | ||||
-rw-r--r-- | lib/domain/libimagmail/src/notmuch/mod.rs (renamed from lib/domain/libimagmail/src/mid.rs) | 42 | ||||
-rw-r--r-- | lib/domain/libimagmail/src/notmuch/thread.rs (renamed from lib/domain/libimagmail/src/hasher.rs) | 40 | ||||
-rw-r--r-- | lib/domain/libimagmail/src/send.rs | 111 | ||||
-rw-r--r-- | lib/domain/libimagmail/src/store.rs | 306 | ||||
-rw-r--r-- | lib/domain/libimagmail/src/util.rs | 77 |
15 files changed, 808 insertions, 1111 deletions
diff --git a/doc/src/05100-lib-mails.md b/doc/src/05100-lib-mails.md index 9a42a47a..b108f364 100644 --- a/doc/src/05100-lib-mails.md +++ b/doc/src/05100-lib-mails.md @@ -1,14 +1,10 @@ -## libimagmails +## libimagmail -The mail library implements everything that is needed for being used to -implement a mail reader (MUA). +This library implements an integration from notmuch for imag. -It therefor provides reading mailboxes, getting related content or mails, saving -attachments to external locations, crafting new mails and responses,... +The library does not (yet) provide functionality to alter the database of +notmuch and maybe never will. -It also offers, natively, ways to search for mails (which are represented as -imag entries). - -For more information on the domain of the `imag-mail` command, look at the -documentation of the @sec:modules:mails module. +It creates one imag entry for one notmuch message id. It only stores the message +id in the entry, all other data can be fetched from notmuch at runtime. diff --git a/lib/domain/libimagmail/Cargo.toml b/lib/domain/libimagmail/Cargo.toml index ad6c2a90..22bd5eba 100644 --- a/lib/domain/libimagmail/Cargo.toml +++ b/lib/domain/libimagmail/Cargo.toml @@ -27,8 +27,11 @@ mailparse = "0.8.0" filters = "0.3.0" anyhow = "1" resiter = "0.4.0" -serde = "1.0.94" -serde_derive = "1.0.94" +notmuch = "0.6" +chrono = "0.4" +indextree = "4" +itertools = "0.8" +mda = { git = "https://github.com/matthiasbeyer/mda-rs", branch = "my-master" } # "0.1" libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" } libimagerror = { version = "0.10.0", path = "../../../lib/core/libimagerror" } diff --git a/lib/domain/libimagmail/src/config.rs b/lib/domain/libimagmail/src/config.rs deleted file mode 100644 index 4c59cb87..00000000 --- a/lib/domain/libimagmail/src/config.rs +++ /dev/null @@ -1,131 +0,0 @@ -// -// imag - the personal information management suite for the commandline -// Copyright (C) 2015-2020 Matthias Beyer <mail@beyermatthias.de> and contributors -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Lesser General Public -// License as published by the Free Software Foundation; version -// 2.1 of the License. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -// - -use std::path::PathBuf; - -/// A struct representing a full mail configuration, required for working with this library -/// -/// For convenience reasons, this implements Serialize and Deserialize, so it can be fetched from a -/// configuration file for example -/// -/// # TODO -/// -/// Figure out how to use handlebars with variables on this. Right now the support for that is not -/// implemented yet. -/// -#[derive(Serialize, Deserialize, Debug)] -pub struct MailConfig { - default_account : String, - accounts : Vec<MailAccountConfig>, - fetchcommand : MailCommand, - postfetchcommand : Option<MailCommand>, - sendcommand : MailCommand, - postsendcommand : Option<MailCommand>, -} - -impl MailConfig { - pub fn default_account(&self) -> &String { - &self.default_account - } - - pub fn accounts(&self) -> &Vec<MailAccountConfig> { - &self.accounts - } - - pub fn account(&self, name: &str) -> Option<&MailAccountConfig> { - self.accounts() - .iter() - .find(|a| a.name == name) - } - - pub fn fetchcommand(&self) -> &MailCommand { - &self.fetchcommand - } - - pub fn postfetchcommand(&self) -> Option<&MailCommand> { - self.postfetchcommand.as_ref() - } - - pub fn sendcommand(&self) -> &MailCommand { - &self.sendcommand - } - - pub fn postsendcommand(&self) -> Option<&MailCommand> { - self.postsendcommand.as_ref() - } - - pub fn fetchcommand_for_account(&self, account_name: &str) -> &MailCommand { - self.accounts() - .iter() - .find(|a| a.name == account_name) - .and_then(|a| a.fetchcommand.as_ref()) - .unwrap_or_else(|| self.fetchcommand()) - } - - pub fn postfetchcommand_for_account(&self, account_name: &str) -> Option<&MailCommand> { - self.accounts() - .iter() - .find(|a| a.name == account_name) - .and_then(|a| a.postfetchcommand.as_ref()) - .or_else(|| self.postfetchcommand()) - } - - pub fn sendcommand_for_account(&self, account_name: &str) -> &MailCommand { - self.accounts() - .iter() - .find(|a| a.name == account_name) - .and_then(|a| a.sendcommand.as_ref()) - .unwrap_or_else(|| self.sendcommand()) - } - - pub fn postsendcommand_for_account(&self, account_name: &str) -> Option<&MailCommand> { - self.accounts() - .iter() - .find(|a| a.name == account_name) - .and_then(|a| a.postsendcommand.as_ref()) - .or_else(|| self.postsendcommand()) - } - -} - -/// A configuration for a single mail accounts -/// -/// If one of the keys `fetchcommand`, `postfetchcommand`, `sendcommand` or `postsendcommand` is -/// not available, the implementation of the `MailConfig` will automatically use the global -/// configuration if applicable. -#[derive(Serialize, Deserialize, Debug)] -pub struct MailAccountConfig { - pub name : String, - pub outgoingbox : PathBuf, - pub draftbox : PathBuf, - pub sentbox : PathBuf, - pub maildirroot : PathBuf, - pub fetchcommand : Option<MailCommand>, - pub postfetchcommand : Option<MailCommand>, - pub sendcommand : Option<MailCommand>, - pub postsendcommand : Option<MailCommand>, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct MailCommand { - command: String, - env: Vec<String>, - args: Vec<String>, -} - diff --git a/lib/domain/libimagmail/src/fetch.rs b/lib/domain/libimagmail/src/fetch.rs deleted file mode 100644 index 72f5d32c..00000000 --- a/lib/domain/libimagmail/src/fetch.rs +++ /dev/null @@ -1,117 +0,0 @@ -// -// imag - the personal information management suite for the commandline -// Copyright (C) 2015-2020 Matthias Beyer <mail@beyermatthias.de> and contributors -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Lesser General Public -// License as published by the Free Software Foundation; version -// 2.1 of the License. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -// - -use config::MailConfig; - -pub struct MailFetcher<'a> { - config: &'a MailConfig, - account_name_to_fetch: Option<String>, - boxes: Vec<String>, - - rescan_maildirs: bool, -} - -impl MailFetcher { - pub fn new(config: &MailConfig) -> Self { - MailFetcher { - config, - account_name_to_fetch: None, - rescan_maildirs: false - } - } - - pub fn fetch_account(mut self, name: String) -> Self { - self.account_name_to_fetch = Some(name); - self - } - - pub fn fetch_box(mut self, name: String) -> Self { - self.boxes.push(name); - self - } - - pub fn fetch_boxes<I>(mut self, names: I) -> Self - where I: IntoIterator<Item = String> - { - self.boxes.append(names.into_iter().collect()) - self - } - - pub fn rescan_maildirs(mut self, b: bool) -> Self { - self.rescan_maildirs = b; - self - } - - pub fn run(&self, store: &Store) -> Result<()> { - let fetchcommand = match self.account_name_to_fetch { - Some(name) => self.config.fetchcommand_for_account(name), - None => self.confnig.fetchcommand(), - }; - - let postfetchcommand = match self.account_name_to_fetch { - Some(name) => self.config.postfetchcommand_for_account(name), - None => self.confnig.postfetchcommand(), - }; - - let account = config - .account(self.account_name_to_fetch) - .ok_or_else(|| anyhow!("Account '{}' does not exist", self.account_name_to_fetch))?; - - if fetchcommand.contains(" ") { - // error on whitespace in command - } - - if postfetchcommand.contains(" ") { - // error on whitespace in command - } - - // fetchcommand - - let mut output = Command::new(fetchcommand) - // TODO: Add argument support - // TODO: Add support for passing config variables - // TODO: Add support for passing environment - .args(self.boxes) - .wait_with_output() - .context("Mail fetching")?; - - write!(rt.stdout(), "{}", output.stdout)?; - write!(rt.stderr(), "{}", output.stderr)?; - - // postfetchcommand - - let output = Command::new(postfetchcommand) - // TODO: Add argument support - // TODO: Add support for passing config variables - .wait_with_output() - .context("Post 'Mail fetching' command")?; - - write!(rt.stdout(), "{}", output.stdout)?; - write!(rt.stderr(), "{}", output.stderr)?; - - if self.rescan_maildirs { - // scan - // account.maildirroot - // recursively for new mail and store them in imag - } - } - -} - - diff --git a/lib/domain/libimagmail/src/iter.rs b/lib/domain/libimagmail/src/iter.rs deleted file mode 100644 index f1bb11a0..00000000 --- a/lib/domain/libimagmail/src/iter.rs +++ /dev/null @@ -1,83 +0,0 @@ -// -// imag - the personal information management suite for the commandline -// Copyright (C) 2015-2020 Matthias Beyer <mail@beyermatthias.de> and contributors -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Lesser General Public -// License as published by the Free Software Foundation; version -// 2.1 of the License. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -// - -use anyhow::Result; -use anyhow::Context; -use anyhow::Error; - - -use libimagstore::iter::get::StoreGetIterator; -use libimagstore::store::FileLockEntry; - -use crate::mail::Mail; - -pub struct MailIterator<'a> { - inner: StoreGetIterator<'a>, - ignore_ungetable: bool, -} - -impl<'a> MailIterator<'a> { - pub fn new(sgi: StoreGetIterator<'a>) -> Self { - MailIterator { inner: sgi, ignore_ungetable: true } - } - - pub fn ignore_ungetable(mut self, b: bool) -> Self { - self.ignore_ungetable = b; - self - } -} - -pub trait IntoMailIterator<'a> { - fn into_mail_iterator(self) -> MailIterator<'a>; -} - -impl<'a> IntoMailIterator<'a> for StoreGetIterator<'a> { - fn into_mail_iterator(self) -> MailIterator<'a> { - MailIterator::new(self) - } -} - -impl<'a> Iterator for MailIterator<'a> { - type Item = Result<FileLockEntry<'a>>; - - fn next(&mut self) -> Option<Self::Item> { - while let Some(n) = self.inner.next() { - match n { - Ok(Some(fle)) => { - match fle.is_mail().context("Checking whether entry is a Mail").map_err(Error::from) { - Err(e) => return Some(Err(e)), - Ok(true) => return Some(Ok(fle)), - Ok(false) => continue, - } - }, - - Ok(None) => if self.ignore_ungetable { - continue - } else { - return Some(Err(anyhow!("Failed to get one entry"))) - }, - - Err(e) => return Some(Err(e)), - } - } - - None - } -} - diff --git a/lib/domain/libimagmail/src/lib.rs b/lib/domain/libimagmail/src/lib.rs index 65ea418c..f30c5e7c 100644 --- a/lib/domain/libimagmail/src/lib.rs +++ b/lib/domain/libimagmail/src/lib.rs @@ -38,14 +38,16 @@ )] #[macro_use] extern crate log; -extern crate mailparse; extern crate toml; extern crate toml_query; extern crate filters; #[macro_use] extern crate anyhow; extern crate resiter; -extern crate serde; -#[macro_use] extern crate serde_derive; +extern crate chrono; +extern crate notmuch as notmuch_rs; +extern crate mda; +extern crate indextree; +extern crate itertools; extern crate libimagerror; #[macro_use] extern crate libimagstore; @@ -55,12 +57,9 @@ extern crate libimagentrylink; module_entry_path_mod!("mail"); -pub mod config; -pub mod hasher; -pub mod iter; +pub mod notmuch; pub mod mail; -pub mod mailflags; -pub mod mid; +pub mod mailtree; pub mod store; -pub mod util; +mod util; diff --git a/lib/domain/libimagmail/src/mail.rs b/lib/domain/libimagmail/src/mail.rs index d26346dd..1f001aab 100644 --- a/lib/domain/libimagmail/src/mail.rs +++ b/lib/domain/libimagmail/src/mail.rs @@ -17,371 +17,300 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // -use std::str::FromStr; - -use anyhow::Result; -use anyhow::Context; -use anyhow::Error; -use toml_query::read::TomlValueReadExt; -use resiter::Filter; +use std::path::PathBuf; +use std::fs::OpenOptions; +use std::io::Read; +use std::fmt::{Debug, Result as FmtResult, Formatter}; +use std::ops::Deref; + +use failure::Fallible as Result; +use failure::Error; +use toml_query::read::TomlValueReadTypeExt; +use chrono::NaiveDateTime; +use mda::Email; use libimagstore::store::Entry; -use libimagentryutil::isa::Is; +use libimagstore::store::FileLockEntry; use libimagentryutil::isa::IsKindHeaderPathProvider; -use libimagentryref::reference::Config as RefConfig; -use libimagentryref::reference::{Ref, RefFassade}; -use libimagentrylink::linkable::Linkable; -use libimagstore::store::Store; -use libimagstore::storeid::StoreId; -use libimagstore::storeid::StoreIdIterator; -use libimagstore::iter::get::StoreIdGetIteratorExtension; - -use crate::mid::MessageId; -use crate::mailflags::MailFlag; -use crate::hasher::MailHasher; -use crate::iter::MailIterator; -use crate::iter::IntoMailIterator; +use libimagentryutil::isa::Is; + +use crate::notmuch::connection::NotmuchConnection; +use crate::notmuch::thread::Thread; +use crate::store::MailStoreWithConnection; provide_kindflag_path!(pub IsMail, "mail.is_mail"); -pub trait Mail : RefFassade + Linkable { - fn is_mail(&self) -> Result<bool>; - fn get_field(&self, refconfig: &RefConfig, field: &str) -> Result<Option<String>>; - fn get_from(&self, refconfig: &RefConfig) -> Result<Option<String>>; - fn get_to(&self, refconfig: &RefConfig) -> Result<Option<String>>; - fn get_subject(&self, refconfig: &RefConfig) -> Result<Option<String>>; - fn get_message_id(&self, refconfig: &RefConfig) -> Result<Option<MessageId>>; - fn get_in_reply_to(&self, refconfig: &RefConfig) -> Result<Option<MessageId>>; - - fn flags(&self, refconfig: &RefConfig) -> Result<Vec<MailFlag>>; - fn is_passed(&self, refconfig: &RefConfig) -> Result<bool>; - fn is_replied(&self, refconfig: &RefConfig) -> Result<bool>; - fn is_seen(&self, refconfig: &RefConfig) -> Result<bool>; - fn is_trashed(&self, refconfig: &RefConfig) -> Result<bool>; - fn is_draft(&self, refconfig: &RefConfig) -> Result<bool>; - fn is_flagged(&self, refconfig: &RefConfig) -> Result<bool>; - - fn neighbors(&self) -> Result<StoreIdIterator>; - fn get_neighbors<'a>(&self, store: &'a Store) -> Result<MailIterator<'a>>; - fn get_thread<'a>(&self, store: &'a Store) -> Result<MailIterator<'a>>; +pub trait Mail { + fn is_mail(&self) -> Result<bool>; + fn get_cached_id(&self) -> Result<String>; + + fn load(&self, connection: &NotmuchConnection) -> Result<Option<LoadedMail>>; } impl Mail for Entry { - fn is_mail(&self) -> Result<bool> { - self.is::<IsMail>() + self.is::<IsMail>().map_err(Error::from) } - /// Get a value of a single field of the mail file - fn get_field(&self, refconfig: &RefConfig, field: &str) -> Result<Option<String>> { - use std::fs::read_to_string; + fn get_cached_id(&self) -> Result<String> { + self.get_header() + .read_string("mail.id")? + .ok_or_else(|| format_err!("Cached ID missing for: {}", self.get_location())) + } - debug!("Getting field in mail: {:?}", field); - let mail_file_location = self.as_ref_with_hasher::<MailHasher>().get_path(refconfig)?; + fn load(&self, connection: &NotmuchConnection) -> Result<Option<LoadedMail>> { + LoadedMail::load(self, connection) + } +} - match ::mailparse::parse_mail(read_to_string(mail_file_location.as_path())?.as_bytes()) - .context(anyhow!("Cannot parse Email {}", mail_file_location.display()))? - .headers - .into_iter() - .filter_map(|hdr| { - match hdr.get_key() - .context(anyhow!("Cannot fetch key '{}' from Email {}", field, mail_file_location.display())) - .map_err(Error::from) - { - Ok(k) => if k == field { - Some(Ok(hdr)) - } else { - None - }, - Err(e) => Some(Err(e)), +#[derive(Debug)] +pub struct LoadedMail { + id: String, + thread_id: String, + files: i32, + filename: PathBuf, + filenames: Vec<PathBuf>, + date: NaiveDateTime, + tags: Vec<String>, +} + +impl LoadedMail { + + pub fn load(entry: &Entry, conn: &NotmuchConnection) -> Result<Option<Self>> { + let id = entry + .get_header() + .read_string("mail.id")? + .ok_or_else(|| format_err!("Missing header: 'mail.id' in {}", entry.get_location()))?; + + trace!("Loading {} with id: {}", entry.get_location(), id); + conn.execute(|db| { + if let Some(msg) = db.find_message(&id)? { + let id = msg.id().into_owned(); + let thread_id = msg.thread_id().into_owned(); + let files = msg.count_files(); + let filenames = msg.filenames().collect(); + let filename = msg.filename(); + let tags = msg.tags().collect(); + let date = NaiveDateTime::from_timestamp_opt(msg.date(), 0) + .ok_or_else(|| format_err!("Invalid timestamp: {}", msg.date()))?; + + let lm = LoadedMail { + id, + thread_id, + files, + filename, + filenames, + date, + tags, + }; + + trace!("Loaded {:?}", lm); + Ok(Some(lm)) + } else { + trace!("Loaded None"); + Ok(None) } }) - .next() - { - None => Ok(None), - Some(Err(e)) => Err(e), - Some(Ok(hdr)) => Ok(Some(hdr.get_value()?)) - } + } - /// Get a value of the `From` field of the mail file - /// - /// # Note - /// - /// Use `Mail::mail_header()` if you need to read more than one field. - fn get_from(&self, refconfig: &RefConfig) -> Result<Option<String>> { - self.get_field(refconfig, "From") - } - - /// Get a value of the `To` field of the mail file - /// - /// # Note - /// - /// Use `Mail::mail_header()` if you need to read more than one field. - fn get_to(&self, refconfig: &RefConfig) -> Result<Option<String>> { - self.get_field(refconfig, "To") - } - - /// Get a value of the `Subject` field of the mail file - /// - /// # Note - /// - /// Use `Mail::mail_header()` if you need to read more than one field. - fn get_subject(&self, refconfig: &RefConfig) -> Result<Option<String>> { - self.get_field(refconfig, "Subject") - } - - /// Get a value of the `Message-ID` field of the mail file - /// - /// # Note - /// - /// Use `Mail::mail_header()` if you need to read more than one field. - fn get_message_id(&self, refconfig: &RefConfig) -> Result<Option<MessageId>> { - if let Some(s) = self.get_header().read("mail.message-id")? { - let s = s.as_str() - .ok_or_else(|| anyhow!("'mail.message-id' is not a String in {}", self.get_location()))?; - Ok(Some(MessageId::from(String::from(s)))) - } else { - self.get_field(refconfig, "Message-ID") - .map(|o| o.map(crate::util::strip_message_delimiters).map(MessageId::from)) - } + pub fn load_entry<'a>(&self, store: &'a MailStoreWithConnection<'a>) -> Result<Option<FileLockEntry<'a>>> { + store.get_mail_by_id(self.get_id()) } - /// Get a value of the `In-Reply-To` field of the mail file - /// - /// # Note - /// - /// Use `Mail::mail_header()` if you need to read more than one field. - fn get_in_reply_to(&self, refconfig: &RefConfig) -> Result<Option<MessageId>> { - self.get_field(refconfig, "In-Reply-To") - .map(|o| o.map(crate::util::strip_message_delimiters).map(MessageId::from)) + pub fn get_id(&self) -> &String { + &self.id } - /// Get the flags of the message - fn flags(&self, refconfig: &RefConfig) -> Result<Vec<MailFlag>> { - let path = self.as_ref_with_hasher::<MailHasher>().get_path(refconfig)?; + pub fn get_thread_id(&self) -> &String { + &self.thread_id + } - if !path.exists() { - return Err(anyhow!("Path {} does not exist", path.display())) - } + pub fn get_files(&self) -> &i32 { + &self.files + } - { - // Now parse mail flags - path.to_str() - .ok_or_else(|| anyhow!("Path is not UTF-8: {}", path.display()))? - .split("2,") - .map(String::from) - .collect::<Vec<String>>() - .split_last() - .ok_or_else(|| anyhow!("Splitting path into prefix and flags failed: {}", path.display()))? - .0 - .chars() - .map(|c| c.to_string()) - .map(|c| MailFlag::from_str(&c)) - .collect::<Result<Vec<_>>>() - } + pub fn get_filename(&self) -> &PathBuf { + &self.filename } - /// Check whether the mail is passed - fn is_passed(&self, refconfig: &RefConfig) -> Result<bool> { - self.flags(refconfig).map(|fs| fs.into_iter().any(|f| MailFlag::Passed == f)) + pub fn get_filenames(&self) -> &Vec<PathBuf> { + &self.filenames } - /// Check whether the mail is replied - fn is_replied(&self, refconfig: &RefConfig) -> Result<bool> { - self.flags(refconfig).map(|fs| fs.into_iter().any(|f| MailFlag::Replied == f)) + pub fn get_date(&self) -> &NaiveDateTime { + &self.date } - /// Check whether the mail is seen - fn is_seen(&self, refconfig: &RefConfig) -> Result<bool> { - self.flags(refconfig).map(|fs| fs.into_iter().any(|f| MailFlag::Seen == f)) + pub fn get_tags(&self) -> &Vec<String> { + &self.tags } - /// Check whether the mail is trashed - fn is_trashed(&self, refconfig: &RefConfig) -> Result<bool> { - self.flags(refconfig).map(|fs| fs.into_iter().any(|f| MailFlag::Trashed == f)) + + // More convenience functionality + + pub fn threads(&self, conn: &NotmuchConnection) -> Result<Vec<Thread>> { + let id = self.get_id(); + conn.execute(|db| { + db.create_query(&format!("thread:{}", id))? + .search_threads() + .map_err(Error::from) + .map(|it| it.map(Thread::from).collect()) + }) } - /// Check whether the mail is draft - fn is_draft(&self, refconfig: &RefConfig) -> Result<bool> { - self.flags(refconfig).map(|fs| fs.into_iter().any(|f| MailFlag::Draft == f)) + pub fn replies(&self, conn: &NotmuchConnection) -> Result<Vec<String>> { + let id = self.get_id(); + conn.execute(|db| { + db.find_message(id)? + .ok_or_else(|| format_err!("Cannot find message with id {}", id))? + .replies() + .map(|msg| Ok(msg.id().into_owned())) + .collect() + }) } - /// Check whether the mail is flagged - fn is_flagged(&self, refconfig: &RefConfig) -> Result<bool> { - self.flags(refconfig).map(|fs| fs.into_iter().any(|f| MailFlag::Flagged == f)) + // Parsing the actual mail file for further processing + pub fn parsed(self) -> Result<ParsedMail> { + let mut buffer = Vec::with_capacity(4096); + OpenOptions::new() + .read(true) + .open(self.get_filename())? + .read_to_end(&mut buffer)?; + + Ok(ParsedMail { + loaded: self, + parsed: Email::from_vec(buffer).map_err(|e| format_err!("Parser error: {}", e.description()))?, + }) } +} - /// Get all direct neighbors for the Mail - /// - /// # Note - /// - /// This fetches only the neighbors which are linked. So it basically only checks the entries - /// which this entry is linked to and filters them for Mail::is_mail() - /// - /// # Warning - /// - /// Might yield store entries which are not a Mail in the Mail::is_mail() sence but are simply - /// stored in /mail in the store. - /// - /// To be sure, you should filter this iterator after getting the FileLockEntries from Store. - /// Or use `Mail::get_neighbors(&store)`. - /// - fn neighbors(&self) -> Result<StoreIdIterator> { - let iter = self - .links()? - .map(|link| link.into()) - .filter(|id: &StoreId| id.is_in_collection(&["mail"])) - .map(Ok); +pub struct ParsedMail { + loaded: LoadedMail, + parsed: Email, +} - Ok(StoreIdIterator::new(Box::new(iter))) - } +impl Deref for ParsedMail { + type Target = LoadedMail; - /// Get alldirect neighbors for the Mail (as FileLockEntry) - /// - /// # See also - /// - /// Documentation of `Mail::neighbors()`. - fn get_neighbors<'a>(&self, store: &'a Store) -> Result<MailIterator<'a>> { - self.links() - .map(|iter| { - iter.map(|link| link.into()) - .map(Ok) - .into_get_iter(store) - .into_mail_iterator() - }) + fn deref(&self) -> &Self::Target { + &self.loaded } +} - /// Get the full thread starting from this Mail - /// - /// This function recursively traverses the linked mails, assumes them all to be in the same - /// thread and returns an iterator over all Mails it finds in this way. - /// - /// # Warning - /// - /// If a Mail is linked to this mail (even transitively!) but is _not_ in the same thread, it - /// is considered to be in the same thread. - /// - /// This function works recursively. Keep that in mind for large threads. Because it needs to - /// collect() internally, it might take a lot of memory for large threads. - /// - /// # Return value - /// - /// This function returns an Iterator over StoreIds in the same thread as this mail itself. - /// It does not yield any qualification about the distance between a mail in this thread and - /// this very mail. - /// - fn get_thread<'a>(&self, store: &'a Store) -> Result<MailIterator<'a>> { - trace!("Getting thread, starting point at: {}", self.get_location()); - let mut thread = vec![self.get_location().clone()]; - - fn traverse<'a>(entry: &'a Entry, thread: &mut Vec<StoreId>, store: &Store) -> Result<()> { - // Helper function to get neighbors of a Mail, but filtered - fn get_filtered_neighbors<'a>(entry: &'a Entry, skiplist: &[StoreId]) -> Result<Vec<StoreId>> { - trace!("Getting filtered neighbors of {}", entry.get_location()); - entry.neighbors()?.filter_ok(|id| !skiplist.contains(id)).collect() - } +impl Debug for ParsedMail { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "ParsedMail {{ {:?}, ... }}", self.loaded) + } +} - // Get the neighbors, filtered by StoreIds which are already in the thread - // Then iterate over them - for n in get_filtered_neighbors(entry, thread)? { - trace!("Fetching {}", n); +impl ParsedMail { - // Get the FileLockEntry for the StoreId, or fail if it cannot be found - let next_entry = store.get(n.clone())?.ok_or_else(|| anyhow!("Cannot find {}", n))?; + pub fn get_keys(&self) -> Result<Vec<String>> { + Ok(self.parsed.header_keys().into_iter().map(ToOwned::to_owned).collect()) + } - // if the FileLockEntry is a Mail - if next_entry.is_mail()? { - |