introduce --header arg for read command (#338)

This commit is contained in:
Clément DOUIN 2022-03-12 13:05:57 +01:00
parent eb6f51456b
commit 86b41e4914
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
7 changed files with 129 additions and 19 deletions

7
Cargo.lock generated
View file

@ -167,6 +167,12 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "convert_case"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8"
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.2" version = "0.9.2"
@ -443,6 +449,7 @@ dependencies = [
"atty", "atty",
"chrono", "chrono",
"clap", "clap",
"convert_case",
"env_logger", "env_logger",
"erased-serde", "erased-serde",
"html-escape", "html-escape",

View file

@ -28,6 +28,7 @@ anyhow = "1.0.44"
atty = "0.2.14" atty = "0.2.14"
chrono = "0.4.19" chrono = "0.4.19"
clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] } clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] }
convert_case = "0.5.0"
env_logger = "0.8.3" env_logger = "0.8.3"
erased-serde = "0.3.18" erased-serde = "0.3.18"
html-escape = "0.2.9" html-escape = "0.2.9"

View file

@ -221,8 +221,8 @@ fn main() -> Result<()> {
Some(msg_args::Cmd::Move(seq, mbox_dst)) => { Some(msg_args::Cmd::Move(seq, mbox_dst)) => {
return msg_handlers::move_(seq, mbox, mbox_dst, &mut printer, backend); return msg_handlers::move_(seq, mbox, mbox_dst, &mut printer, backend);
} }
Some(msg_args::Cmd::Read(seq, text_mime, raw)) => { Some(msg_args::Cmd::Read(seq, text_mime, raw, headers)) => {
return msg_handlers::read(seq, text_mime, raw, mbox, &mut printer, backend); return msg_handlers::read(seq, text_mime, raw, headers, mbox, &mut printer, backend);
} }
Some(msg_args::Cmd::Reply(seq, all, attachment_paths, encrypt)) => { Some(msg_args::Cmd::Reply(seq, all, attachment_paths, encrypt)) => {
return msg_handlers::reply( return msg_handlers::reply(

View file

@ -25,6 +25,7 @@ type AttachmentPaths<'a> = Vec<&'a str>;
type MaxTableWidth = Option<usize>; type MaxTableWidth = Option<usize>;
type Encrypt = bool; type Encrypt = bool;
type Criteria = String; type Criteria = String;
type Headers<'a> = Vec<&'a str>;
/// Message commands. /// Message commands.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@ -35,7 +36,7 @@ pub enum Cmd<'a> {
Forward(Seq<'a>, AttachmentPaths<'a>, Encrypt), Forward(Seq<'a>, AttachmentPaths<'a>, Encrypt),
List(MaxTableWidth, Option<PageSize>, Page), List(MaxTableWidth, Option<PageSize>, Page),
Move(Seq<'a>, Mbox<'a>), 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), Reply(Seq<'a>, All, AttachmentPaths<'a>, Encrypt),
Save(RawMsg<'a>), Save(RawMsg<'a>),
Search(Query, MaxTableWidth, Option<PageSize>, Page), Search(Query, MaxTableWidth, Option<PageSize>, Page),
@ -121,7 +122,9 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
debug!("text mime: {}", mime); debug!("text mime: {}", mime);
let raw = m.is_present("raw"); let raw = m.is_present("raw");
debug!("raw: {}", 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") { if let Some(m) = m.subcommand_matches("reply") {
@ -318,7 +321,7 @@ fn page_arg<'a>() -> Arg<'a, 'a> {
} }
/// Message attachment argument. /// Message attachment argument.
pub fn attachment_arg<'a>() -> Arg<'a, 'a> { pub fn attachments_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("attachments") Arg::with_name("attachments")
.help("Adds attachment to the message") .help("Adds attachment to the message")
.short("a") .short("a")
@ -327,6 +330,16 @@ pub fn attachment_arg<'a>() -> Arg<'a, 'a> {
.multiple(true) .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. /// Message encrypt argument.
pub fn encrypt_arg<'a>() -> Arg<'a, 'a> { pub fn encrypt_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("encrypt") Arg::with_name("encrypt")
@ -399,7 +412,7 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
), ),
SubCommand::with_name("write") SubCommand::with_name("write")
.about("Writes a new message") .about("Writes a new message")
.arg(attachment_arg()) .arg(attachments_arg())
.arg(encrypt_arg()), .arg(encrypt_arg()),
SubCommand::with_name("send") SubCommand::with_name("send")
.about("Sends a raw message") .about("Sends a raw message")
@ -424,19 +437,20 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
.help("Reads raw message") .help("Reads raw message")
.long("raw") .long("raw")
.short("r"), .short("r"),
), )
.arg(headers_arg()),
SubCommand::with_name("reply") SubCommand::with_name("reply")
.aliases(&["rep", "r"]) .aliases(&["rep", "r"])
.about("Answers to a message") .about("Answers to a message")
.arg(seq_arg()) .arg(seq_arg())
.arg(reply_all_arg()) .arg(reply_all_arg())
.arg(attachment_arg()) .arg(attachments_arg())
.arg(encrypt_arg()), .arg(encrypt_arg()),
SubCommand::with_name("forward") SubCommand::with_name("forward")
.aliases(&["fwd", "f"]) .aliases(&["fwd", "f"])
.about("Forwards a message") .about("Forwards a message")
.arg(seq_arg()) .arg(seq_arg())
.arg(attachment_arg()) .arg(attachments_arg())
.arg(encrypt_arg()), .arg(encrypt_arg()),
SubCommand::with_name("copy") SubCommand::with_name("copy")
.aliases(&["cp", "c"]) .aliases(&["cp", "c"])

View file

@ -1,11 +1,19 @@
use ammonia; use ammonia;
use anyhow::{anyhow, Context, Error, Result}; use anyhow::{anyhow, Context, Error, Result};
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
use convert_case::{Case, Casing};
use html_escape; use html_escape;
use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart}; use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart};
use log::{debug, info, trace, warn}; use log::{debug, info, trace, warn};
use regex::Regex; 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 uuid::Uuid;
use crate::{ use crate::{
@ -41,6 +49,7 @@ pub struct Msg {
pub bcc: Option<Addrs>, pub bcc: Option<Addrs>,
pub in_reply_to: Option<String>, pub in_reply_to: Option<String>,
pub message_id: Option<String>, pub message_id: Option<String>,
pub headers: HashMap<String, String>,
/// The internal date of the message. /// The internal date of the message.
/// ///
@ -665,9 +674,11 @@ impl Msg {
"message-id" => msg.message_id = Some(val), "message-id" => msg.message_id = Some(val),
"in-reply-to" => msg.in_reply_to = Some(val), "in-reply-to" => msg.in_reply_to = Some(val),
"subject" => { "subject" => {
msg.subject = val; msg.subject = rfc2047_decoder::decode(val.as_bytes())?;
} }
"date" => { "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( msg.date = DateTime::parse_from_rfc2822(
val.split_at(val.find(" (").unwrap_or_else(|| val.len())).0, val.split_at(val.find(" (").unwrap_or_else(|| val.len())).0,
) )
@ -697,7 +708,12 @@ impl Msg {
msg.bcc = from_slice_to_addrs(val) msg.bcc = from_slice_to_addrs(val)
.context(format!("cannot parse header {:?}", key))? .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"); info!("end: building message from parsed mail");
Ok(msg) 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<String> {
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<lettre::address::Envelope> for Msg { impl TryInto<lettre::address::Envelope> for Msg {

View file

@ -207,19 +207,19 @@ pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str, seq: &str,
text_mime: &str, text_mime: &str,
raw: bool, raw: bool,
headers: Vec<&str>,
mbox: &str, mbox: &str,
printer: &mut P, printer: &mut P,
backend: Box<&'a mut B>, backend: Box<&'a mut B>,
) -> Result<()> { ) -> Result<()> {
let msg = backend.get_msg(mbox, seq)?; 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. // Emails don't always have valid utf8. Using "lossy" to display what we can.
String::from_utf8_lossy(&msg.raw).into_owned() String::from_utf8_lossy(&msg.raw).into_owned()
} else { } else {
msg.fold_text_parts(text_mime) msg.to_readable_string(text_mime, headers)?
}; })
printer.print_struct(msg)
} }
/// Reply to the given message UID. /// Reply to the given message UID.

View file

@ -183,13 +183,13 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
.subcommand( .subcommand(
SubCommand::with_name("save") SubCommand::with_name("save")
.about("Saves a message based on the given template") .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)), .arg(Arg::with_name("template").raw(true)),
) )
.subcommand( .subcommand(
SubCommand::with_name("send") SubCommand::with_name("send")
.about("Sends a message based on the given template") .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)), .arg(Arg::with_name("template").raw(true)),
)] )]
} }