Rewrite libimagmail

Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
This commit is contained in:
Matthias Beyer 2018-12-15 02:44:32 +01:00
parent 198c59f717
commit a819aeb6ed
10 changed files with 778 additions and 181 deletions

View file

@ -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" }

View file

@ -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>,
}

View file

@ -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
}
}
}

View file

@ -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 Hasher for MailHasher {
const NAME: &'static str = "MailHasher";
impl<'a, I: Iterator<Item = FileLockEntry<'a>>> 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<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)
}
}

View file

@ -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;

View file

@ -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<A: AsRef<Path>>(path: A) -> Result<String> {
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<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);
}
let s : String = v.join("");
Ok(s)
})
}
/// Postprocess the generated `StoreId` object
fn postprocess_storeid(sid: StoreId) -> Result<StoreId> {
Ok(sid)
}
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>>;
}
struct Buffer(String);
impl Mail for Entry {
impl Buffer {
pub fn parsed(&self) -> EmailParsingResult<MimeMessage> {
MimeMessage::parse(&self.0)
}
}
impl From<String> 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<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))
})
fn is_mail(&self) -> Result<bool> {
self.is::<IsMail>()
}
/// 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 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;
/// 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))
}
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())
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
}
})
.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<String>> {
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<Option<String>> {
self.get_field(refconfig, "In-Reply-To")
}
}
#[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>> {
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.
}

View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -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"))
}
}

View file

@ -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)
}