summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias Beyer <mail@beyermatthias.de>2020-04-18 11:18:07 +0200
committerMatthias Beyer <mail@beyermatthias.de>2020-06-01 14:01:39 +0200
commit70f974ee7ab1d15a200d625878bd8b7db861bb98 (patch)
treeb6eca3d13c7f1f6223add6005a5c9c4f972ccf55
parent908972af726bc46089b4f5d92a53ff206c088657 (diff)
Add implementation for crafting new mail
Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
-rw-r--r--bin/domain/imag-mail/Cargo.toml4
-rw-r--r--bin/domain/imag-mail/src/config.rs35
-rw-r--r--bin/domain/imag-mail/src/import.rs4
-rw-r--r--bin/domain/imag-mail/src/lib.rs11
-rw-r--r--bin/domain/imag-mail/src/new.rs219
-rw-r--r--bin/domain/imag-mail/src/ui.rs14
-rw-r--r--imagrc.toml34
7 files changed, 314 insertions, 7 deletions
diff --git a/bin/domain/imag-mail/Cargo.toml b/bin/domain/imag-mail/Cargo.toml
index 8342ff43..77879b59 100644
--- a/bin/domain/imag-mail/Cargo.toml
+++ b/bin/domain/imag-mail/Cargo.toml
@@ -27,6 +27,9 @@ handlebars = "3"
itertools = "0.9"
serde = "1"
serde_derive = "1"
+chrono = "0.4"
+email-format = "0.8"
+maildir = "0.4"
libimagrt = { version = "0.10.0", path = "../../../lib/core/libimagrt" }
libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" }
@@ -36,6 +39,7 @@ libimagutil = { version = "0.10.0", path = "../../../lib/etc/libimagutil" }
libimagentryref = { version = "0.10.0", path = "../../../lib/entry/libimagentryref" }
libimagentrytag = { version = "0.10.0", path = "../../../lib/entry/libimagentrytag" }
libimaginteraction = { version = "0.10.0", path = "../../../lib/etc/libimaginteraction" }
+libimagentryedit = { version = "0.10.0", path = "../../../lib/entry/libimagentryedit" }
[dependencies.clap]
version = "2.33.0"
diff --git a/bin/domain/imag-mail/src/config.rs b/bin/domain/imag-mail/src/config.rs
index 0e43ac8e..a7a991d7 100644
--- a/bin/domain/imag-mail/src/config.rs
+++ b/bin/domain/imag-mail/src/config.rs
@@ -38,12 +38,25 @@ pub struct MailConfig {
import_tag: Option<String>,
#[serde(rename = "import_notmuch_tags")]
- import_notmuch_tags: bool
+ import_notmuch_tags: bool,
+
+ #[serde(rename = "edit_headers")]
+ edit_headers: bool,
+
+ #[serde(rename = "default_template")]
+ default_template: String,
+
+ #[serde(rename = "header_template")]
+ header_template: String,
+
+ #[serde(rename = "from_address")]
+ from_address: String,
+
}
impl MailConfig {
- pub fn get_list_format(&self, scmd: &ArgMatches) -> Result<Handlebars> {
+ pub fn get_list_format_or_cli(&self, scmd: &ArgMatches) -> Result<Handlebars> {
let fmt = scmd
.value_of("format")
.map(String::from)
@@ -58,7 +71,8 @@ impl MailConfig {
Ok(hb)
}
- pub fn get_notmuch_database_path(&self, rt: &Runtime) -> PathBuf {
+ /// Get the notmuch database path either from CLI or from config
+ pub fn get_notmuch_database_path_or_cli(&self, rt: &Runtime) -> PathBuf {
if let Some(pb) = rt.cli()
.value_of("database_path")
.map(String::from)
@@ -78,5 +92,20 @@ impl MailConfig {
self.import_notmuch_tags
}
+ pub fn get_edit_headers(&self) -> bool {
+ self.edit_headers
+ }
+
+ pub fn get_default_template(&self) -> &String {
+ &self.default_template
+ }
+
+ pub fn get_header_template(&self) -> &String {
+ &self.header_template
+ }
+
+ pub fn get_from_address(&self) -> &String {
+ &self.from_address
+ }
}
diff --git a/bin/domain/imag-mail/src/import.rs b/bin/domain/imag-mail/src/import.rs
index 75ce3606..217e6cf7 100644
--- a/bin/domain/imag-mail/src/import.rs
+++ b/bin/domain/imag-mail/src/import.rs
@@ -37,7 +37,7 @@ pub fn import(rt: &Runtime) -> Result<()> {
let query = scmd.value_of("query").unwrap();
let quick = scmd.is_present("quick");
let store = rt.store();
- let notmuch_path = config.get_notmuch_database_path(rt);
+ let notmuch_path = config.get_notmuch_database_path_or_cli(rt);
let notmuch_connection = NotmuchConnection::open(notmuch_path)?;
let import_nm_tags = config.get_import_notmuch_tags();
@@ -49,7 +49,7 @@ pub fn import(rt: &Runtime) -> Result<()> {
let r = store
.with_connection(&notmuch_connection)
- .import_with_query(query, config.get_import_tag().map(String::clone), import_nm_tags, quick)?
+ .import_with_query(query, config.get_import_tag().clone(), *import_nm_tags, quick)?
.into_iter()
.map(|fle| rt.report_touched(fle.get_location()))
.collect::<Result<Vec<_>>>()
diff --git a/bin/domain/imag-mail/src/lib.rs b/bin/domain/imag-mail/src/lib.rs
index 866dcac9..2ef1da37 100644
--- a/bin/domain/imag-mail/src/lib.rs
+++ b/bin/domain/imag-mail/src/lib.rs
@@ -43,6 +43,9 @@ extern crate resiter;
extern crate handlebars;
extern crate itertools;
extern crate serde;
+extern crate chrono;
+extern crate email_format;
+extern crate maildir;
#[macro_use] extern crate serde_derive;
extern crate libimagrt;
@@ -53,6 +56,7 @@ extern crate libimagutil;
extern crate libimagentryref;
extern crate libimagentrytag;
extern crate libimaginteraction;
+extern crate libimagentryedit;
use std::io::Write;
use std::io::BufRead;
@@ -81,6 +85,7 @@ mod config;
mod import;
mod ui;
mod util;
+mod new;
use config::MailConfig;
@@ -95,6 +100,8 @@ impl ImagApplication for ImagMail {
"import" => import::import(&rt),
"list" => list(&rt),
"print-id" => print_id(&rt),
+ "new" => new::new(&rt),
+ "reply-to" => new::reply_to(&rt),
other => {
debug!("Unknown command");
if rt.handle_unknown_subcommand("imag-mail", other, rt.cli())?.success() {
@@ -132,10 +139,10 @@ fn list(rt: &Runtime) -> Result<()> {
debug!("Listing mail");
let scmd = rt.cli().subcommand_matches("list").unwrap();
let store = rt.store();
- let notmuch_path = config.get_notmuch_database_path(rt);
+ let notmuch_path = config.get_notmuch_database_path_or_cli(rt);
debug!("notmuch path: {:?}", notmuch_path);
- let list_format = config.get_list_format(&scmd)?;
+ let list_format = config.get_list_format_or_cli(&scmd)?;
debug!("List-format: {:?}", list_format);
let notmuch_connection = NotmuchConnection::open(notmuch_path)?;
diff --git a/bin/domain/imag-mail/src/new.rs b/bin/domain/imag-mail/src/new.rs
new file mode 100644
index 00000000..0d196529
--- /dev/null
+++ b/bin/domain/imag-mail/src/new.rs
@@ -0,0 +1,219 @@
+//
+// imag - the personal information management suite for the commandline
+// Copyright (C) 2015-2020 Matthias Beyer <mail@beyermatthias.de> and contributors
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; version
+// 2.1 of the License.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+//
+
+use std::path::PathBuf;
+use std::collections::BTreeMap;
+
+use failure::Fallible as Result;
+use failure::err_msg;
+use toml_query::read::TomlValueReadExt;
+use clap::ArgMatches;
+use resiter::IterInnerOkOrElse;
+use resiter::AndThen;
+use resiter::Filter;
+use resiter::Map;
+use chrono::Local;
+use email_format::Email;
+use email_format::rfc5322::Parsable;
+use handlebars::Handlebars;
+use failure::Error;
+
+use libimagmail::mail::Mail;
+use libimagmail::store::MailStore;
+use libimagmail::notmuch::connection::NotmuchConnection;
+use libimagrt::runtime::Runtime;
+use libimagstore::storeid::StoreId;
+use libimagstore::iter::get::StoreIdGetIteratorExtension;
+use libimagentryedit::edit::edit_in_tmpfile;
+use libimaginteraction::format::HandlebarsData;
+
+use crate::config::MailConfig;
+
+fn sid_value_of(scmd: &ArgMatches, s: &str) -> Option<Result<StoreId>> {
+ scmd.value_of(s).map(String::from).map(PathBuf::from).map(StoreId::new)
+}
+
+pub fn new(rt: &Runtime) -> Result<()> {
+ let config = rt.config()
+ .ok_or_else(|| err_msg("Configuration missing"))?
+ .read_partial::<MailConfig>()?
+ .ok_or_else(|| err_msg("Configuration for \"mail\" missing"))?;
+
+ let scmd = rt.cli().subcommand_matches("new").unwrap(); // safe by main()
+ let notmuch_path = config.get_notmuch_database_path_or_cli(rt);
+ debug!("notmuch path: {:?}", notmuch_path);
+ let notmuch_connection = NotmuchConnection::open(notmuch_path)?;
+ let store = rt.store().with_connection(&notmuch_connection);
+
+ let bcc = sid_value_of(&scmd, "bcc").transpose()?;
+ let cc = sid_value_of(&scmd, "cc").transpose()?;
+ let to = sid_value_of(&scmd, "to")
+ .map(|r| r.map(|e| Some(vec![e])))
+ .unwrap_or_else(|| rt.ids::<crate::ui::PathProvider>())?
+ .ok_or_else(|| err_msg("No ids supplied"))?;
+ let subject = scmd.value_of("subject").map(String::from);
+
+ let in_reply_to = scmd.value_of("in-reply-to")
+ .map(String::from)
+ .map(|s| {
+ match StoreId::new(PathBuf::from(&s))
+ .and_then(|sid| store.get(sid))?
+ {
+ Some(fle) => Ok(Some(fle)),
+ None => store.get_mail_by_id(&s),
+ }
+ })
+ .transpose()?
+ .flatten();
+
+ let notmuch_path = config.get_notmuch_database_path_or_cli(rt);
+ debug!("notmuch path: {:?}", notmuch_path);
+
+ let written_message_id = mk_processed_template(rt, &scmd, &config)
+ .and_then(|msg| edit_message_validated(rt, msg))
+ .and_then(|msg| {
+ // Write message to maildir and return message id
+ maildir::Maildir::from(config.get_outgoing_maildir().to_path_buf())
+ .store_new(&msg.as_bytes())
+ .map_err(Error::from)
+ })?;
+
+ //
+ // Writing the valid message to the outgoing maildir
+ //
+
+ info!("Stored: {}", written_message_id);
+ debug!("Stored: {} in {}", written_message_id, config.get_outgoing_maildir().display());
+
+ Ok(())
+}
+
+pub fn reply_to(rt: &Runtime) -> Result<()> {
+ let config = rt.config()
+ .ok_or_else(|| err_msg("Configuration missing"))?
+ .read_partial::<MailConfig>()?
+ .ok_or_else(|| err_msg("Configuration for \"mail\" missing"))?;
+
+ let scmd = rt.cli().subcommand_matches("reply-to").unwrap(); // safe by main()
+ let store = rt.store();
+
+ let in_reply_to = sid_value_of(&scmd, "in-reply-to")
+ .map(|r| r.map(|e| Some(vec![e])))
+ .unwrap_or_else(|| rt.ids::<crate::ui::PathProvider>())?
+ .ok_or_else(|| err_msg("No ids supplied"))?
+ .into_iter()
+ .map(Ok)
+ .into_get_iter(rt.store())
+ .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
+ .and_then_ok(|m| m.is_mail().map(|b| (b, m)))
+ .filter_ok(|tpl| tpl.0)
+ .map_ok(|tpl| tpl.1);
+
+ unimplemented!()
+}
+
+fn mk_processed_template(rt: &Runtime, scmd: &ArgMatches, config: &MailConfig) -> Result<String> {
+ debug!("Processing the template for the mail...");
+ let mut hb_data = BTreeMap::new();
+
+ hb_data.insert(String::from("message_id"), HandlebarsData::Str(generate_message_id()?));
+ hb_data.insert(String::from("date"), HandlebarsData::Str({
+ scmd.value_of("date")
+ .map(String::from)
+ .unwrap_or_else(|| {
+ Local::now().to_rfc2822()
+ })
+ }));
+
+ hb_data.insert(String::from("from"), HandlebarsData::Str({
+ scmd.value_of("from")
+ .map(String::from)
+ .unwrap_or_else(|| {
+ config.get_from_address().clone()
+ })
+ }));
+
+ hb_data.insert(String::from("to"), HandlebarsData::Str({
+ scmd.value_of("to")
+ .map(String::from)
+ .ok_or_else(|| {
+ err_msg("Missing value for field field: 'to'")
+ })?
+ }));
+
+ if let Some(in_reply_to) = scmd.value_of("in-reply-to").map(String::from) {
+ hb_data.insert(String::from("in_reply_to") , HandlebarsData::Str(in_reply_to));
+ }
+
+ if let Some(cc) = scmd.value_of("cc").map(String::from) {
+ hb_data.insert(String::from("cc") , HandlebarsData::Str(cc));
+ }
+
+ if let Some(bcc) = scmd.value_of("bcc").map(String::from) {
+ hb_data.insert(String::from("bcc") , HandlebarsData::Str(bcc));
+ }
+
+ hb_data.insert(String::from("subject"), HandlebarsData::Str({
+ scmd.value_of("subject")
+ .map(String::from)
+ .unwrap_or_else(|| String::new())
+ }));
+
+ let template = if *config.get_edit_headers() {
+ debug!("Template with header header editing");
+ config.get_header_template()
+ } else {
+ debug!("Template without header editing");
+ config.get_default_template()
+ };
+
+ process_template(template, &hb_data)
+}
+
+///
+/// Editing the text and validating it
+///
+fn edit_message_validated(rt: &Runtime, mut msg: String) -> Result<String> {
+ edit_in_tmpfile(&rt, &mut msg)?;
+
+ let (mail, remainder) = Email::parse(&msg.as_bytes())?;
+ debug!("Parsed: {}", mail);
+ debug!("Remainder: {:?}", remainder);
+
+ if remainder.len() != 0 {
+ Err(err_msg("Some bytes are not parsed... cannot verify that the mail is correct!"))?
+ }
+
+ Ok(msg)
+}
+
+fn process_template(template: &str, data: &BTreeMap<String, HandlebarsData>) -> Result<String> {
+ let mut hb = Handlebars::new();
+ hb.register_template_string("format", template)?;
+
+ hb.register_escape_fn(::handlebars::no_escape);
+ ::libimaginteraction::format::register_all_color_helpers(&mut hb);
+ ::libimaginteraction::format::register_all_format_helpers(&mut hb);
+
+ hb.render("format", data).map_err(Error::from)
+}
+
+fn generate_message_id() -> Result<String> {
+ unimplemented!()
+}
diff --git a/bin/domain/imag-mail/src/ui.rs b/bin/domain/imag-mail/src/ui.rs
index 7ea993ab..3345a6aa 100644
--- a/bin/domain/imag-mail/src/ui.rs
+++ b/bin/domain/imag-mail/src/ui.rs
@@ -135,6 +135,20 @@ pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
.required(false)
.value_name("ADDRESS")
.help("The subject of the mail"))
+ .arg(Arg::with_name("date")
+ .long("date")
+ .takes_value(true)
+ .multiple(true)
+ .required(false)
+ .value_name("DATETIME")
+ .help("The date to put into the mail (defaults to now)"))
+ .arg(Arg::with_name("from")
+ .long("from")
+ .takes_value(true)
+ .multiple(true)
+ .required(false)
+ .value_name("FROM")
+ .help("The from address to put into the mail, defaults to configured value"))
.arg(Arg::with_name("in-reply-to")
.long("in-reply-to")
.short("r")
diff --git a/imagrc.toml b/imagrc.toml
index 5056ff22..e30dec15 100644
--- a/imagrc.toml
+++ b/imagrc.toml
@@ -243,6 +243,40 @@ import_tag = "mail"
# Import tags from notmuch into imag
import_notmuch_tags = true
+# edit headers when typing mail
+edit_headers = true
+
+from_address = "User Name <user@name.tld>"
+
+default_template = """
+Dear {{to | get_name}}
+{{if cc}}
+dear {{cc | get_name}}
+{{/if}}
+
+With kind regards
+"""
+
+header_template = """
+Date: {{date}}
+From: {{from}}
+Reply-To: {{from}}
+Message-Id: {{message_id}}
+{{#if is_in_reply_to}}
+In-Reply-To: {{in_reply_to | message_id}}
+{{/if}}
+To: {{to}}
+Cc: {{cc}}
+Subject: {{subject}}
+
+Dear {{to | get_name}},
+{{if cc}}
+dear {{cc | get_name}}
+{{/if}}
+
+With kind regards
+"""
+
[todo]
show_format = """
{{i}} {{uuid}}