Rewrite libimagmail
Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
This commit is contained in:
parent
198c59f717
commit
a819aeb6ed
10 changed files with 778 additions and 181 deletions
|
@ -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/" }
|
||||
|
||||
|
|
136
lib/domain/libimagmail/src/config.rs
Normal file
136
lib/domain/libimagmail/src/config.rs
Normal 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>,
|
||||
}
|
||||
|
117
lib/domain/libimagmail/src/fetch.rs
Normal file
117
lib/domain/libimagmail/src/fetch.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
|
33
lib/domain/libimagmail/src/mid.rs
Normal file
33
lib/domain/libimagmail/src/mid.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
111
lib/domain/libimagmail/src/send.rs
Normal file
111
lib/domain/libimagmail/src/send.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
151
lib/domain/libimagmail/src/store.rs
Normal file
151
lib/domain/libimagmail/src/store.rs
Normal 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"))
|
||||
}
|
||||
}
|
||||
|
64
lib/domain/libimagmail/src/util.rs
Normal file
64
lib/domain/libimagmail/src/util.rs
Normal 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)
|
||||
}
|
||||
|
Loading…
Reference in a new issue