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",
]
[[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",

View file

@ -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"

View file

@ -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(

View file

@ -25,6 +25,7 @@ type AttachmentPaths<'a> = Vec<&'a str>;
type MaxTableWidth = Option<usize>;
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<PageSize>, 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<PageSize>, Page),
@ -121,7 +122,9 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
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<App<'a, 'a>> {
),
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<App<'a, 'a>> {
.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"])

View file

@ -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<Addrs>,
pub in_reply_to: Option<String>,
pub message_id: Option<String>,
pub headers: HashMap<String, String>,
/// 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<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 {

View file

@ -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.

View file

@ -183,13 +183,13 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
.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)),
)]
}