mirror of
https://github.com/soywod/himalaya.git
synced 2024-07-05 17:15:12 +00:00
introduce --header arg for read command (#338)
This commit is contained in:
parent
eb6f51456b
commit
86b41e4914
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)),
|
||||||
)]
|
)]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue