From a819aeb6edc1778ea3ace256a15da698777fa81c Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 15 Dec 2018 02:44:32 +0100 Subject: [PATCH] Rewrite libimagmail Signed-off-by: Matthias Beyer --- lib/domain/libimagmail/Cargo.toml | 14 +- lib/domain/libimagmail/src/config.rs | 136 +++++++++ lib/domain/libimagmail/src/fetch.rs | 117 ++++++++ .../libimagmail/src/{iter.rs => hasher.rs} | 41 +-- lib/domain/libimagmail/src/lib.rs | 19 +- lib/domain/libimagmail/src/mail.rs | 273 ++++++++---------- lib/domain/libimagmail/src/mid.rs | 33 +++ lib/domain/libimagmail/src/send.rs | 111 +++++++ lib/domain/libimagmail/src/store.rs | 151 ++++++++++ lib/domain/libimagmail/src/util.rs | 64 ++++ 10 files changed, 778 insertions(+), 181 deletions(-) create mode 100644 lib/domain/libimagmail/src/config.rs create mode 100644 lib/domain/libimagmail/src/fetch.rs rename lib/domain/libimagmail/src/{iter.rs => hasher.rs} (52%) create mode 100644 lib/domain/libimagmail/src/mid.rs create mode 100644 lib/domain/libimagmail/src/send.rs create mode 100644 lib/domain/libimagmail/src/store.rs create mode 100644 lib/domain/libimagmail/src/util.rs diff --git a/lib/domain/libimagmail/Cargo.toml b/lib/domain/libimagmail/Cargo.toml index 294040f3..09825f1e 100644 --- a/lib/domain/libimagmail/Cargo.toml +++ b/lib/domain/libimagmail/Cargo.toml @@ -21,10 +21,16 @@ maintenance = { status = "actively-developed" } [dependencies] log = "0.4.0" -email = "0.0.20" +toml = "0.4" +toml-query = "0.8" +mailparse = "0.6.5" filters = "0.3" failure = "0.1" +serde = "1" +serde_derive = "1" + +libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" } +libimagerror = { version = "0.10.0", path = "../../../lib/core/libimagerror" } +libimagentryref = { version = "0.10.0", path = "../../../lib/entry/libimagentryref" } +libimagentryutil = { version = "0.10.0", path = "../../../lib/entry/libimagentryutil/" } -libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" } -libimagerror = { version = "0.10.0", path = "../../../lib/core/libimagerror" } -libimagentryref = { version = "0.10.0", path = "../../../lib/entry/libimagentryref" } diff --git a/lib/domain/libimagmail/src/config.rs b/lib/domain/libimagmail/src/config.rs new file mode 100644 index 00000000..6258e6e3 --- /dev/null +++ b/lib/domain/libimagmail/src/config.rs @@ -0,0 +1,136 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer 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, + fetchcommand : MailCommand, + postfetchcommand : Option, + sendcommand : MailCommand, + postsendcommand : Option, +} + +impl MailConfig { + pub fn default_account(&self) -> &String { + &self.default_account + } + + pub fn accounts(&self) -> &Vec { + &self.accounts + } + + pub fn account(&self, name: &str) -> Option<&MailAccountConfig> { + self.accounts() + .iter() + .filter(|a| a.name == name) + .next() + } + + 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() + .filter(|a| a.name == account_name) + .next() + .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() + .filter(|a| a.name == account_name) + .next() + .and_then(|a| a.postfetchcommand.as_ref()) + .or_else(|| self.postfetchcommand()) + } + + pub fn sendcommand_for_account(&self, account_name: &str) -> &MailCommand { + self.accounts() + .iter() + .filter(|a| a.name == account_name) + .next() + .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() + .filter(|a| a.name == account_name) + .next() + .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, + pub postfetchcommand : Option, + pub sendcommand : Option, + pub postsendcommand : Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct MailCommand { + command: String, + env: Vec, + args: Vec, +} + diff --git a/lib/domain/libimagmail/src/fetch.rs b/lib/domain/libimagmail/src/fetch.rs new file mode 100644 index 00000000..de4f4b0e --- /dev/null +++ b/lib/domain/libimagmail/src/fetch.rs @@ -0,0 +1,117 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer 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, + boxes: Vec, + + 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(mut self, names: I) -> Self + where I: IntoIterator + { + 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(|| format_err!("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/hasher.rs similarity index 52% rename from lib/domain/libimagmail/src/iter.rs rename to lib/domain/libimagmail/src/hasher.rs index e4d375cd..270b1777 100644 --- a/lib/domain/libimagmail/src/iter.rs +++ b/lib/domain/libimagmail/src/hasher.rs @@ -17,39 +17,24 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // -//! Module for the MailIter -//! -//! MailIter is a iterator which takes an Iterator that yields `Ref` and yields itself -//! `Result`, where `Err(_)` is returned if the Ref is not a Mail or parsing of the -//! referenced mail file failed. -//! +use std::path::Path; -use mail::Mail; use failure::Fallible as Result; -use libimagstore::store::FileLockEntry; +use libimagentryref::hasher::Hasher; -use std::marker::PhantomData; +pub struct MailHasher; -pub struct MailIter<'a, I: Iterator>> { - _marker: PhantomData, - i: I, -} +impl Hasher for MailHasher { + const NAME: &'static str = "MailHasher"; -impl<'a, I: Iterator>> MailIter<'a, I> { - - pub fn new(i: I) -> MailIter<'a, I> { - MailIter { _marker: PhantomData, i: i } + /// hash the file at path `path` + /// + /// TODO: This is the expensive implementation. We use the message Id as hash, which is + /// convenient and _should_ be safe + /// + /// TODO: Confirm that this approach is right + fn hash>(path: P) -> Result { + ::util::get_message_id_for_mailfile(path) } - } - -impl<'a, I: Iterator>> Iterator for MailIter<'a, I> { - type Item = Result>; - - fn next(&mut self) -> Option { - self.i.next().map(Mail::from_fle) - } - -} - diff --git a/lib/domain/libimagmail/src/lib.rs b/lib/domain/libimagmail/src/lib.rs index 3ba7397f..5bbeeb33 100644 --- a/lib/domain/libimagmail/src/lib.rs +++ b/lib/domain/libimagmail/src/lib.rs @@ -38,14 +38,25 @@ )] #[macro_use] extern crate log; -extern crate email; +extern crate mailparse; +extern crate toml; +extern crate toml_query; extern crate filters; -extern crate failure; +#[macro_use] extern crate failure; +extern crate serde; +#[macro_use] extern crate serde_derive; extern crate libimagerror; -extern crate libimagstore; +#[macro_use] extern crate libimagstore; extern crate libimagentryref; +#[macro_use] extern crate libimagentryutil; -pub mod iter; +module_entry_path_mod!("mail"); + +pub mod config; +pub mod hasher; pub mod mail; +pub mod mid; +pub mod store; +pub mod util; diff --git a/lib/domain/libimagmail/src/mail.rs b/lib/domain/libimagmail/src/mail.rs index ab59a694..b6429446 100644 --- a/lib/domain/libimagmail/src/mail.rs +++ b/lib/domain/libimagmail/src/mail.rs @@ -17,185 +17,168 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // -use std::path::Path; -use std::fs::File; -use std::io::Read; -use std::fs::OpenOptions; - -use libimagstore::store::Store; -use libimagstore::storeid::StoreId; -use libimagstore::store::FileLockEntry; -use libimagentryref::reference::Ref; -use libimagentryref::refstore::RefStore; -use libimagentryref::refstore::UniqueRefPathGenerator; -use libimagerror::errors::ErrorMsg as EM; - -use email::MimeMessage; -use email::results::ParsingResult as EmailParsingResult; - use failure::Fallible as Result; use failure::ResultExt; use failure::Error; -use failure::err_msg; -struct UniqueMailRefGenerator; -impl UniqueRefPathGenerator for UniqueMailRefGenerator { - /// The collection the `StoreId` should be created for - fn collection() -> &'static str { - "mail" - } +use libimagstore::store::Entry; +use libimagentryutil::isa::Is; +use libimagentryutil::isa::IsKindHeaderPathProvider; +use libimagentryref::reference::Config as RefConfig; +use libimagentryref::reference::{Ref, RefFassade}; - /// A function which should generate a unique string for a Path - fn unique_hash>(path: A) -> Result { - use filters::filter::Filter; - use email::Header; +provide_kindflag_path!(pub IsMail, "mail.is_mail"); - let mut s = String::new(); - let _ = OpenOptions::new() - .read(true) - .write(false) - .create(false) - .open(path)? - .read_to_string(&mut s)?; - - MimeMessage::parse(&s) - .context(err_msg("Error creating ref")) - .map_err(Error::from) - .and_then(|mail| { - let has_key = |hdr: &Header, exp: &str| hdr.name == exp; - - let subject_filter = |hdr: &Header| has_key(hdr, "Subject"); - let from_filter = |hdr: &Header| has_key(hdr, "From"); - let to_filter = |hdr: &Header| has_key(hdr, "To"); - - let filter = subject_filter.or(from_filter).or(to_filter); - - let mut v : Vec = vec![]; - for hdr in mail.headers.iter().filter(|item| filter.filter(item)) { - let s = hdr - .get_value() - .context(err_msg("Ref creation error"))?; - - v.push(s); - } - let s : String = v.join(""); - Ok(s) - }) - } - - /// Postprocess the generated `StoreId` object - fn postprocess_storeid(sid: StoreId) -> Result { - Ok(sid) - } +pub trait Mail : RefFassade { + fn is_mail(&self) -> Result; + fn get_field(&self, refconfig: &RefConfig, field: &str) -> Result>; + fn get_from(&self, refconfig: &RefConfig) -> Result>; + fn get_to(&self, refconfig: &RefConfig) -> Result>; + fn get_subject(&self, refconfig: &RefConfig) -> Result>; + fn get_message_id(&self, refconfig: &RefConfig) -> Result>; + fn get_in_reply_to(&self, refconfig: &RefConfig) -> Result>; } -struct Buffer(String); +impl Mail for Entry { -impl Buffer { - pub fn parsed(&self) -> EmailParsingResult { - MimeMessage::parse(&self.0) - } -} - -impl From for Buffer { - fn from(data: String) -> Buffer { - Buffer(data) - } -} - -pub struct Mail<'a>(FileLockEntry<'a>, Buffer); - -impl<'a> Mail<'a> { - - /// Imports a mail from the Path passed - pub fn import_from_path>(store: &Store, p: P) -> Result { - debug!("Importing Mail from path"); - store.retrieve_ref::(p) - .and_then(|reference| { - debug!("Build reference file: {:?}", reference); - reference.get_path() - .context(err_msg("Ref handling error")) - .map_err(Error::from) - .and_then(|path| File::open(path).context(EM::IO).map_err(Error::from)) - .and_then(|mut file| { - let mut s = String::new(); - file.read_to_string(&mut s) - .map(|_| s) - .context(EM::IO) - .map_err(Error::from) - }) - .map(Buffer::from) - .map(|buffer| Mail(reference, buffer)) - }) + fn is_mail(&self) -> Result { + self.is::() } - /// Opens a mail by the passed hash - pub fn open>(store: &Store, hash: S) -> Result> { - debug!("Opening Mail by Hash"); - store.get_ref::(hash) - .context(err_msg("Fetch by hash error")) - .context(err_msg("Fetch error")) - .map_err(Error::from) - .and_then(|o| match o { - Some(r) => Mail::from_fle(r).map(Some), - None => Ok(None), - }) - } + /// Get a value of a single field of the mail file + fn get_field(&self, refconfig: &RefConfig, field: &str) -> Result> { + use std::fs::read_to_string; + use hasher::MailHasher; - /// Implement me as TryFrom as soon as it is stable - pub fn from_fle(fle: FileLockEntry<'a>) -> Result> { - fle.get_path() - .context(err_msg("Ref handling error")) - .map_err(Error::from) - .and_then(|path| File::open(path).context(EM::IO).map_err(Error::from)) - .and_then(|mut file| { - let mut s = String::new(); - file.read_to_string(&mut s) - .map(|_| s) - .context(EM::IO) - .map_err(Error::from) - }) - .map(Buffer::from) - .map(|buffer| Mail(fle, buffer)) - } - - pub fn get_field(&self, field: &str) -> Result> { debug!("Getting field in mail: {:?}", field); - self.1 - .parsed() - .context(err_msg("Mail parsing error")) - .map_err(Error::from) - .map(|parsed| { - parsed.headers - .iter() - .filter(|hdr| hdr.name == field) - .nth(0) - .and_then(|field| field.get_value().ok()) + let mail_file_location = self.as_ref_with_hasher::().get_path(refconfig)?; + + match ::mailparse::parse_mail(read_to_string(mail_file_location.as_path())?.as_bytes()) + .context(format_err!("Cannot parse Email {}", mail_file_location.display()))? + .headers + .into_iter() + .filter_map(|hdr| match hdr.get_key() { + Err(e) => Some(Err(e).map_err(Error::from)), + Ok(k) => if k == field { + Some(Ok(hdr)) + } else { + 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> { + 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> { + 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> { + 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> { + self.get_field(refconfig, "Message-ID") + .map(|o| o.map(::util::strip_message_delimiters)) + } + + /// 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> { + self.get_field(refconfig, "In-Reply-To") + } + +} + +#[derive(Debug)] +pub struct MailHeader<'a>(Vec<::mailparse::MailHeader<'a>>); + +impl<'a> From>> for MailHeader<'a> { + fn from(mh: Vec<::mailparse::MailHeader<'a>>) -> Self { + MailHeader(mh) + } +} + +impl<'a> MailHeader<'a> { + /// Get a value of a single field of the mail file + pub fn get_field(&self, field: &str) -> Result> { + match self.0 + .iter() + .filter_map(|hdr| match hdr.get_key() { + Err(e) => Some(Err(e).map_err(Error::from)), + Ok(key) => if key == field { + Some(Ok(hdr)) + } else { + 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 pub fn get_from(&self) -> Result> { self.get_field("From") } + /// Get a value of the `To` field of the mail file pub fn get_to(&self) -> Result> { self.get_field("To") } + /// Get a value of the `Subject` field of the mail file pub fn get_subject(&self) -> Result> { self.get_field("Subject") } + /// Get a value of the `Message-ID` field of the mail file pub fn get_message_id(&self) -> Result> { self.get_field("Message-ID") } + /// Get a value of the `In-Reply-To` field of the mail file pub fn get_in_reply_to(&self) -> Result> { self.get_field("In-Reply-To") } - pub fn fle(&self) -> &FileLockEntry<'a> { - &self.0 - } - + // TODO: Offer functionality to load and parse mail _once_ from disk, and then use helper object + // to offer access to header fields and content. + // + // With the existing functionality, one has to open-parse-close the file all the time, which is + // _NOT_ optimal. } diff --git a/lib/domain/libimagmail/src/mid.rs b/lib/domain/libimagmail/src/mid.rs new file mode 100644 index 00000000..ec732027 --- /dev/null +++ b/lib/domain/libimagmail/src/mid.rs @@ -0,0 +1,33 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer 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 +// + +/// Helper type for handling message IDs +/// +/// Message IDs are used to identfy emails uniquely, so we should at least have a type for +/// representing them and make handling a bit easier. +/// +#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct MessageId(String); + +impl Into for MessageId { + fn into(self) -> String { + self.0 + } +} + diff --git a/lib/domain/libimagmail/src/send.rs b/lib/domain/libimagmail/src/send.rs new file mode 100644 index 00000000..9c4c5407 --- /dev/null +++ b/lib/domain/libimagmail/src/send.rs @@ -0,0 +1,111 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer 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 MailSender<'a> { + config: &'a MailConfig, + account_name_to_send_with: Option, + + rescan_maildirs: bool, +} + +impl MailSender { + pub fn new(config: &MailConfig) -> Self { + MailSender { + config, + account_name_to_send_with: None, + rescan_maildirs: false + } + } + + pub fn send_account(mut self, name: String) -> Self { + self.account_name_to_send_with = Some(name); + self + } + + pub fn rescan_maildirs(mut self, b: bool) -> Self { + self.rescan_maildirs = b; + self + } + + pub fn run(&self, store: &Store) -> Result<()> { + let sendcommand = match self.account_name_to_send_with { + Some(name) => self.config.sendcommand_for_account(name), + None => self.confnig.sendcommand(), + }; + + let postsendcommand = match self.account_name_to_send_with { + Some(name) => self.config.postsendcommand_for_account(name), + None => self.confnig.sendcommand(), + }; + + let account = config + .account(self.account_name_to_send_with) + .ok_or_else(|| format_err!("Account '{}' does not exist", self.account_name_to_send_with))?; + + if sendcommand.contains(" ") { + // error on whitespace in command + } + + if postsendcommand.contains(" ") { + // error on whitespace in command + } + + // sendcommand + // + let outgoingbox = account + .outgoingbox + .to_str() + .ok_or_else(|| format_err!("Cannot use '{:?}' as outgoingbox", account.outgoingbox))?; + + let mut output = Command::new(sendcommand) + // TODO: Add argument support + // TODO: Add support for passing config variables + // TODO: Add support for passing environment + .arg(outgoingbox) + .wait_with_output() + .context("Mail sending")?; + + write!(rt.stdout(), "{}", output.stdout)?; + write!(rt.stderr(), "{}", output.stderr)?; + + // TODO: Move all files in outgoingbox to account.sentbox + + // postfetchcommand + + let output = Command::new(postsendcommand) + // TODO: Add argument support + // TODO: Add support for passing config variables + .wait_with_output() + .context("Post 'Mail sending' 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/store.rs b/lib/domain/libimagmail/src/store.rs new file mode 100644 index 00000000..6c672d9e --- /dev/null +++ b/lib/domain/libimagmail/src/store.rs @@ -0,0 +1,151 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer 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::Path; +use std::path::PathBuf; +use std::fmt::Debug; + +use failure::Fallible as Result; +use toml::Value; +use toml_query::insert::TomlValueInsertExt; + +use libimagstore::store::FileLockEntry; +use libimagstore::store::Store; +use libimagstore::storeid::IntoStoreId; +use libimagstore::storeid::StoreId; +use libimagstore::iter::Entries; +use libimagentryref::hasher::default::DefaultHasher; +use libimagentryref::reference::Config; +use libimagentryref::reference::RefFassade; +use libimagentryref::reference::Ref; +use libimagentryref::reference::MutRef; + +use module_path::ModuleEntryPath; +use mid::MessageId; +use mail::Mail; +use hasher::MailHasher; +use util::get_message_id_for_mailfile; + +pub trait MailStore<'a> { + fn create_mail_from_path(&'a self, p: P, collection_name: CollName, config: &Config) + -> Result> + where P: AsRef + Debug, + CollName: AsRef + Debug; + + fn get_mail_from_path

(&'a self, p: P) + -> Result>> + where P: AsRef + Debug; + + fn retrieve_mail_from_path(&'a self, p: P, collection_name: CollName, config: &Config) + -> Result> + where P: AsRef + Debug, + CollName: AsRef + Debug; + + fn get_mail(&'a self, mid: MessageId) -> Result>>; + fn all_mails(&'a self) -> Result>; +} + +impl<'a> MailStore<'a> for Store { + + fn create_mail_from_path(&'a self, p: P, collection_name: CollName, config: &Config) + -> Result> + where P: AsRef + Debug, + CollName: AsRef + Debug + { + let message_id = get_message_id_for_mailfile(p.as_ref())?; + let new_sid = ModuleEntryPath::new(message_id.clone()).into_storeid()?; + + let mut entry = self.create(new_sid)?; + let _ = entry + .as_ref_with_hasher_mut::() + .make_ref(p, collection_name, config, false)?; + + let _ = entry + .get_header_mut() + .insert("mail.message-id", Value::String(message_id))?; + + Ok(entry) + } + + /// Same as MailStore::retrieve_mail_from_path() but uses Store::get() instead of + /// Store::retrieve() + fn get_mail_from_path

(&'a self, p: P) + -> Result>> + where P: AsRef + Debug + { + let message_id = get_message_id_for_mailfile(p.as_ref())?; + let new_sid = ModuleEntryPath::new(message_id.clone()).into_storeid()?; + + match self.get(new_sid)? { + Some(mut entry) => { + if !entry.is_ref()? { + return Err(format_err!("{} is not a ref", entry.get_location())) + } + + if p.as_ref().ends_with(entry.as_ref_with_hasher::().get_relative_path()?) { + return Err(format_err!("{} is not a ref to {:?}", + entry.get_location(), + p.as_ref().display())) + } + + let _ = entry.get_header_mut().insert("mail.message-id", Value::String(message_id))?; + Ok(Some(entry)) + }, + None => Ok(None), + } + } + + fn retrieve_mail_from_path(&'a self, p: P, collection_name: CollName, config: &Config) + -> Result> + where P: AsRef + Debug, + CollName: AsRef + Debug + { + let message_id = get_message_id_for_mailfile(&p)?; + let new_sid = ModuleEntryPath::new(message_id.clone()).into_storeid()?; + let mut entry = self.retrieve(new_sid)?; + + let _ = entry + .get_header_mut() + .insert("mail.message-id", Value::String(message_id))?; + + let _ = entry + .as_ref_with_hasher_mut::() + .make_ref(p, collection_name, config, false)?; + + Ok(entry) + } + + fn get_mail(&'a self, mid: MessageId) -> Result>> { + let mid_s : String = mid.into(); + self.get(StoreId::new(PathBuf::from(mid_s))?) + .and_then(|oe| match oe { + Some(e) => if e.is_mail()? { + Ok(Some(e)) + } else { + Err(format_err!("{} is not a mail entry", e.get_location())) + }, + None => Ok(None) + }) + } + + fn all_mails(&'a self) -> Result> { + self.entries().map(|ent| ent.in_collection("mail")) + } +} + diff --git a/lib/domain/libimagmail/src/util.rs b/lib/domain/libimagmail/src/util.rs new file mode 100644 index 00000000..8b51a9f8 --- /dev/null +++ b/lib/domain/libimagmail/src/util.rs @@ -0,0 +1,64 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer 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::Path; + +use failure::Error; +use failure::Fallible as Result; +use failure::ResultExt; + +pub(crate) fn get_message_id_for_mailfile>(p: P) -> Result { + ::mailparse::parse_mail(::std::fs::read_to_string(p.as_ref())?.as_bytes()) + .context(format_err!("Cannot parse Email {}", p.as_ref().display()))? + .headers + .into_iter() + .filter_map(|hdr| match hdr.get_key() { + Err(e) => Some(Err(e).map_err(Error::from)), + Ok(k) => if k.to_lowercase() == "message-id" { + Some(Ok(hdr)) + } else { + None + } + }) + .next() + .ok_or_else(|| format_err!("Message Id not found in {}", p.as_ref().display()))? + .and_then(|hdr| hdr.get_value().map_err(Error::from)) + .map(strip_message_delimiters) +} + +/// Strips message delimiters ('<' and '>') from a Message-ID field. +pub(crate) fn strip_message_delimiters>(id: ID) -> String { + let len = id.as_ref().len(); + // We have to strip the '<' and '>' if there are any, because they do not belong to the + // Message-Id at all + id.as_ref() + .chars() + .enumerate() + .filter(|(idx, chr)| !(*idx == 0 && *chr == '<' || *idx == len - 1 && *chr == '>')) + .map(|tpl| tpl.1) + .collect() +} + +pub fn get_mail_text_content>(p: P) -> Result { + ::mailparse::parse_mail(::std::fs::read_to_string(p.as_ref())?.as_bytes()) + .context(format_err!("Cannot parse Email {}", p.as_ref().display()))? + .get_body() + .map_err(Error::from) +} +