summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Beyer <mail@beyermatthias.de>2018-12-15 02:44:32 +0100
committerMatthias Beyer <mail@beyermatthias.de>2019-02-20 16:22:52 +0100
commita819aeb6edc1778ea3ace256a15da698777fa81c (patch)
tree6a0f22624940ea35c6adc5a26cc13a29c130b626
parent198c59f717a2c3b2dff8c9d9d5a7a1f45684ceda (diff)
downloadimag-a819aeb6edc1778ea3ace256a15da698777fa81c.zip
imag-a819aeb6edc1778ea3ace256a15da698777fa81c.tar.gz
Rewrite libimagmail
Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
-rw-r--r--lib/domain/libimagmail/Cargo.toml14
-rw-r--r--lib/domain/libimagmail/src/config.rs136
-rw-r--r--lib/domain/libimagmail/src/fetch.rs117
-rw-r--r--lib/domain/libimagmail/src/hasher.rs (renamed from lib/domain/libimagmail/src/iter.rs)41
-rw-r--r--lib/domain/libimagmail/src/lib.rs19
-rw-r--r--lib/domain/libimagmail/src/mail.rs253
-rw-r--r--lib/domain/libimagmail/src/mid.rs33
-rw-r--r--lib/domain/libimagmail/src/send.rs111
-rw-r--r--lib/domain/libimagmail/src/store.rs151
-rw-r--r--lib/domain/libimagmail/src/util.rs64
10 files changed, 768 insertions, 171 deletions
diff --git a/lib/domain/libimagmail/Cargo.toml b/lib/domain/libimagmail/Cargo.toml
index 294040f..09825f1 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 0000000..6258e6e
--- /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 <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()
+ .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<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
new file mode 100644
index 0000000..de4f4b0
--- /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 <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(|| 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
index e4d375c..270b177 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<Mail>`, 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<Item = FileLockEntry<'a>>> {
- _marker: PhantomData<I>,
- i: I,
-}
-
-impl<'a, I: Iterator<Item = FileLockEntry<'a>>> MailIter<'a, I> {
+impl Hasher for MailHasher {
+ const NAME: &'static str = "MailHasher";
- 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<P: AsRef<Path>>(path: P) -> Result<String> {
+ ::util::get_message_id_for_mailfile(path)
}
-
}
-
-impl<'a, I: Iterator<Item = FileLockEntry<'a>>> Iterator for MailIter<'a, I> {
- type Item = Result<Mail<'a>>;
-
- fn next(&mut self) -> Option<Self::Item> {
- self.i.next().map(Mail::from_fle)
- }
-
-}
-
diff --git a/lib/domain/libimagmail/src/lib.rs b/lib/domain/libimagmail/src/lib.rs
index 3ba7397..5bbeeb3 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 ab59a69..b642944 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};
+
+provide_kindflag_path!(pub IsMail, "mail.is_mail");
+
+pub trait Mail : RefFassade {
+ 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<String>>;
+ fn get_in_reply_to(&self, refconfig: &RefConfig) -> Result<Option<String>>;
+}
+
+impl Mail for Entry {
+
+ fn is_mail(&self) -> Result<bool> {
+ self.is::<IsMail>()
}
- /// A function which should generate a unique string for a Path
- fn unique_hash<A: AsRef<Path>>(path: A) -> Result<String> {
- use filters::filter::Filter;
- use email::Header;
-
- 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<String> = 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);
+ /// 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;
+ use hasher::MailHasher;
+
+ debug!("Getting field in mail: {:?}", field);
+ let mail_file_location = self.as_ref_with_hasher::<MailHasher>().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
}
- let s : String = v.join("");
- Ok(s)
})
+ .next()
+ {
+ None => Ok(None),
+ Some(Err(e)) => Err(e),
+ Some(Ok(hdr)) => Ok(Some(hdr.get_value()?))
+ }
}
- /// Postprocess the generated `StoreId` object
- fn postprocess_storeid(sid: StoreId) -> Result<StoreId> {
- Ok(sid)
+ /// 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")
}
-}
-
-struct Buffer(String);
-impl Buffer {
- pub fn parsed(&self) -> EmailParsingResult<MimeMessage> {
- MimeMessage::parse(&self.0)
+ /// 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")
}
-}
-impl From<String> for Buffer {
- fn from(data: String) -> Buffer {
- Buffer(data)
+ /// 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")
}
-}
-pub struct Mail<'a>(FileLockEntry<'a>, Buffer);
-
-impl<'a> Mail<'a> {
-
- /// Imports a mail from the Path passed
- pub fn import_from_path<P: AsRef<Path>>(store: &Store, p: P) -> Result<Mail> {
- debug!("Importing Mail from path");
- store.retrieve_ref::<UniqueMailRefGenerator, P>(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))
- })
+ /// 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<String>> {
+ self.get_field(refconfig, "Message-ID")
+ .map(|o| o.map(::util::strip_message_delimiters))
}
- /// Opens a mail by the passed hash
- pub fn open<S: AsRef<str>>(store: &Store, hash: S) -> Result<Option<Mail>> {
- debug!("Opening Mail by Hash");
- store.get_ref::<UniqueMailRefGenerator, S>(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 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<String>> {
+ self.get_field(refconfig, "In-Reply-To")
}
- /// Implement me as TryFrom as soon as it is stable
- pub fn from_fle(fle: FileLockEntry<'a>) -> Result<Mail<'a>> {
- 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))
+}
+
+#[derive(Debug)]
+pub struct MailHeader<'a>(Vec<::mailparse::MailHeader<'a>>);
+
+impl<'a> From<Vec<::mailparse::MailHeader<'a>>> 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<Option<String>> {
- 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())
+ 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<Option<String>> {
self.get_field("From")
}
+ /// Get a value of the `To` field of the mail file
pub fn get_to(&self) -> Result<Option<String>> {
self.get_field("To")
}
+ /// Get a value of the `Subject` field of the mail file
pub fn get_subject(&self) -> Result<Option<String>> {
self.get_field("Subject")
}
+ /// Get a value of the `Message-ID` field of the mail file
pub fn get_message_id(&self) -> Result<Option<String>> {
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<Option<String>> {
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 0000000..ec73202
--- /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 <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
+//
+
+/// 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<String> 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 0000000..9c4c540
--- /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 <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 MailSender<'a> {
+ config: &'a MailConfig,
+ account_name_to_send_with: Option<String>,
+
+ 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 0000000..6c672d9
--- /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 <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::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<P, CollName>(&'a self, p: P, collection_name: CollName, config: &Config)
+ -> Result<FileLockEntry<'a>>
+ where P: AsRef<Path> + Debug,
+ CollName: AsRef<str> + Debug;
+
+ fn get_mail_from_path<P>(&'a self, p: P)
+ -> Result<Option<FileLockEntry<'a>>>
+ where P: AsRef<Path> + Debug;
+
+ fn retrieve_mail_from_path<P, CollName>(&'a self, p: P, collection_name: CollName, config: &Config)
+ -> Result<FileLockEntry<'a>>
+ where P: AsRef<Path> + Debug,
+ CollName: AsRef<str> + Debug;
+
+ fn get_mail(&'a self, mid: MessageId) -> Result<Option<FileLockEntry<'a>>>;
+ fn all_mails(&'a self) -> Result<Entries<'a>>;
+}
+
+impl<'a> MailStore<'a> for Store {
+
+ fn create_mail_from_path<P, CollName>(&'a self, p: P, collection_name: CollName, config: &Config)
+ -> Result<FileLockEntry<'a>>
+ where P: AsRef<Path> + Debug,
+ CollName: AsRef<str> + 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::<MailHasher>()
+ .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<P>(&'a self, p: P)
+ -> Result<Option<FileLockEntry<'a>>>
+ where P: AsRef<Path> + 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::<MailHasher>().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<P, CollName>(&'a self, p: P, collection_name: CollName, config: &Config)
+ -> Result<FileLockEntry<'a>>
+ where P: AsRef<Path> + Debug,
+ CollName: AsRef<str> + 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::<DefaultHasher>()
+ .make_ref(p, collection_name, config, false)?;
+
+ Ok(entry)
+ }
+
+ fn get_mail(&'a self, mid: MessageId) -> Result<Option<FileLockEntry<'a>>> {
+ 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<Entries<'a>> {
+ 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 0000000..8b51a9f
--- /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 <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::Path;
+
+use failure::Error;
+use failure::Fallible as Result;
+use failure::ResultExt;
+
+pub(crate) fn get_message_id_for_mailfile<P: AsRef<Path>>(p: P) -> Result<String> {
+ ::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: AsRef<str>>(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: AsRef<Path>>(p: P) -> Result<String> {
+ ::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)
+}
+