diff --git a/README.md b/README.md index 3fa1103..68661a7 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ all the options.* [![paypal](https://img.shields.io/badge/-PayPal-0079c1?logo=PayPal&logoColor=ffffff)](https://www.paypal.com/paypalme/soywod) [![ko-fi](https://img.shields.io/badge/-Ko--fi-ff5e5a?logo=Ko-fi&logoColor=ffffff)](https://ko-fi.com/soywod) [![buy-me-a-coffee](https://img.shields.io/badge/-Buy%20Me%20a%20Coffee-ffdd00?logo=Buy%20Me%20A%20Coffee&logoColor=000000)](https://www.buymeacoffee.com/soywod) +[![liberapay](https://img.shields.io/badge/-Liberapay-f6c915?logo=Liberapay&logoColor=222222)](https://liberapay.com/soywod) ## Credits diff --git a/src/config/model.rs b/src/config/model.rs index 8a612b4..cc7a33a 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -20,7 +20,7 @@ const DEFAULT_PAGE_SIZE: usize = 10; // Account -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Account { // Override diff --git a/src/flag/cli.rs b/src/flag/cli.rs index 370d212..abddb38 100644 --- a/src/flag/cli.rs +++ b/src/flag/cli.rs @@ -20,25 +20,22 @@ fn flags_arg<'a>() -> clap::Arg<'a, 'a> { pub fn flag_subcmds<'a>() -> Vec> { vec![clap::SubCommand::with_name("flags") - .aliases(&["flag"]) .about("Handles flags") .subcommand( clap::SubCommand::with_name("set") - .aliases(&["s"]) .about("Replaces all message flags") .arg(uid_arg()) .arg(flags_arg()), ) .subcommand( clap::SubCommand::with_name("add") - .aliases(&["a"]) .about("Appends flags to a message") .arg(uid_arg()) .arg(flags_arg()), ) .subcommand( clap::SubCommand::with_name("remove") - .aliases(&["rm", "r"]) + .aliases(&["rm"]) .about("Removes flags from a message") .arg(uid_arg()) .arg(flags_arg()), diff --git a/src/main.rs b/src/main.rs index 40f6116..556eece 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,7 @@ fn parse_args<'a>() -> clap::App<'a, 'a> { .version(env!("CARGO_PKG_VERSION")) .about(env!("CARGO_PKG_DESCRIPTION")) .author(env!("CARGO_PKG_AUTHORS")) + .setting(clap::AppSettings::InferSubcommands) .args(&output_args()) .args(&config_args()) .arg(mbox_source_arg()) diff --git a/src/msg/cli.rs b/src/msg/cli.rs index 007a883..6e350d5 100644 --- a/src/msg/cli.rs +++ b/src/msg/cli.rs @@ -13,7 +13,13 @@ use crate::{ imap::model::ImapConnector, input, mbox::cli::mbox_target_arg, - msg::model::{Attachments, Msg, Msgs, ReadableMsg}, + msg::{ + model::{Attachments, Msg, Msgs, ReadableMsg}, + tpl::{ + cli::{tpl_matches, tpl_subcommand}, + model::Tpl, + }, + }, smtp, }; @@ -22,6 +28,7 @@ error_chain! { Imap(crate::imap::model::Error, crate::imap::model::ErrorKind); Input(crate::input::Error, crate::input::ErrorKind); MsgModel(crate::msg::model::Error, crate::msg::model::ErrorKind); + TplCli(crate::msg::tpl::cli::Error, crate::msg::tpl::cli::ErrorKind); Smtp(crate::smtp::Error, crate::smtp::ErrorKind); } foreign_links { @@ -67,18 +74,17 @@ fn attachment_arg<'a>() -> clap::Arg<'a, 'a> { .long("attachment") .value_name("PATH") .multiple(true) - .takes_value(true) } pub fn msg_subcmds<'a>() -> Vec> { vec![ clap::SubCommand::with_name("list") - .aliases(&["lst", "l"]) + .aliases(&["lst"]) .about("Lists all messages") .arg(page_size_arg()) .arg(page_arg()), clap::SubCommand::with_name("search") - .aliases(&["query", "q", "s"]) + .aliases(&["query", "q"]) .about("Lists messages matching the given IMAP query") .arg(page_size_arg()) .arg(page_arg()) @@ -90,7 +96,6 @@ pub fn msg_subcmds<'a>() -> Vec> { .required(true), ), clap::SubCommand::with_name("write") - .aliases(&["w"]) .about("Writes a new message") .arg(attachment_arg()), clap::SubCommand::with_name("send") @@ -100,7 +105,6 @@ pub fn msg_subcmds<'a>() -> Vec> { .about("Saves a raw message") .arg(clap::Arg::with_name("message").raw(true)), clap::SubCommand::with_name("read") - .aliases(&["r"]) .about("Reads text bodies of a message") .arg(uid_arg()) .arg( @@ -119,55 +123,33 @@ pub fn msg_subcmds<'a>() -> Vec> { .short("r"), ), clap::SubCommand::with_name("attachments") - .aliases(&["attach", "att", "a"]) .about("Downloads all message attachments") .arg(uid_arg()), clap::SubCommand::with_name("reply") - .aliases(&["rep", "re"]) .about("Answers to a message") .arg(uid_arg()) .arg(reply_all_arg()) .arg(attachment_arg()), clap::SubCommand::with_name("forward") - .aliases(&["fwd", "f"]) + .aliases(&["fwd"]) .about("Forwards a message") .arg(uid_arg()) .arg(attachment_arg()), clap::SubCommand::with_name("copy") - .aliases(&["cp", "c"]) + .aliases(&["cp"]) .about("Copies a message to the targetted mailbox") .arg(uid_arg()) .arg(mbox_target_arg()), clap::SubCommand::with_name("move") - .aliases(&["mv", "m"]) + .aliases(&["mv"]) .about("Moves a message to the targetted mailbox") .arg(uid_arg()) .arg(mbox_target_arg()), clap::SubCommand::with_name("delete") - .aliases(&["remove", "rm", "del", "d"]) + .aliases(&["remove", "rm"]) .about("Deletes a message") .arg(uid_arg()), - clap::SubCommand::with_name("template") - .aliases(&["tpl", "t"]) - .about("Generates a message template") - .subcommand( - clap::SubCommand::with_name("new") - .aliases(&["n"]) - .about("Generates a new message template"), - ) - .subcommand( - clap::SubCommand::with_name("reply") - .aliases(&["rep", "r"]) - .about("Generates a reply message template") - .arg(uid_arg()) - .arg(reply_all_arg()), - ) - .subcommand( - clap::SubCommand::with_name("forward") - .aliases(&["fwd", "fw", "f"]) - .about("Generates a forward message template") - .arg(uid_arg()), - ), + tpl_subcommand(), ] } @@ -183,9 +165,10 @@ pub fn msg_matches(app: &App) -> Result { ("save", Some(matches)) => msg_matches_save(app, matches), ("search", Some(matches)) => msg_matches_search(app, matches), ("send", Some(matches)) => msg_matches_send(app, matches), - ("template", Some(matches)) => msg_matches_template(app, matches), ("write", Some(matches)) => msg_matches_write(app, matches), + ("template", Some(matches)) => Ok(tpl_matches(app, matches)?), + ("list", opt_matches) => msg_matches_list(app, opt_matches), (_other, opt_matches) => msg_matches_list(app, opt_matches), } @@ -195,19 +178,11 @@ fn msg_matches_list(app: &App, opt_matches: Option<&clap::ArgMatches>) -> Result debug!("list command matched"); let page_size: usize = opt_matches - .and_then(|matches| { - matches.value_of("page-size") - .and_then(|s| s.parse().ok()) - }) + .and_then(|matches| matches.value_of("page-size").and_then(|s| s.parse().ok())) .unwrap_or_else(|| app.config.default_page_size(&app.account)); debug!("page size: {:?}", page_size); let page: usize = opt_matches - .and_then(|matches| { - matches.value_of("page") - .unwrap() - .parse() - .ok() - }) + .and_then(|matches| matches.value_of("page").unwrap().parse().ok()) .unwrap_or_default(); debug!("page: {}", &page); @@ -294,8 +269,8 @@ fn msg_matches_read(app: &App, matches: &clap::ArgMatches) -> Result { let mut imap_conn = ImapConnector::new(&app.account)?; let msg = imap_conn.read_msg(&app.mbox, &uid)?; if raw { - let msg = String::from_utf8(msg) - .chain_err(|| "Could not decode raw message as utf8 string")?; + let msg = + String::from_utf8(msg).chain_err(|| "Could not decode raw message as utf8 string")?; let msg = msg.trim_end_matches("\n"); app.output.print(msg); } else { @@ -352,7 +327,7 @@ fn msg_matches_write(app: &App, matches: &clap::ArgMatches) -> Result { .unwrap_or_default() .map(String::from) .collect::>(); - let tpl = Msg::build_new_tpl(&app.config, &app.account)?; + let tpl = Tpl::new(&app); let content = input::open_editor_with_tpl(tpl.to_string().as_bytes())?; let mut msg = Msg::from(content); msg.attachments = attachments; @@ -513,53 +488,6 @@ fn msg_matches_forward(app: &App, matches: &clap::ArgMatches) -> Result { Ok(true) } -fn msg_matches_template(app: &App, matches: &clap::ArgMatches) -> Result { - debug!("template command matched"); - - if let Some(_) = matches.subcommand_matches("new") { - debug!("new command matched"); - let tpl = Msg::build_new_tpl(&app.config, &app.account)?; - trace!("tpl: {:?}", tpl); - app.output.print(tpl); - } - - if let Some(matches) = matches.subcommand_matches("reply") { - debug!("reply command matched"); - - let uid = matches.value_of("uid").unwrap(); - debug!("uid: {}", uid); - - let mut imap_conn = ImapConnector::new(&app.account)?; - let msg = Msg::from(imap_conn.read_msg(&app.mbox, &uid)?); - let tpl = if matches.is_present("reply-all") { - msg.build_reply_all_tpl(&app.config, &app.account)? - } else { - msg.build_reply_tpl(&app.config, &app.account)? - }; - trace!("tpl: {:?}", tpl); - app.output.print(tpl); - - imap_conn.logout(); - } - - if let Some(matches) = matches.subcommand_matches("forward") { - debug!("forward command matched"); - - let uid = matches.value_of("uid").unwrap(); - debug!("uid: {}", uid); - - let mut imap_conn = ImapConnector::new(&app.account)?; - let msg = Msg::from(imap_conn.read_msg(&app.mbox, &uid)?); - let tpl = msg.build_forward_tpl(&app.config, &app.account)?; - trace!("tpl: {:?}", tpl); - app.output.print(tpl); - - imap_conn.logout(); - } - - Ok(true) -} - fn msg_matches_copy(app: &App, matches: &clap::ArgMatches) -> Result { debug!("copy command matched"); @@ -665,4 +593,3 @@ fn msg_matches_save(app: &App, matches: &clap::ArgMatches) -> Result { imap_conn.logout(); Ok(true) } - diff --git a/src/msg/mod.rs b/src/msg/mod.rs index 9225677..807eee0 100644 --- a/src/msg/mod.rs +++ b/src/msg/mod.rs @@ -1,2 +1,3 @@ pub mod cli; pub mod model; +pub mod tpl; diff --git a/src/msg/model.rs b/src/msg/model.rs index 2311900..5267ee0 100644 --- a/src/msg/model.rs +++ b/src/msg/model.rs @@ -100,23 +100,23 @@ impl<'a> Attachments { // Readable message -#[derive(Debug)] +#[derive(Debug, Serialize)] pub struct ReadableMsg { pub content: String, pub has_attachment: bool, } -impl Serialize for ReadableMsg { - fn serialize(&self, serializer: S) -> result::Result - where - S: ser::Serializer, - { - let mut state = serializer.serialize_struct("ReadableMsg", 2)?; - state.serialize_field("content", &self.content)?; - state.serialize_field("hasAttachment", if self.has_attachment { &1 } else { &0 })?; - state.end() - } -} +// impl Serialize for ReadableMsg { +// fn serialize(&self, serializer: S) -> result::Result +// where +// S: ser::Serializer, +// { +// let mut state = serializer.serialize_struct("ReadableMsg", 2)?; +// state.serialize_field("content", &self.content)?; +// state.serialize_field("hasAttachment", if self.has_attachment { &1 } else { &0 })?; +// state.end() +// } +// } impl fmt::Display for ReadableMsg { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -374,7 +374,11 @@ impl<'m> Msg<'m> { Ok(msg) } - fn extract_text_bodies_into(part: &mailparse::ParsedMail, mime: &str, parts: &mut Vec) { + pub fn extract_text_bodies_into( + part: &mailparse::ParsedMail, + mime: &str, + parts: &mut Vec, + ) { match part.subparts.len() { 0 => { let content_type = part diff --git a/src/msg/tpl/cli.rs b/src/msg/tpl/cli.rs new file mode 100644 index 0000000..4b21968 --- /dev/null +++ b/src/msg/tpl/cli.rs @@ -0,0 +1,302 @@ +use clap; +use error_chain::error_chain; +use log::{debug, trace}; +use mailparse; + +use crate::{app::App, imap::model::ImapConnector, msg::tpl::model::Tpl}; + +error_chain! { + links { + Imap(crate::imap::model::Error, crate::imap::model::ErrorKind); + } + foreign_links { + Clap(clap::Error); + MailParse(mailparse::MailParseError); + } +} + +pub fn uid_arg<'a>() -> clap::Arg<'a, 'a> { + clap::Arg::with_name("uid") + .help("Specifies the targetted message") + .value_name("UID") + .required(true) +} + +fn reply_all_arg<'a>() -> clap::Arg<'a, 'a> { + clap::Arg::with_name("reply-all") + .help("Includes all recipients") + .short("A") + .long("all") +} + +pub fn tpl_subcommand<'a>() -> clap::App<'a, 'a> { + clap::SubCommand::with_name("template") + .aliases(&["tpl"]) + .about("Generates a message template") + .subcommand( + clap::SubCommand::with_name("new") + .aliases(&["n"]) + .about("Generates a new message template") + .args(&tpl_args()), + ) + .subcommand( + clap::SubCommand::with_name("reply") + .aliases(&["rep", "r"]) + .about("Generates a reply message template") + .arg(uid_arg()) + .arg(reply_all_arg()) + .args(&tpl_args()), + ) + .subcommand( + clap::SubCommand::with_name("forward") + .aliases(&["fwd", "fw", "f"]) + .about("Generates a forward message template") + .arg(uid_arg()) + .args(&tpl_args()), + ) +} + +pub fn tpl_args<'a>() -> Vec> { + vec![ + clap::Arg::with_name("subject") + .help("Overrides the Subject header") + .short("s") + .long("subject") + .value_name("STRING"), + clap::Arg::with_name("from") + .help("Overrides the From header") + .short("f") + .long("from") + .value_name("ADDR"), + clap::Arg::with_name("to") + .help("Overrides the To header") + .short("t") + .long("to") + .value_name("ADDR") + .multiple(true), + clap::Arg::with_name("cc") + .help("Overrides the Cc header") + .short("c") + .long("cc") + .value_name("ADDR") + .multiple(true), + clap::Arg::with_name("bcc") + .help("Overrides the Bcc header") + .short("b") + .long("bcc") + .value_name("ADDR") + .multiple(true), + clap::Arg::with_name("header") + .help("Overrides a specific header") + .short("h") + .long("header") + .value_name("KEY: VAL") + .multiple(true), + clap::Arg::with_name("body") + .help("Overrides the body") + .short("B") + .long("body") + .value_name("STRING"), + clap::Arg::with_name("signature") + .help("Overrides the signature") + .short("S") + .long("signature") + .value_name("STRING"), + ] +} + +pub fn tpl_matches(app: &App, matches: &clap::ArgMatches) -> Result { + match matches.subcommand() { + ("new", Some(matches)) => tpl_matches_new(app, matches), + ("reply", Some(matches)) => tpl_matches_reply(app, matches), + ("forward", Some(matches)) => tpl_matches_forward(app, matches), + + // TODO: find a way to show the help message for template subcommand + _ => Err("Subcommand not found".into()), + } +} + +fn tpl_matches_new(app: &App, matches: &clap::ArgMatches) -> Result { + debug!("new command matched"); + let mut tpl = Tpl::new(&app); + + if let Some(from) = matches.value_of("from") { + debug!("overriden from: {:?}", from); + tpl.header("From", from); + }; + + if let Some(subject) = matches.value_of("subject") { + debug!("overriden subject: {:?}", subject); + tpl.header("Subject", subject); + }; + + let addrs = matches.values_of("to").unwrap_or_default(); + if addrs.len() > 0 { + debug!("overriden to: {:?}", addrs); + tpl.header("To", addrs.collect::>().join(", ")); + } + + let addrs = matches.values_of("cc").unwrap_or_default(); + if addrs.len() > 0 { + debug!("overriden cc: {:?}", addrs); + tpl.header("Cc", addrs.collect::>().join(", ")); + } + + let addrs = matches.values_of("bcc").unwrap_or_default(); + if addrs.len() > 0 { + debug!("overriden bcc: {:?}", addrs); + tpl.header("Bcc", addrs.collect::>().join(", ")); + } + + for header in matches.values_of("header").unwrap_or_default() { + let mut header = header.split(":"); + let key = header.next().unwrap_or_default(); + let val = header.next().unwrap_or_default().trim_start(); + debug!("overriden header: {}={}", key, val); + tpl.header(key, val); + } + + if let Some(body) = matches.value_of("body") { + debug!("overriden body: {:?}", body); + tpl.body(body); + }; + + if let Some(signature) = matches.value_of("signature") { + debug!("overriden signature: {:?}", signature); + tpl.signature(signature); + }; + + trace!("tpl: {:?}", tpl); + app.output.print(tpl); + + Ok(true) +} + +fn tpl_matches_reply(app: &App, matches: &clap::ArgMatches) -> Result { + debug!("reply command matched"); + + let uid = matches.value_of("uid").unwrap(); + debug!("uid: {}", uid); + + let mut imap_conn = ImapConnector::new(&app.account)?; + let msg = &imap_conn.read_msg(&app.mbox, &uid)?; + let msg = mailparse::parse_mail(&msg)?; + let mut tpl = if matches.is_present("reply-all") { + Tpl::reply(&app, &msg) + } else { + Tpl::reply_all(&app, &msg) + }; + if let Some(from) = matches.value_of("from") { + debug!("overriden from: {:?}", from); + tpl.header("From", from); + }; + + if let Some(subject) = matches.value_of("subject") { + debug!("overriden subject: {:?}", subject); + tpl.header("Subject", subject); + }; + + let addrs = matches.values_of("to").unwrap_or_default(); + if addrs.len() > 0 { + debug!("overriden to: {:?}", addrs); + tpl.header("To", addrs.collect::>().join(", ")); + } + + let addrs = matches.values_of("cc").unwrap_or_default(); + if addrs.len() > 0 { + debug!("overriden cc: {:?}", addrs); + tpl.header("Cc", addrs.collect::>().join(", ")); + } + + let addrs = matches.values_of("bcc").unwrap_or_default(); + if addrs.len() > 0 { + debug!("overriden bcc: {:?}", addrs); + tpl.header("Bcc", addrs.collect::>().join(", ")); + } + + for header in matches.values_of("header").unwrap_or_default() { + let mut header = header.split(":"); + let key = header.next().unwrap_or_default(); + let val = header.next().unwrap_or_default().trim_start(); + debug!("overriden header: {}={}", key, val); + tpl.header(key, val); + } + + if let Some(body) = matches.value_of("body") { + debug!("overriden body: {:?}", body); + tpl.body(body); + }; + + if let Some(signature) = matches.value_of("signature") { + debug!("overriden signature: {:?}", signature); + tpl.signature(signature); + }; + + trace!("tpl: {:?}", tpl); + app.output.print(tpl); + + Ok(true) +} + +fn tpl_matches_forward(app: &App, matches: &clap::ArgMatches) -> Result { + debug!("forward command matched"); + + let uid = matches.value_of("uid").unwrap(); + debug!("uid: {}", uid); + + let mut imap_conn = ImapConnector::new(&app.account)?; + let msg = &imap_conn.read_msg(&app.mbox, &uid)?; + let msg = mailparse::parse_mail(&msg)?; + let mut tpl = Tpl::forward(&app, &msg); + + if let Some(from) = matches.value_of("from") { + debug!("overriden from: {:?}", from); + tpl.header("From", from); + }; + + if let Some(subject) = matches.value_of("subject") { + debug!("overriden subject: {:?}", subject); + tpl.header("Subject", subject); + }; + + let addrs = matches.values_of("to").unwrap_or_default(); + if addrs.len() > 0 { + debug!("overriden to: {:?}", addrs); + tpl.header("To", addrs.collect::>().join(", ")); + } + + let addrs = matches.values_of("cc").unwrap_or_default(); + if addrs.len() > 0 { + debug!("overriden cc: {:?}", addrs); + tpl.header("Cc", addrs.collect::>().join(", ")); + } + + let addrs = matches.values_of("bcc").unwrap_or_default(); + if addrs.len() > 0 { + debug!("overriden bcc: {:?}", addrs); + tpl.header("Bcc", addrs.collect::>().join(", ")); + } + + for header in matches.values_of("header").unwrap_or_default() { + let mut header = header.split(":"); + let key = header.next().unwrap_or_default(); + let val = header.next().unwrap_or_default().trim_start(); + debug!("overriden header: {}={}", key, val); + tpl.header(key, val); + } + + if let Some(body) = matches.value_of("body") { + debug!("overriden body: {:?}", body); + tpl.body(body); + }; + + if let Some(signature) = matches.value_of("signature") { + debug!("overriden signature: {:?}", signature); + tpl.signature(signature); + }; + + trace!("tpl: {:?}", tpl); + app.output.print(tpl); + + Ok(true) +} diff --git a/src/msg/tpl/mod.rs b/src/msg/tpl/mod.rs new file mode 100644 index 0000000..9225677 --- /dev/null +++ b/src/msg/tpl/mod.rs @@ -0,0 +1,2 @@ +pub mod cli; +pub mod model; diff --git a/src/msg/tpl/model.rs b/src/msg/tpl/model.rs new file mode 100644 index 0000000..e12e25d --- /dev/null +++ b/src/msg/tpl/model.rs @@ -0,0 +1,636 @@ +use error_chain::error_chain; +use mailparse::{self, MailHeaderMap}; +use serde::Serialize; +use std::{collections::HashMap, fmt}; + +use crate::{app::App, msg::model::Msg}; + +error_chain! {} + +const TPL_HEADERS: &[&str] = &["From", "To", "In-Reply-To", "Cc", "Bcc", "Subject"]; + +#[derive(Debug, Clone, Serialize)] +pub struct Tpl { + headers: HashMap, + body: Option, + signature: Option, +} + +impl Tpl { + pub fn new(app: &App) -> Self { + let mut headers = HashMap::new(); + headers.insert("From".to_string(), app.config.address(app.account)); + headers.insert("To".to_string(), String::new()); + headers.insert("Subject".to_string(), String::new()); + + Self { + headers, + body: None, + signature: app.config.signature(app.account), + } + } + + pub fn reply(app: &App, msg: &mailparse::ParsedMail) -> Self { + let parsed_headers = msg.get_headers(); + let mut headers = HashMap::new(); + + headers.insert("From".to_string(), app.config.address(app.account)); + + let to = parsed_headers + .get_first_value("reply-to") + .or(parsed_headers.get_first_value("from")) + .unwrap_or_default(); + headers.insert("To".to_string(), to); + + if let Some(in_reply_to) = parsed_headers.get_first_value("message-id") { + headers.insert("In-Reply-To".to_string(), in_reply_to); + } + + let subject = parsed_headers + .get_first_value("subject") + .unwrap_or_default(); + headers.insert("Subject".to_string(), format!("Re: {}", subject)); + + let mut parts = vec![]; + Msg::extract_text_bodies_into(&msg, "text/plain", &mut parts); + if parts.is_empty() { + Msg::extract_text_bodies_into(&msg, "text/html", &mut parts); + } + + let body = parts + .join("\r\n\r\n") + .replace("\r", "") + .split("\n") + .map(|line| format!(">{}", line)) + .collect::>() + .join("\n"); + + Self { + headers, + body: Some(body), + signature: app.config.signature(&app.account), + } + } + + pub fn reply_all(app: &App, msg: &mailparse::ParsedMail) -> Self { + let parsed_headers = msg.get_headers(); + let mut headers = HashMap::new(); + + let from: lettre::message::Mailbox = app.config.address(app.account).parse().unwrap(); + headers.insert("From".to_string(), from.to_string()); + + let to = parsed_headers + .get_all_values("to") + .iter() + .flat_map(|addrs| addrs.split(",")) + .fold(vec![], |mut mboxes, addr| { + match addr.trim().parse::() { + Err(_) => mboxes, + Ok(mbox) => { + if mbox != from { + mboxes.push(mbox.to_string()); + } + mboxes + } + } + }); + let reply_to = parsed_headers + .get_all_values("reply-to") + .iter() + .flat_map(|addrs| addrs.split(",")) + .map(|addr| addr.trim().to_string()) + .collect::>(); + let reply_to = if reply_to.is_empty() { + parsed_headers + .get_all_values("from") + .iter() + .flat_map(|addrs| addrs.split(",")) + .map(|addr| addr.trim().to_string()) + .collect::>() + } else { + reply_to + }; + headers.insert("To".to_string(), [reply_to, to].concat().join(", ")); + + if let Some(in_reply_to) = parsed_headers.get_first_value("message-id") { + headers.insert("In-Reply-To".to_string(), in_reply_to); + } + + let cc = parsed_headers.get_all_values("cc"); + if !cc.is_empty() { + headers.insert("Cc".to_string(), cc.join(", ")); + } + + let subject = parsed_headers + .get_first_value("subject") + .unwrap_or_default(); + headers.insert("Subject".to_string(), format!("Re: {}", subject)); + + let mut parts = vec![]; + Msg::extract_text_bodies_into(&msg, "text/plain", &mut parts); + if parts.is_empty() { + Msg::extract_text_bodies_into(&msg, "text/html", &mut parts); + } + + let body = parts + .join("\r\n\r\n") + .replace("\r", "") + .split("\n") + .map(|line| format!(">{}", line)) + .collect::>() + .join("\n"); + + Self { + headers, + body: Some(body), + signature: app.config.signature(&app.account), + } + } + + pub fn forward(app: &App, msg: &mailparse::ParsedMail) -> Self { + let parsed_headers = msg.get_headers(); + let mut headers = HashMap::new(); + + headers.insert("From".to_string(), app.config.address(app.account)); + headers.insert("To".to_string(), String::new()); + let subject = parsed_headers + .get_first_value("subject") + .unwrap_or_default(); + headers.insert("Subject".to_string(), format!("Fwd: {}", subject)); + + let mut parts = vec![]; + Msg::extract_text_bodies_into(&msg, "text/plain", &mut parts); + if parts.is_empty() { + Msg::extract_text_bodies_into(&msg, "text/html", &mut parts); + } + + let mut body = String::from("-------- Forwarded Message --------\n"); + body.push_str(&parts.join("\r\n\r\n").replace("\r", "")); + + Self { + headers, + body: Some(body), + signature: app.config.signature(&app.account), + } + } + + pub fn header(&mut self, key: K, val: V) -> &Self { + self.headers.insert(key.to_string(), val.to_string()); + self + } + + pub fn body(&mut self, body: T) -> &Self { + self.body = Some(body.to_string()); + self + } + + pub fn signature(&mut self, signature: T) -> &Self { + self.signature = Some(signature.to_string()); + self + } +} + +impl fmt::Display for Tpl { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut tpl = TPL_HEADERS.iter().fold(String::new(), |mut tpl, &key| { + if let Some(val) = self.headers.get(key) { + tpl.push_str(&format!("{}: {}\n", key, val)); + }; + tpl + }); + + for (key, val) in self.headers.iter() { + if !TPL_HEADERS.contains(&key.as_str()) { + tpl.push_str(&format!("{}: {}\n", key, val)); + } + } + + tpl.push_str("\n"); + + if let Some(body) = self.body.as_ref() { + tpl.push_str(&body); + } + + if let Some(signature) = self.signature.as_ref() { + tpl.push_str("\n\n"); + tpl.push_str(&signature); + } + + write!(f, "{}", tpl) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + app::App, + config::model::{Account, Config}, + msg::tpl::model::Tpl, + output::model::Output, + }; + + #[test] + fn new_tpl() { + let account = Account { + name: Some(String::from("Test")), + downloads_dir: None, + signature: None, + default_page_size: None, + default: Some(true), + email: String::from("test@localhost"), + watch_cmds: None, + imap_host: String::new(), + imap_port: 0, + imap_starttls: None, + imap_insecure: None, + imap_login: String::new(), + imap_passwd_cmd: String::new(), + smtp_host: String::new(), + smtp_port: 0, + smtp_starttls: None, + smtp_insecure: None, + smtp_login: String::new(), + smtp_passwd_cmd: String::new(), + }; + let config = Config { + name: String::new(), + downloads_dir: None, + notify_cmd: None, + signature: None, + default_page_size: None, + watch_cmds: None, + accounts: vec![(String::from("account"), account.clone())] + .into_iter() + .collect(), + }; + let output = Output::new("plain"); + let mbox = String::new(); + let arg_matches = clap::ArgMatches::new(); + let app = App::new(&config, &account, &output, &mbox, &arg_matches); + let tpl = Tpl::new(&app); + + assert_eq!( + "From: Test \nTo: \nSubject: \n\n", + tpl.to_string() + ); + } + + #[test] + fn new_tpl_with_signature() { + let account = Account { + name: Some(String::from("Test")), + downloads_dir: None, + signature: Some(String::from("-- \nCordialement,")), + default_page_size: None, + default: Some(true), + email: String::from("test@localhost"), + watch_cmds: None, + imap_host: String::new(), + imap_port: 0, + imap_starttls: None, + imap_insecure: None, + imap_login: String::new(), + imap_passwd_cmd: String::new(), + smtp_host: String::new(), + smtp_port: 0, + smtp_starttls: None, + smtp_insecure: None, + smtp_login: String::new(), + smtp_passwd_cmd: String::new(), + }; + let config = Config { + name: String::new(), + downloads_dir: None, + notify_cmd: None, + signature: None, + default_page_size: None, + watch_cmds: None, + accounts: vec![(String::from("account"), account.clone())] + .into_iter() + .collect(), + }; + let output = Output::new("plain"); + let mbox = String::new(); + let arg_matches = clap::ArgMatches::new(); + let app = App::new(&config, &account, &output, &mbox, &arg_matches); + let tpl = Tpl::new(&app); + + assert_eq!( + "From: Test \nTo: \nSubject: \n\n\n\n-- \nCordialement,", + tpl.to_string() + ); + } + + #[test] + fn reply_tpl() { + let account = Account { + name: Some(String::from("Test")), + downloads_dir: None, + signature: None, + default_page_size: None, + default: Some(true), + email: String::from("test@localhost"), + watch_cmds: None, + imap_host: String::new(), + imap_port: 0, + imap_starttls: None, + imap_insecure: None, + imap_login: String::new(), + imap_passwd_cmd: String::new(), + smtp_host: String::new(), + smtp_port: 0, + smtp_starttls: None, + smtp_insecure: None, + smtp_login: String::new(), + smtp_passwd_cmd: String::new(), + }; + let config = Config { + name: String::new(), + downloads_dir: None, + notify_cmd: None, + signature: None, + default_page_size: None, + watch_cmds: None, + accounts: vec![(String::from("account"), account.clone())] + .into_iter() + .collect(), + }; + let output = Output::new("plain"); + let mbox = String::new(); + let arg_matches = clap::ArgMatches::new(); + let app = App::new(&config, &account, &output, &mbox, &arg_matches); + let parsed_mail = mailparse::parse_mail( + b"Content-Type: text/plain\r\nFrom: Sender \r\nSubject: Test\r\n\r\nHello, world!", + ) + .unwrap(); + let tpl = Tpl::reply(&app, &parsed_mail); + + assert_eq!( + "From: Test \nTo: Sender \nSubject: Re: Test\n\n>Hello, world!", + tpl.to_string() + ); + } + + #[test] + fn reply_tpl_with_signature() { + let account = Account { + name: Some(String::from("Test")), + downloads_dir: None, + signature: Some(String::from("-- \nCordialement,")), + default_page_size: None, + default: Some(true), + email: String::from("test@localhost"), + watch_cmds: None, + imap_host: String::new(), + imap_port: 0, + imap_starttls: None, + imap_insecure: None, + imap_login: String::new(), + imap_passwd_cmd: String::new(), + smtp_host: String::new(), + smtp_port: 0, + smtp_starttls: None, + smtp_insecure: None, + smtp_login: String::new(), + smtp_passwd_cmd: String::new(), + }; + let config = Config { + name: String::new(), + downloads_dir: None, + notify_cmd: None, + signature: None, + default_page_size: None, + watch_cmds: None, + accounts: vec![(String::from("account"), account.clone())] + .into_iter() + .collect(), + }; + let output = Output::new("plain"); + let mbox = String::new(); + let arg_matches = clap::ArgMatches::new(); + let app = App::new(&config, &account, &output, &mbox, &arg_matches); + let parsed_mail = mailparse::parse_mail( + b"Content-Type: text/plain\r\nFrom: Sender \r\nSubject: Test\r\n\r\nHello, world!", + ) + .unwrap(); + let tpl = Tpl::reply(&app, &parsed_mail); + + assert_eq!( + "From: Test \nTo: Sender \nSubject: Re: Test\n\n>Hello, world!\n\n-- \nCordialement,", + tpl.to_string() + ); + } + + #[test] + fn reply_all_tpl() { + let account = Account { + name: Some(String::from("To")), + downloads_dir: None, + signature: None, + default_page_size: None, + default: Some(true), + email: String::from("to@localhost"), + watch_cmds: None, + imap_host: String::new(), + imap_port: 0, + imap_starttls: None, + imap_insecure: None, + imap_login: String::new(), + imap_passwd_cmd: String::new(), + smtp_host: String::new(), + smtp_port: 0, + smtp_starttls: None, + smtp_insecure: None, + smtp_login: String::new(), + smtp_passwd_cmd: String::new(), + }; + let config = Config { + name: String::new(), + downloads_dir: None, + notify_cmd: None, + signature: None, + default_page_size: None, + watch_cmds: None, + accounts: vec![(String::from("account"), account.clone())] + .into_iter() + .collect(), + }; + let output = Output::new("plain"); + let mbox = String::new(); + let arg_matches = clap::ArgMatches::new(); + let app = App::new(&config, &account, &output, &mbox, &arg_matches); + let parsed_mail = mailparse::parse_mail( + b"Message-Id: 1\r +Content-Type: text/plain\r +From: From \r +To: To ,to_bis@localhost\r +Cc: Cc , cc_bis@localhost\r +Subject: Test\r +\r +Hello, world!", + ) + .unwrap(); + let tpl = Tpl::reply_all(&app, &parsed_mail); + + assert_eq!( + "From: To +To: From , to_bis@localhost +In-Reply-To: 1 +Cc: Cc , cc_bis@localhost +Subject: Re: Test + +>Hello, world!", + tpl.to_string() + ); + } + + #[test] + fn reply_all_tpl_with_signature() { + let account = Account { + name: Some(String::from("Test")), + downloads_dir: None, + signature: Some(String::from("-- \nCordialement,")), + default_page_size: None, + default: Some(true), + email: String::from("test@localhost"), + watch_cmds: None, + imap_host: String::new(), + imap_port: 0, + imap_starttls: None, + imap_insecure: None, + imap_login: String::new(), + imap_passwd_cmd: String::new(), + smtp_host: String::new(), + smtp_port: 0, + smtp_starttls: None, + smtp_insecure: None, + smtp_login: String::new(), + smtp_passwd_cmd: String::new(), + }; + let config = Config { + name: String::new(), + downloads_dir: None, + notify_cmd: None, + signature: None, + default_page_size: None, + watch_cmds: None, + accounts: vec![(String::from("account"), account.clone())] + .into_iter() + .collect(), + }; + let output = Output::new("plain"); + let mbox = String::new(); + let arg_matches = clap::ArgMatches::new(); + let app = App::new(&config, &account, &output, &mbox, &arg_matches); + let parsed_mail = mailparse::parse_mail( + b"Content-Type: text/plain\r\nFrom: Sender \r\nSubject: Test\r\n\r\nHello, world!", + ) + .unwrap(); + let tpl = Tpl::reply(&app, &parsed_mail); + + assert_eq!( + "From: Test \nTo: Sender \nSubject: Re: Test\n\n>Hello, world!\n\n-- \nCordialement,", + tpl.to_string() + ); + } + + #[test] + fn forward_tpl() { + let account = Account { + name: Some(String::from("Test")), + downloads_dir: None, + signature: None, + default_page_size: None, + default: Some(true), + email: String::from("test@localhost"), + watch_cmds: None, + imap_host: String::new(), + imap_port: 0, + imap_starttls: None, + imap_insecure: None, + imap_login: String::new(), + imap_passwd_cmd: String::new(), + smtp_host: String::new(), + smtp_port: 0, + smtp_starttls: None, + smtp_insecure: None, + smtp_login: String::new(), + smtp_passwd_cmd: String::new(), + }; + let config = Config { + name: String::new(), + downloads_dir: None, + notify_cmd: None, + signature: None, + default_page_size: None, + watch_cmds: None, + accounts: vec![(String::from("account"), account.clone())] + .into_iter() + .collect(), + }; + let output = Output::new("plain"); + let mbox = String::new(); + let arg_matches = clap::ArgMatches::new(); + let app = App::new(&config, &account, &output, &mbox, &arg_matches); + let parsed_mail = mailparse::parse_mail( + b"Content-Type: text/plain\r\nFrom: Sender \r\nSubject: Test\r\n\r\nHello, world!", + ) + .unwrap(); + let tpl = Tpl::forward(&app, &parsed_mail); + + assert_eq!( + "From: Test \nTo: \nSubject: Fwd: Test\n\n-------- Forwarded Message --------\nHello, world!", + tpl.to_string() + ); + } + + #[test] + fn forward_tpl_with_signature() { + let account = Account { + name: Some(String::from("Test")), + downloads_dir: None, + signature: Some(String::from("-- \nCordialement,")), + default_page_size: None, + default: Some(true), + email: String::from("test@localhost"), + watch_cmds: None, + imap_host: String::new(), + imap_port: 0, + imap_starttls: None, + imap_insecure: None, + imap_login: String::new(), + imap_passwd_cmd: String::new(), + smtp_host: String::new(), + smtp_port: 0, + smtp_starttls: None, + smtp_insecure: None, + smtp_login: String::new(), + smtp_passwd_cmd: String::new(), + }; + let config = Config { + name: String::new(), + downloads_dir: None, + notify_cmd: None, + signature: None, + default_page_size: None, + watch_cmds: None, + accounts: vec![(String::from("account"), account.clone())] + .into_iter() + .collect(), + }; + let output = Output::new("plain"); + let mbox = String::new(); + let arg_matches = clap::ArgMatches::new(); + let app = App::new(&config, &account, &output, &mbox, &arg_matches); + let parsed_mail = mailparse::parse_mail( + b"Content-Type: text/plain\r\nFrom: Sender \r\nSubject: Test\r\n\r\nHello, world!", + ) + .unwrap(); + let tpl = Tpl::forward(&app, &parsed_mail); + + assert_eq!( + "From: Test \nTo: \nSubject: Fwd: Test\n\n-------- Forwarded Message --------\nHello, world!\n\n-- \nCordialement,", + tpl.to_string() + ); + } +}