diff --git a/Cargo.lock b/Cargo.lock index 027fa1f..9dd6278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -167,6 +167,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "convert_case" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8" + [[package]] name = "core-foundation" version = "0.9.2" @@ -443,6 +449,7 @@ dependencies = [ "atty", "chrono", "clap", + "convert_case", "env_logger", "erased-serde", "html-escape", diff --git a/Cargo.toml b/Cargo.toml index 642dab2..4cde60e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ anyhow = "1.0.44" atty = "0.2.14" chrono = "0.4.19" clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] } +convert_case = "0.5.0" env_logger = "0.8.3" erased-serde = "0.3.18" html-escape = "0.2.9" diff --git a/src/main.rs b/src/main.rs index 334005a..9c0b8af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -221,8 +221,8 @@ fn main() -> Result<()> { Some(msg_args::Cmd::Move(seq, mbox_dst)) => { return msg_handlers::move_(seq, mbox, mbox_dst, &mut printer, backend); } - Some(msg_args::Cmd::Read(seq, text_mime, raw)) => { - return msg_handlers::read(seq, text_mime, raw, mbox, &mut printer, backend); + Some(msg_args::Cmd::Read(seq, text_mime, raw, headers)) => { + return msg_handlers::read(seq, text_mime, raw, headers, mbox, &mut printer, backend); } Some(msg_args::Cmd::Reply(seq, all, attachment_paths, encrypt)) => { return msg_handlers::reply( diff --git a/src/msg/msg_args.rs b/src/msg/msg_args.rs index 12ccbde..32e02b6 100644 --- a/src/msg/msg_args.rs +++ b/src/msg/msg_args.rs @@ -25,6 +25,7 @@ type AttachmentPaths<'a> = Vec<&'a str>; type MaxTableWidth = Option; type Encrypt = bool; type Criteria = String; +type Headers<'a> = Vec<&'a str>; /// Message commands. #[derive(Debug, PartialEq, Eq)] @@ -35,7 +36,7 @@ pub enum Cmd<'a> { Forward(Seq<'a>, AttachmentPaths<'a>, Encrypt), List(MaxTableWidth, Option, Page), Move(Seq<'a>, Mbox<'a>), - Read(Seq<'a>, TextMime<'a>, Raw), + Read(Seq<'a>, TextMime<'a>, Raw, Headers<'a>), Reply(Seq<'a>, All, AttachmentPaths<'a>, Encrypt), Save(RawMsg<'a>), Search(Query, MaxTableWidth, Option, Page), @@ -121,7 +122,9 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { debug!("text mime: {}", mime); let raw = m.is_present("raw"); debug!("raw: {}", raw); - return Ok(Some(Cmd::Read(seq, mime, raw))); + let headers: Vec<&str> = m.values_of("headers").unwrap_or_default().collect(); + debug!("headers: {:?}", headers); + return Ok(Some(Cmd::Read(seq, mime, raw, headers))); } if let Some(m) = m.subcommand_matches("reply") { @@ -318,7 +321,7 @@ fn page_arg<'a>() -> Arg<'a, 'a> { } /// Message attachment argument. -pub fn attachment_arg<'a>() -> Arg<'a, 'a> { +pub fn attachments_arg<'a>() -> Arg<'a, 'a> { Arg::with_name("attachments") .help("Adds attachment to the message") .short("a") @@ -327,6 +330,16 @@ pub fn attachment_arg<'a>() -> Arg<'a, 'a> { .multiple(true) } +/// Represents the message headers argument. +pub fn headers_arg<'a>() -> Arg<'a, 'a> { + Arg::with_name("headers") + .help("Shows additional headers with the message") + .short("h") + .long("header") + .value_name("STR") + .multiple(true) +} + /// Message encrypt argument. pub fn encrypt_arg<'a>() -> Arg<'a, 'a> { Arg::with_name("encrypt") @@ -399,7 +412,7 @@ pub fn subcmds<'a>() -> Vec> { ), SubCommand::with_name("write") .about("Writes a new message") - .arg(attachment_arg()) + .arg(attachments_arg()) .arg(encrypt_arg()), SubCommand::with_name("send") .about("Sends a raw message") @@ -424,19 +437,20 @@ pub fn subcmds<'a>() -> Vec> { .help("Reads raw message") .long("raw") .short("r"), - ), + ) + .arg(headers_arg()), SubCommand::with_name("reply") .aliases(&["rep", "r"]) .about("Answers to a message") .arg(seq_arg()) .arg(reply_all_arg()) - .arg(attachment_arg()) + .arg(attachments_arg()) .arg(encrypt_arg()), SubCommand::with_name("forward") .aliases(&["fwd", "f"]) .about("Forwards a message") .arg(seq_arg()) - .arg(attachment_arg()) + .arg(attachments_arg()) .arg(encrypt_arg()), SubCommand::with_name("copy") .aliases(&["cp", "c"]) diff --git a/src/msg/msg_entity.rs b/src/msg/msg_entity.rs index edbc867..7d34adb 100644 --- a/src/msg/msg_entity.rs +++ b/src/msg/msg_entity.rs @@ -1,11 +1,19 @@ use ammonia; use anyhow::{anyhow, Context, Error, Result}; use chrono::{DateTime, FixedOffset}; +use convert_case::{Case, Casing}; use html_escape; use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart}; use log::{debug, info, trace, warn}; use regex::Regex; -use std::{collections::HashSet, convert::TryInto, env::temp_dir, fmt::Debug, fs, path::PathBuf}; +use std::{ + collections::{HashMap, HashSet}, + convert::TryInto, + env::temp_dir, + fmt::Debug, + fs, + path::PathBuf, +}; use uuid::Uuid; use crate::{ @@ -41,6 +49,7 @@ pub struct Msg { pub bcc: Option, pub in_reply_to: Option, pub message_id: Option, + pub headers: HashMap, /// The internal date of the message. /// @@ -665,9 +674,11 @@ impl Msg { "message-id" => msg.message_id = Some(val), "in-reply-to" => msg.in_reply_to = Some(val), "subject" => { - msg.subject = val; + msg.subject = rfc2047_decoder::decode(val.as_bytes())?; } "date" => { + // TODO: use date format instead + // https://github.com/jonhoo/rust-imap/blob/afbc5118f251da4e3f6a1e560e749c0700020b54/src/types/fetch.rs#L16 msg.date = DateTime::parse_from_rfc2822( val.split_at(val.find(" (").unwrap_or_else(|| val.len())).0, ) @@ -697,7 +708,12 @@ impl Msg { msg.bcc = from_slice_to_addrs(val) .context(format!("cannot parse header {:?}", key))? } - _ => (), + key => { + msg.headers.insert( + key.to_owned(), + rfc2047_decoder::decode(val.as_bytes()).unwrap_or(val), + ); + } } } @@ -708,6 +724,78 @@ impl Msg { info!("end: building message from parsed mail"); Ok(msg) } + + /// Transforms a message into a readable string. A readable + /// message is like a template, except that: + /// - headers part is customizable (can be omitted if empty filter given in argument) + /// - body type is customizable (plain or html) + pub fn to_readable_string(&self, text_mime: &str, headers: Vec<&str>) -> Result { + let mut readable_msg = String::new(); + + for h in headers { + match h.to_lowercase().as_str() { + "message-id" => match self.message_id { + Some(ref message_id) if !message_id.is_empty() => { + readable_msg.push_str(&format!("Message-Id: {}\n", message_id)); + } + _ => (), + }, + "in-reply-to" => match self.in_reply_to { + Some(ref in_reply_to) if !in_reply_to.is_empty() => { + readable_msg.push_str(&format!("In-Reply-To: {}\n", in_reply_to)); + } + _ => (), + }, + "subject" => { + readable_msg.push_str(&format!("Subject: {}\n", self.subject)); + } + "date" => { + if let Some(ref date) = self.date { + readable_msg.push_str(&format!("Date: {}\n", date)); + } + } + "from" => match self.from { + Some(ref addrs) if !addrs.is_empty() => { + readable_msg.push_str(&format!("From: {}\n", addrs)); + } + _ => (), + }, + "to" => match self.to { + Some(ref addrs) if !addrs.is_empty() => { + readable_msg.push_str(&format!("To: {}\n", addrs)); + } + _ => (), + }, + "reply-to" => match self.reply_to { + Some(ref addrs) if !addrs.is_empty() => { + readable_msg.push_str(&format!("Reply-To: {}\n", addrs)); + } + _ => (), + }, + "cc" => match self.cc { + Some(ref addrs) if !addrs.is_empty() => { + readable_msg.push_str(&format!("Cc: {}\n", addrs)); + } + _ => (), + }, + "bcc" => match self.bcc { + Some(ref addrs) if !addrs.is_empty() => { + readable_msg.push_str(&format!("Bcc: {}\n", addrs)); + } + _ => (), + }, + key => match self.headers.get(key) { + Some(ref val) if !val.is_empty() => { + readable_msg.push_str(&format!("{}: {}\n", key.to_case(Case::Pascal), val)); + } + _ => (), + }, + }; + } + readable_msg.push_str("\n"); + readable_msg.push_str(&self.fold_text_parts(text_mime)); + Ok(readable_msg) + } } impl TryInto for Msg { diff --git a/src/msg/msg_handlers.rs b/src/msg/msg_handlers.rs index d58e737..4728565 100644 --- a/src/msg/msg_handlers.rs +++ b/src/msg/msg_handlers.rs @@ -207,19 +207,19 @@ pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>( seq: &str, text_mime: &str, raw: bool, + headers: Vec<&str>, mbox: &str, printer: &mut P, backend: Box<&'a mut B>, ) -> Result<()> { let msg = backend.get_msg(mbox, seq)?; - let msg = if raw { + + printer.print_struct(if raw { // Emails don't always have valid utf8. Using "lossy" to display what we can. String::from_utf8_lossy(&msg.raw).into_owned() } else { - msg.fold_text_parts(text_mime) - }; - - printer.print_struct(msg) + msg.to_readable_string(text_mime, headers)? + }) } /// Reply to the given message UID. diff --git a/src/msg/tpl_args.rs b/src/msg/tpl_args.rs index 5379acc..e3254ff 100644 --- a/src/msg/tpl_args.rs +++ b/src/msg/tpl_args.rs @@ -183,13 +183,13 @@ pub fn subcmds<'a>() -> Vec> { .subcommand( SubCommand::with_name("save") .about("Saves a message based on the given template") - .arg(&msg_args::attachment_arg()) + .arg(&msg_args::attachments_arg()) .arg(Arg::with_name("template").raw(true)), ) .subcommand( SubCommand::with_name("send") .about("Sends a message based on the given template") - .arg(&msg_args::attachment_arg()) + .arg(&msg_args::attachments_arg()) .arg(Arg::with_name("template").raw(true)), )] }