From 70f974ee7ab1d15a200d625878bd8b7db861bb98 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 18 Apr 2020 11:18:07 +0200 Subject: Add implementation for crafting new mail Signed-off-by: Matthias Beyer --- bin/domain/imag-mail/Cargo.toml | 4 + bin/domain/imag-mail/src/config.rs | 35 +++++- bin/domain/imag-mail/src/import.rs | 4 +- bin/domain/imag-mail/src/lib.rs | 11 +- bin/domain/imag-mail/src/new.rs | 219 +++++++++++++++++++++++++++++++++++++ bin/domain/imag-mail/src/ui.rs | 14 +++ imagrc.toml | 34 ++++++ 7 files changed, 314 insertions(+), 7 deletions(-) create mode 100644 bin/domain/imag-mail/src/new.rs 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, #[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 { + pub fn get_list_format_or_cli(&self, scmd: &ArgMatches) -> Result { 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(¬much_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::>>() 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 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> { + 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::()? + .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(¬much_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::())? + .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::()? + .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::())? + .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 { + 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 { + 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) -> Result { + 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 { + 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 " + +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}} -- cgit v1.2.3