mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-19 06:37:55 +08:00
use cargo workspace feature (#340)
For now, everything has been moved as it is in the "cli" workspace. The next step is to separate the "lib" from the "cli".
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
//! Module related to email addresses.
|
||||
//!
|
||||
//! This module regroups email address entities and converters.
|
||||
|
||||
use anyhow::Result;
|
||||
use mailparse;
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Defines a single email address.
|
||||
pub type Addr = mailparse::MailAddr;
|
||||
|
||||
/// Defines a list of email addresses.
|
||||
pub type Addrs = mailparse::MailAddrList;
|
||||
|
||||
/// Converts a slice into an optional list of addresses.
|
||||
pub fn from_slice_to_addrs<S: AsRef<str> + Debug>(addrs: S) -> Result<Option<Addrs>> {
|
||||
let addrs = mailparse::addrparse(addrs.as_ref())?;
|
||||
Ok(if addrs.is_empty() { None } else { Some(addrs) })
|
||||
}
|
||||
|
||||
/// Converts a list of addresses into a list of [`lettre::message::Mailbox`].
|
||||
pub fn from_addrs_to_sendable_mbox(addrs: &Addrs) -> Result<Vec<lettre::message::Mailbox>> {
|
||||
let mut sendable_addrs: Vec<lettre::message::Mailbox> = vec![];
|
||||
for addr in addrs.iter() {
|
||||
match addr {
|
||||
Addr::Single(mailparse::SingleInfo { display_name, addr }) => sendable_addrs.push(
|
||||
lettre::message::Mailbox::new(display_name.clone(), addr.parse()?),
|
||||
),
|
||||
Addr::Group(mailparse::GroupInfo { group_name, addrs }) => {
|
||||
for addr in addrs {
|
||||
sendable_addrs.push(lettre::message::Mailbox::new(
|
||||
addr.display_name.clone().or(Some(group_name.clone())),
|
||||
addr.to_string().parse()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(sendable_addrs)
|
||||
}
|
||||
|
||||
/// Converts a list of addresses into a list of [`lettre::Address`].
|
||||
pub fn from_addrs_to_sendable_addrs(addrs: &Addrs) -> Result<Vec<lettre::Address>> {
|
||||
let mut sendable_addrs = vec![];
|
||||
for addr in addrs.iter() {
|
||||
match addr {
|
||||
mailparse::MailAddr::Single(mailparse::SingleInfo {
|
||||
display_name: _,
|
||||
addr,
|
||||
}) => {
|
||||
sendable_addrs.push(addr.parse()?);
|
||||
}
|
||||
mailparse::MailAddr::Group(mailparse::GroupInfo {
|
||||
group_name: _,
|
||||
addrs,
|
||||
}) => {
|
||||
for addr in addrs {
|
||||
sendable_addrs.push(addr.addr.parse()?);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(sendable_addrs)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use std::{any, fmt};
|
||||
|
||||
use crate::output::PrintTable;
|
||||
|
||||
pub trait Envelopes: fmt::Debug + erased_serde::Serialize + PrintTable + any::Any {
|
||||
fn as_any(&self) -> &dyn any::Any;
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug + erased_serde::Serialize + PrintTable + any::Any> Envelopes for T {
|
||||
fn as_any(&self) -> &dyn any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//! Message flag CLI module.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher related to the message flag
|
||||
//! domain.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::msg::msg_args;
|
||||
|
||||
type SeqRange<'a> = &'a str;
|
||||
type Flags = String;
|
||||
|
||||
/// Represents the flag commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
/// Represents the add flags command.
|
||||
Add(SeqRange<'a>, Flags),
|
||||
/// Represents the set flags command.
|
||||
Set(SeqRange<'a>, Flags),
|
||||
/// Represents the remove flags command.
|
||||
Remove(SeqRange<'a>, Flags),
|
||||
}
|
||||
|
||||
/// Defines the flag command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
info!("entering message flag command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("add") {
|
||||
info!("add subcommand matched");
|
||||
let seq_range = m.value_of("seq-range").unwrap();
|
||||
debug!("seq range: {}", seq_range);
|
||||
let flags: String = m
|
||||
.values_of("flags")
|
||||
.unwrap_or_default()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
debug!("flags: {:?}", flags);
|
||||
return Ok(Some(Cmd::Add(seq_range, flags)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("set") {
|
||||
info!("set subcommand matched");
|
||||
let seq_range = m.value_of("seq-range").unwrap();
|
||||
debug!("seq range: {}", seq_range);
|
||||
let flags: String = m
|
||||
.values_of("flags")
|
||||
.unwrap_or_default()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
debug!("flags: {:?}", flags);
|
||||
return Ok(Some(Cmd::Set(seq_range, flags)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("remove") {
|
||||
info!("remove subcommand matched");
|
||||
let seq_range = m.value_of("seq-range").unwrap();
|
||||
debug!("seq range: {}", seq_range);
|
||||
let flags: String = m
|
||||
.values_of("flags")
|
||||
.unwrap_or_default()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
debug!("flags: {:?}", flags);
|
||||
return Ok(Some(Cmd::Remove(seq_range, flags)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Defines the flags argument.
|
||||
fn flags_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("flags")
|
||||
.help("IMAP flags")
|
||||
.long_help("IMAP flags. Flags are case-insensitive, and they do not need to be prefixed with `\\`.")
|
||||
.value_name("FLAGS…")
|
||||
.multiple(true)
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Contains flag subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name("flag")
|
||||
.aliases(&["flags", "flg"])
|
||||
.about("Handles flags")
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand(
|
||||
SubCommand::with_name("add")
|
||||
.aliases(&["a"])
|
||||
.about("Adds flags to a message")
|
||||
.arg(msg_args::seq_range_arg())
|
||||
.arg(flags_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("set")
|
||||
.aliases(&["s", "change", "c"])
|
||||
.about("Replaces all message flags")
|
||||
.arg(msg_args::seq_range_arg())
|
||||
.arg(flags_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("remove")
|
||||
.aliases(&["rem", "rm", "r", "delete", "del", "d"])
|
||||
.about("Removes flags from a message")
|
||||
.arg(msg_args::seq_range_arg())
|
||||
.arg(flags_arg()),
|
||||
)]
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//! Message flag handling module.
|
||||
//!
|
||||
//! This module gathers all flag actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{backends::Backend, output::PrinterService};
|
||||
|
||||
/// Adds flags to all messages matching the given sequence range.
|
||||
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
|
||||
pub fn add<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &'a str,
|
||||
flags: &'a str,
|
||||
mbox: &'a str,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.add_flags(mbox, seq_range, flags)?;
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully added to message(s) {:?}",
|
||||
flags, seq_range
|
||||
))
|
||||
}
|
||||
|
||||
/// Removes flags from all messages matching the given sequence range.
|
||||
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
|
||||
pub fn remove<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &'a str,
|
||||
flags: &'a str,
|
||||
mbox: &'a str,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.del_flags(mbox, seq_range, flags)?;
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully removed from message(s) {:?}",
|
||||
flags, seq_range
|
||||
))
|
||||
}
|
||||
|
||||
/// Replaces flags of all messages matching the given sequence range.
|
||||
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
|
||||
pub fn set<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &'a str,
|
||||
flags: &'a str,
|
||||
mbox: &'a str,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.set_flags(mbox, seq_range, flags)?;
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully set for message(s) {:?}",
|
||||
flags, seq_range
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
//! Module related to message CLI.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher related to message.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, Arg, ArgMatches, SubCommand};
|
||||
use log::{debug, info, trace};
|
||||
|
||||
use crate::{
|
||||
mbox::mbox_args,
|
||||
msg::{flag_args, msg_args, tpl_args},
|
||||
ui::table_arg,
|
||||
};
|
||||
|
||||
type Seq<'a> = &'a str;
|
||||
type PageSize = usize;
|
||||
type Page = usize;
|
||||
type Mbox<'a> = &'a str;
|
||||
type TextMime<'a> = &'a str;
|
||||
type Raw = bool;
|
||||
type All = bool;
|
||||
type RawMsg<'a> = &'a str;
|
||||
type Query = String;
|
||||
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)]
|
||||
pub enum Cmd<'a> {
|
||||
Attachments(Seq<'a>),
|
||||
Copy(Seq<'a>, Mbox<'a>),
|
||||
Delete(Seq<'a>),
|
||||
Forward(Seq<'a>, AttachmentPaths<'a>, Encrypt),
|
||||
List(MaxTableWidth, Option<PageSize>, Page),
|
||||
Move(Seq<'a>, Mbox<'a>),
|
||||
Read(Seq<'a>, TextMime<'a>, Raw, Headers<'a>),
|
||||
Reply(Seq<'a>, All, AttachmentPaths<'a>, Encrypt),
|
||||
Save(RawMsg<'a>),
|
||||
Search(Query, MaxTableWidth, Option<PageSize>, Page),
|
||||
Sort(Criteria, Query, MaxTableWidth, Option<PageSize>, Page),
|
||||
Send(RawMsg<'a>),
|
||||
Write(AttachmentPaths<'a>, Encrypt),
|
||||
|
||||
Flag(Option<flag_args::Cmd<'a>>),
|
||||
Tpl(Option<tpl_args::Cmd<'a>>),
|
||||
}
|
||||
|
||||
/// Message command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
info!("entering message command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("attachments") {
|
||||
info!("attachments command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
return Ok(Some(Cmd::Attachments(seq)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("copy") {
|
||||
info!("copy command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
let mbox = m.value_of("mbox-target").unwrap();
|
||||
debug!(r#"target mailbox: "{:?}""#, mbox);
|
||||
return Ok(Some(Cmd::Copy(seq, mbox)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("delete") {
|
||||
info!("copy command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
return Ok(Some(Cmd::Delete(seq)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("forward") {
|
||||
info!("forward command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
debug!("attachments paths: {:?}", paths);
|
||||
let encrypt = m.is_present("encrypt");
|
||||
debug!("encrypt: {}", encrypt);
|
||||
return Ok(Some(Cmd::Forward(seq, paths, encrypt)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("list") {
|
||||
info!("list command matched");
|
||||
let max_table_width = m
|
||||
.value_of("max-table-width")
|
||||
.and_then(|width| width.parse::<usize>().ok());
|
||||
debug!("max table width: {:?}", max_table_width);
|
||||
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
|
||||
debug!("page size: {:?}", page_size);
|
||||
let page = m
|
||||
.value_of("page")
|
||||
.unwrap_or("1")
|
||||
.parse()
|
||||
.ok()
|
||||
.map(|page| 1.max(page) - 1)
|
||||
.unwrap_or_default();
|
||||
debug!("page: {}", page);
|
||||
return Ok(Some(Cmd::List(max_table_width, page_size, page)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("move") {
|
||||
info!("move command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
let mbox = m.value_of("mbox-target").unwrap();
|
||||
debug!("target mailbox: {:?}", mbox);
|
||||
return Ok(Some(Cmd::Move(seq, mbox)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("read") {
|
||||
info!("read command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
let mime = m.value_of("mime-type").unwrap();
|
||||
debug!("text mime: {}", mime);
|
||||
let raw = m.is_present("raw");
|
||||
debug!("raw: {}", 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") {
|
||||
info!("reply command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
let all = m.is_present("reply-all");
|
||||
debug!("reply all: {}", all);
|
||||
let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
debug!("attachments paths: {:?}", paths);
|
||||
let encrypt = m.is_present("encrypt");
|
||||
debug!("encrypt: {}", encrypt);
|
||||
|
||||
return Ok(Some(Cmd::Reply(seq, all, paths, encrypt)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("save") {
|
||||
info!("save command matched");
|
||||
let msg = m.value_of("message").unwrap_or_default();
|
||||
trace!("message: {}", msg);
|
||||
return Ok(Some(Cmd::Save(msg)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("search") {
|
||||
info!("search command matched");
|
||||
let max_table_width = m
|
||||
.value_of("max-table-width")
|
||||
.and_then(|width| width.parse::<usize>().ok());
|
||||
debug!("max table width: {:?}", max_table_width);
|
||||
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
|
||||
debug!("page size: {:?}", page_size);
|
||||
let page = m
|
||||
.value_of("page")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.ok()
|
||||
.map(|page| 1.max(page) - 1)
|
||||
.unwrap_or_default();
|
||||
debug!("page: {}", page);
|
||||
let query = m
|
||||
.values_of("query")
|
||||
.unwrap_or_default()
|
||||
.fold((false, vec![]), |(escape, mut cmds), cmd| {
|
||||
match (cmd, escape) {
|
||||
// Next command is an arg and needs to be escaped
|
||||
("subject", _) | ("body", _) | ("text", _) => {
|
||||
cmds.push(cmd.to_string());
|
||||
(true, cmds)
|
||||
}
|
||||
// Escaped arg commands
|
||||
(_, true) => {
|
||||
cmds.push(format!("\"{}\"", cmd));
|
||||
(false, cmds)
|
||||
}
|
||||
// Regular commands
|
||||
(_, false) => {
|
||||
cmds.push(cmd.to_string());
|
||||
(false, cmds)
|
||||
}
|
||||
}
|
||||
})
|
||||
.1
|
||||
.join(" ");
|
||||
debug!("query: {}", query);
|
||||
return Ok(Some(Cmd::Search(query, max_table_width, page_size, page)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("sort") {
|
||||
info!("sort command matched");
|
||||
let max_table_width = m
|
||||
.value_of("max-table-width")
|
||||
.and_then(|width| width.parse::<usize>().ok());
|
||||
debug!("max table width: {:?}", max_table_width);
|
||||
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
|
||||
debug!("page size: {:?}", page_size);
|
||||
let page = m
|
||||
.value_of("page")
|
||||
.unwrap()
|
||||
.parse()
|
||||
.ok()
|
||||
.map(|page| 1.max(page) - 1)
|
||||
.unwrap_or_default();
|
||||
debug!("page: {:?}", page);
|
||||
let criteria = m
|
||||
.values_of("criterion")
|
||||
.unwrap_or_default()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
debug!("criteria: {:?}", criteria);
|
||||
let query = m
|
||||
.values_of("query")
|
||||
.unwrap_or_default()
|
||||
.fold((false, vec![]), |(escape, mut cmds), cmd| {
|
||||
match (cmd, escape) {
|
||||
// Next command is an arg and needs to be escaped
|
||||
("subject", _) | ("body", _) | ("text", _) => {
|
||||
cmds.push(cmd.to_string());
|
||||
(true, cmds)
|
||||
}
|
||||
// Escaped arg commands
|
||||
(_, true) => {
|
||||
cmds.push(format!("\"{}\"", cmd));
|
||||
(false, cmds)
|
||||
}
|
||||
// Regular commands
|
||||
(_, false) => {
|
||||
cmds.push(cmd.to_string());
|
||||
(false, cmds)
|
||||
}
|
||||
}
|
||||
})
|
||||
.1
|
||||
.join(" ");
|
||||
debug!("query: {:?}", query);
|
||||
return Ok(Some(Cmd::Sort(
|
||||
criteria,
|
||||
query,
|
||||
max_table_width,
|
||||
page_size,
|
||||
page,
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("send") {
|
||||
info!("send command matched");
|
||||
let msg = m.value_of("message").unwrap_or_default();
|
||||
trace!("message: {}", msg);
|
||||
return Ok(Some(Cmd::Send(msg)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("write") {
|
||||
info!("write command matched");
|
||||
let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
debug!("attachments paths: {:?}", attachment_paths);
|
||||
let encrypt = m.is_present("encrypt");
|
||||
debug!("encrypt: {}", encrypt);
|
||||
return Ok(Some(Cmd::Write(attachment_paths, encrypt)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("template") {
|
||||
return Ok(Some(Cmd::Tpl(tpl_args::matches(m)?)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("flag") {
|
||||
return Ok(Some(Cmd::Flag(flag_args::matches(m)?)));
|
||||
}
|
||||
|
||||
info!("default list command matched");
|
||||
Ok(Some(Cmd::List(None, None, 0)))
|
||||
}
|
||||
|
||||
/// Message sequence number argument.
|
||||
pub fn seq_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("seq")
|
||||
.help("Specifies the targetted message")
|
||||
.value_name("SEQ")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Message sequence range argument.
|
||||
pub fn seq_range_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("seq-range")
|
||||
.help("Specifies targetted message(s)")
|
||||
.long_help("Specifies a range of targetted messages. The range follows the [RFC3501](https://datatracker.ietf.org/doc/html/rfc3501#section-9) format: `1:5` matches messages with sequence number between 1 and 5, `1,5` matches messages with sequence number 1 or 5, * matches all messages.")
|
||||
.value_name("SEQ")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Message reply all argument.
|
||||
pub fn reply_all_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("reply-all")
|
||||
.help("Includes all recipients")
|
||||
.short("A")
|
||||
.long("all")
|
||||
}
|
||||
|
||||
/// Message page size argument.
|
||||
fn page_size_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("page-size")
|
||||
.help("Page size")
|
||||
.short("s")
|
||||
.long("size")
|
||||
.value_name("INT")
|
||||
}
|
||||
|
||||
/// Message page argument.
|
||||
fn page_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("page")
|
||||
.help("Page number")
|
||||
.short("p")
|
||||
.long("page")
|
||||
.value_name("INT")
|
||||
.default_value("0")
|
||||
}
|
||||
|
||||
/// Message attachment argument.
|
||||
pub fn attachments_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("attachments")
|
||||
.help("Adds attachment to the message")
|
||||
.short("a")
|
||||
.long("attachment")
|
||||
.value_name("PATH")
|
||||
.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")
|
||||
.help("Encrypts the message")
|
||||
.short("e")
|
||||
.long("encrypt")
|
||||
}
|
||||
|
||||
/// Message subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![
|
||||
flag_args::subcmds(),
|
||||
tpl_args::subcmds(),
|
||||
vec![
|
||||
SubCommand::with_name("attachments")
|
||||
.aliases(&["attachment", "att", "a"])
|
||||
.about("Downloads all message attachments")
|
||||
.arg(msg_args::seq_arg()),
|
||||
SubCommand::with_name("list")
|
||||
.aliases(&["lst", "l"])
|
||||
.about("Lists all messages")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table_arg::max_width()),
|
||||
SubCommand::with_name("search")
|
||||
.aliases(&["s", "query", "q"])
|
||||
.about("Lists messages matching the given IMAP query")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table_arg::max_width())
|
||||
.arg(
|
||||
Arg::with_name("query")
|
||||
.help("IMAP query")
|
||||
.long_help("The IMAP query format follows the [RFC3501](https://tools.ietf.org/html/rfc3501#section-6.4.4). The query is case-insensitive.")
|
||||
.value_name("QUERY")
|
||||
.multiple(true)
|
||||
.required(true),
|
||||
),
|
||||
SubCommand::with_name("sort")
|
||||
.about("Sorts messages by the given criteria and matching the given IMAP query")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table_arg::max_width())
|
||||
.arg(
|
||||
Arg::with_name("criterion")
|
||||
.long("criterion")
|
||||
.short("c")
|
||||
.help("Defines the message sorting preferences")
|
||||
.value_name("CRITERION:ORDER")
|
||||
.takes_value(true)
|
||||
.multiple(true)
|
||||
.required(true)
|
||||
.possible_values(&[
|
||||
"arrival", "arrival:asc", "arrival:desc",
|
||||
"cc", "cc:asc", "cc:desc",
|
||||
"date", "date:asc", "date:desc",
|
||||
"from", "from:asc", "from:desc",
|
||||
"size", "size:asc", "size:desc",
|
||||
"subject", "subject:asc", "subject:desc",
|
||||
"to", "to:asc", "to:desc",
|
||||
]),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("query")
|
||||
.help("IMAP query")
|
||||
.long_help("The IMAP query format follows the [RFC3501](https://tools.ietf.org/html/rfc3501#section-6.4.4). The query is case-insensitive.")
|
||||
.value_name("QUERY")
|
||||
.default_value("ALL")
|
||||
.raw(true),
|
||||
),
|
||||
SubCommand::with_name("write")
|
||||
.about("Writes a new message")
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_arg()),
|
||||
SubCommand::with_name("send")
|
||||
.about("Sends a raw message")
|
||||
.arg(Arg::with_name("message").raw(true)),
|
||||
SubCommand::with_name("save")
|
||||
.about("Saves a raw message")
|
||||
.arg(Arg::with_name("message").raw(true)),
|
||||
SubCommand::with_name("read")
|
||||
.about("Reads text bodies of a message")
|
||||
.arg(seq_arg())
|
||||
.arg(
|
||||
Arg::with_name("mime-type")
|
||||
.help("MIME type to use")
|
||||
.short("t")
|
||||
.long("mime-type")
|
||||
.value_name("MIME")
|
||||
.possible_values(&["plain", "html"])
|
||||
.default_value("plain"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("raw")
|
||||
.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(attachments_arg())
|
||||
.arg(encrypt_arg()),
|
||||
SubCommand::with_name("forward")
|
||||
.aliases(&["fwd", "f"])
|
||||
.about("Forwards a message")
|
||||
.arg(seq_arg())
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_arg()),
|
||||
SubCommand::with_name("copy")
|
||||
.aliases(&["cp", "c"])
|
||||
.about("Copies a message to the targetted mailbox")
|
||||
.arg(seq_arg())
|
||||
.arg(mbox_args::target_arg()),
|
||||
SubCommand::with_name("move")
|
||||
.aliases(&["mv"])
|
||||
.about("Moves a message to the targetted mailbox")
|
||||
.arg(seq_arg())
|
||||
.arg(mbox_args::target_arg()),
|
||||
SubCommand::with_name("delete")
|
||||
.aliases(&["del", "d", "remove", "rm"])
|
||||
.about("Deletes a message")
|
||||
.arg(seq_arg()),
|
||||
],
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,379 @@
|
||||
//! Module related to message handling.
|
||||
//!
|
||||
//! This module gathers all message commands.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use atty::Stream;
|
||||
use log::{debug, info, trace};
|
||||
use mailparse::addrparse;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fs,
|
||||
io::{self, BufRead},
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
backends::Backend,
|
||||
config::{AccountConfig, DEFAULT_SENT_FOLDER},
|
||||
msg::{Msg, Part, Parts, TextPlainPart},
|
||||
output::{PrintTableOpts, PrinterService},
|
||||
smtp::SmtpService,
|
||||
};
|
||||
|
||||
/// Downloads all message attachments to the user account downloads directory.
|
||||
pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let attachments = backend.get_msg(mbox, seq)?.attachments();
|
||||
let attachments_len = attachments.len();
|
||||
|
||||
if attachments_len == 0 {
|
||||
return printer.print_struct(format!("No attachment found for message {:?}", seq));
|
||||
}
|
||||
|
||||
printer.print_str(format!(
|
||||
"Found {:?} attachment{} for message {:?}",
|
||||
attachments_len,
|
||||
if attachments_len > 1 { "s" } else { "" },
|
||||
seq
|
||||
))?;
|
||||
|
||||
for attachment in attachments {
|
||||
let file_path = config.get_download_file_path(&attachment.filename)?;
|
||||
printer.print_str(format!("Downloading {:?}…", file_path))?;
|
||||
fs::write(&file_path, &attachment.content)
|
||||
.context(format!("cannot download attachment {:?}", file_path))?;
|
||||
}
|
||||
|
||||
printer.print_struct(format!(
|
||||
"Attachment{} successfully downloaded to {:?}",
|
||||
if attachments_len > 1 { "s" } else { "" },
|
||||
config.downloads_dir
|
||||
))
|
||||
}
|
||||
|
||||
/// Copy a message from a mailbox to another.
|
||||
pub fn copy<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox_src: &str,
|
||||
mbox_dst: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&mut B>,
|
||||
) -> Result<()> {
|
||||
backend.copy_msg(mbox_src, mbox_dst, seq)?;
|
||||
printer.print_struct(format!(
|
||||
r#"Message {} successfully copied to folder "{}""#,
|
||||
seq, mbox_dst
|
||||
))
|
||||
}
|
||||
|
||||
/// Delete messages matching the given sequence range.
|
||||
pub fn delete<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.del_msg(mbox, seq)?;
|
||||
printer.print_struct(format!(r#"Message(s) {} successfully deleted"#, seq))
|
||||
}
|
||||
|
||||
/// Forward the given message UID from the selected mailbox.
|
||||
pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
seq: &str,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
) -> Result<()> {
|
||||
backend
|
||||
.get_msg(mbox, seq)?
|
||||
.into_forward(config)?
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt)
|
||||
.edit_with_editor(config, printer, backend, smtp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List paginated messages from the selected mailbox.
|
||||
pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
imap: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.default_page_size);
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = imap.get_envelopes(mbox, page_size, page)?;
|
||||
trace!("envelopes: {:?}", msgs);
|
||||
printer.print_table(
|
||||
msgs,
|
||||
PrintTableOpts {
|
||||
format: &config.format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Parses and edits a message from a [mailto] URL string.
|
||||
///
|
||||
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
|
||||
pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
url: &Url,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
) -> Result<()> {
|
||||
info!("entering mailto command handler");
|
||||
|
||||
let to = addrparse(url.path())?;
|
||||
let mut cc = Vec::new();
|
||||
let mut bcc = Vec::new();
|
||||
let mut subject = Cow::default();
|
||||
let mut body = Cow::default();
|
||||
|
||||
for (key, val) in url.query_pairs() {
|
||||
match key.as_bytes() {
|
||||
b"cc" => {
|
||||
cc.push(val.to_string());
|
||||
}
|
||||
b"bcc" => {
|
||||
bcc.push(val.to_string());
|
||||
}
|
||||
b"subject" => {
|
||||
subject = val;
|
||||
}
|
||||
b"body" => {
|
||||
body = val;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let msg = Msg {
|
||||
from: Some(vec![config.address()?].into()),
|
||||
to: if to.is_empty() { None } else { Some(to) },
|
||||
cc: if cc.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(addrparse(&cc.join(","))?)
|
||||
},
|
||||
bcc: if bcc.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(addrparse(&bcc.join(","))?)
|
||||
},
|
||||
subject: subject.into(),
|
||||
parts: Parts(vec![Part::TextPlain(TextPlainPart {
|
||||
content: body.into(),
|
||||
})]),
|
||||
..Msg::default()
|
||||
};
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
msg.edit_with_editor(config, printer, backend, smtp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Move a message from a mailbox to another.
|
||||
pub fn move_<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox_src: &str,
|
||||
mbox_dst: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
backend.move_msg(mbox_src, mbox_dst, seq)?;
|
||||
printer.print_struct(format!(
|
||||
r#"Message {} successfully moved to folder "{}""#,
|
||||
seq, mbox_dst
|
||||
))
|
||||
}
|
||||
|
||||
/// Read a message by its sequence number.
|
||||
pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
text_mime: &str,
|
||||
raw: bool,
|
||||
headers: Vec<&str>,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let msg = backend.get_msg(mbox, seq)?;
|
||||
|
||||
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.to_readable_string(text_mime, headers, config)?
|
||||
})
|
||||
}
|
||||
|
||||
/// Reply to the given message UID.
|
||||
pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
seq: &str,
|
||||
all: bool,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
) -> Result<()> {
|
||||
backend
|
||||
.get_msg(mbox, seq)?
|
||||
.into_reply(all, config)?
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt)
|
||||
.edit_with_editor(config, printer, backend, smtp)?
|
||||
.add_flags(mbox, seq, "replied")
|
||||
}
|
||||
|
||||
/// Saves a raw message to the targetted mailbox.
|
||||
pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
mbox: &str,
|
||||
raw_msg: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&mut B>,
|
||||
) -> Result<()> {
|
||||
info!("entering save message handler");
|
||||
|
||||
debug!("mailbox: {}", mbox);
|
||||
|
||||
let is_tty = atty::is(Stream::Stdin);
|
||||
debug!("is tty: {}", is_tty);
|
||||
let is_json = printer.is_json();
|
||||
debug!("is json: {}", is_json);
|
||||
|
||||
let raw_msg = if is_tty || is_json {
|
||||
raw_msg.replace("\r", "").replace("\n", "\r\n")
|
||||
} else {
|
||||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
backend.add_msg(mbox, raw_msg.as_bytes(), "seen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Paginate messages from the selected mailbox matching the specified query.
|
||||
pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
query: String,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.default_page_size);
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = backend.search_envelopes(mbox, &query, "", page_size, page)?;
|
||||
trace!("messages: {:#?}", msgs);
|
||||
printer.print_table(
|
||||
msgs,
|
||||
PrintTableOpts {
|
||||
format: &config.format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Paginates messages from the selected mailbox matching the specified query, sorted by the given criteria.
|
||||
pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
sort: String,
|
||||
query: String,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.default_page_size);
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = backend.search_envelopes(mbox, &query, &sort, page_size, page)?;
|
||||
trace!("envelopes: {:#?}", msgs);
|
||||
printer.print_table(
|
||||
msgs,
|
||||
PrintTableOpts {
|
||||
format: &config.format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Send a raw message.
|
||||
pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
raw_msg: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&mut B>,
|
||||
smtp: &mut S,
|
||||
) -> Result<()> {
|
||||
info!("entering send message handler");
|
||||
|
||||
let is_tty = atty::is(Stream::Stdin);
|
||||
debug!("is tty: {}", is_tty);
|
||||
let is_json = printer.is_json();
|
||||
debug!("is json: {}", is_json);
|
||||
|
||||
let sent_folder = config
|
||||
.mailboxes
|
||||
.get("sent")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(DEFAULT_SENT_FOLDER);
|
||||
debug!("sent folder: {:?}", sent_folder);
|
||||
|
||||
let raw_msg = if is_tty || is_json {
|
||||
raw_msg.replace("\r", "").replace("\n", "\r\n")
|
||||
} else {
|
||||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
trace!("raw message: {:?}", raw_msg);
|
||||
let msg = Msg::from_tpl(&raw_msg)?;
|
||||
smtp.send(&config, &msg)?;
|
||||
backend.add_msg(&sent_folder, raw_msg.as_bytes(), "seen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compose a new message.
|
||||
pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
) -> Result<()> {
|
||||
Msg::default()
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt)
|
||||
.edit_with_editor(config, printer, backend, smtp)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use anyhow::{Context, Result};
|
||||
use log::{debug, trace};
|
||||
use std::{env, fs, path::PathBuf};
|
||||
|
||||
pub fn local_draft_path() -> PathBuf {
|
||||
let path = env::temp_dir().join("himalaya-draft.eml");
|
||||
trace!("local draft path: {:?}", path);
|
||||
path
|
||||
}
|
||||
|
||||
pub fn remove_local_draft() -> Result<()> {
|
||||
let path = local_draft_path();
|
||||
debug!("remove draft path at {:?}", path);
|
||||
fs::remove_file(&path).context(format!("cannot remove local draft at {:?}", path))
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use mailparse::MailHeaderMap;
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
env, fs,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::config::AccountConfig;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct TextPlainPart {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct TextHtmlPart {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct BinaryPart {
|
||||
pub filename: String,
|
||||
pub mime: String,
|
||||
pub content: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Part {
|
||||
TextPlain(TextPlainPart),
|
||||
TextHtml(TextHtmlPart),
|
||||
Binary(BinaryPart),
|
||||
}
|
||||
|
||||
impl Part {
|
||||
pub fn new_text_plain(content: String) -> Self {
|
||||
Self::TextPlain(TextPlainPart { content })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Parts(pub Vec<Part>);
|
||||
|
||||
impl Parts {
|
||||
pub fn replace_text_plain_parts_with(&mut self, part: TextPlainPart) {
|
||||
self.retain(|part| !matches!(part, Part::TextPlain(_)));
|
||||
self.push(Part::TextPlain(part));
|
||||
}
|
||||
|
||||
pub fn from_parsed_mail<'a>(
|
||||
account: &'a AccountConfig,
|
||||
part: &'a mailparse::ParsedMail<'a>,
|
||||
) -> Result<Self> {
|
||||
let mut parts = vec![];
|
||||
build_parts_map_rec(account, part, &mut parts)?;
|
||||
Ok(Self(parts))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Parts {
|
||||
type Target = Vec<Part>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Parts {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn build_parts_map_rec(
|
||||
account: &AccountConfig,
|
||||
parsed_mail: &mailparse::ParsedMail,
|
||||
parts: &mut Vec<Part>,
|
||||
) -> Result<()> {
|
||||
if parsed_mail.subparts.is_empty() {
|
||||
let cdisp = parsed_mail.get_content_disposition();
|
||||
match cdisp.disposition {
|
||||
mailparse::DispositionType::Attachment => {
|
||||
let filename = cdisp
|
||||
.params
|
||||
.get("filename")
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| String::from("noname"));
|
||||
let content = parsed_mail.get_body_raw().unwrap_or_default();
|
||||
let mime = tree_magic::from_u8(&content);
|
||||
parts.push(Part::Binary(BinaryPart {
|
||||
filename,
|
||||
mime,
|
||||
content,
|
||||
}));
|
||||
}
|
||||
// TODO: manage other use cases
|
||||
_ => {
|
||||
if let Some(ctype) = parsed_mail.get_headers().get_first_value("content-type") {
|
||||
let content = parsed_mail.get_body().unwrap_or_default();
|
||||
if ctype.starts_with("text/plain") {
|
||||
parts.push(Part::TextPlain(TextPlainPart { content }))
|
||||
} else if ctype.starts_with("text/html") {
|
||||
parts.push(Part::TextHtml(TextHtmlPart { content }))
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
} else {
|
||||
let ctype = parsed_mail
|
||||
.get_headers()
|
||||
.get_first_value("content-type")
|
||||
.ok_or_else(|| anyhow!("cannot get content type of multipart"))?;
|
||||
if ctype.starts_with("multipart/encrypted") {
|
||||
let decrypted_part = parsed_mail
|
||||
.subparts
|
||||
.get(1)
|
||||
.ok_or_else(|| anyhow!("cannot find encrypted part of multipart"))
|
||||
.and_then(|part| decrypt_part(account, part))
|
||||
.context("cannot decrypt part of multipart")?;
|
||||
let parsed_mail = mailparse::parse_mail(decrypted_part.as_bytes())
|
||||
.context("cannot parse decrypted part of multipart")?;
|
||||
build_parts_map_rec(account, &parsed_mail, parts)?;
|
||||
} else {
|
||||
for part in parsed_mail.subparts.iter() {
|
||||
build_parts_map_rec(account, part, parts)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decrypt_part(account: &AccountConfig, msg: &mailparse::ParsedMail) -> Result<String> {
|
||||
let msg_path = env::temp_dir().join(Uuid::new_v4().to_string());
|
||||
let msg_body = msg
|
||||
.get_body()
|
||||
.context("cannot get body from encrypted part")?;
|
||||
fs::write(msg_path.clone(), &msg_body)
|
||||
.context(format!("cannot write encrypted part to temporary file"))?;
|
||||
account
|
||||
.pgp_decrypt_file(msg_path.clone())?
|
||||
.ok_or_else(|| anyhow!("cannot find pgp decrypt command in config"))
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
//! Module related to message template CLI.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher related to message template.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
use log::{debug, info, trace};
|
||||
|
||||
use crate::msg::msg_args;
|
||||
|
||||
type Seq<'a> = &'a str;
|
||||
type ReplyAll = bool;
|
||||
type AttachmentPaths<'a> = Vec<&'a str>;
|
||||
type Tpl<'a> = &'a str;
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct TplOverride<'a> {
|
||||
pub subject: Option<&'a str>,
|
||||
pub from: Option<Vec<&'a str>>,
|
||||
pub to: Option<Vec<&'a str>>,
|
||||
pub cc: Option<Vec<&'a str>>,
|
||||
pub bcc: Option<Vec<&'a str>>,
|
||||
pub headers: Option<Vec<&'a str>>,
|
||||
pub body: Option<&'a str>,
|
||||
pub sig: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ArgMatches<'a>> for TplOverride<'a> {
|
||||
fn from(matches: &'a ArgMatches<'a>) -> Self {
|
||||
Self {
|
||||
subject: matches.value_of("subject"),
|
||||
from: matches.values_of("from").map(|v| v.collect()),
|
||||
to: matches.values_of("to").map(|v| v.collect()),
|
||||
cc: matches.values_of("cc").map(|v| v.collect()),
|
||||
bcc: matches.values_of("bcc").map(|v| v.collect()),
|
||||
headers: matches.values_of("headers").map(|v| v.collect()),
|
||||
body: matches.value_of("body"),
|
||||
sig: matches.value_of("signature"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Message template commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
New(TplOverride<'a>),
|
||||
Reply(Seq<'a>, ReplyAll, TplOverride<'a>),
|
||||
Forward(Seq<'a>, TplOverride<'a>),
|
||||
Save(AttachmentPaths<'a>, Tpl<'a>),
|
||||
Send(AttachmentPaths<'a>, Tpl<'a>),
|
||||
}
|
||||
|
||||
/// Message template command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
info!("entering message template command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("new") {
|
||||
info!("new subcommand matched");
|
||||
let tpl = TplOverride::from(m);
|
||||
trace!("template override: {:?}", tpl);
|
||||
return Ok(Some(Cmd::New(tpl)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("reply") {
|
||||
info!("reply subcommand matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("sequence: {}", seq);
|
||||
let all = m.is_present("reply-all");
|
||||
debug!("reply all: {}", all);
|
||||
let tpl = TplOverride::from(m);
|
||||
trace!("template override: {:?}", tpl);
|
||||
return Ok(Some(Cmd::Reply(seq, all, tpl)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("forward") {
|
||||
info!("forward subcommand matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("sequence: {}", seq);
|
||||
let tpl = TplOverride::from(m);
|
||||
trace!("template args: {:?}", tpl);
|
||||
return Ok(Some(Cmd::Forward(seq, tpl)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("save") {
|
||||
info!("save subcommand matched");
|
||||
let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
trace!("attachments paths: {:?}", attachment_paths);
|
||||
let tpl = m.value_of("template").unwrap_or_default();
|
||||
trace!("template: {}", tpl);
|
||||
return Ok(Some(Cmd::Save(attachment_paths, tpl)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("send") {
|
||||
info!("send subcommand matched");
|
||||
let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
|
||||
trace!("attachments paths: {:?}", attachment_paths);
|
||||
let tpl = m.value_of("template").unwrap_or_default();
|
||||
trace!("template: {}", tpl);
|
||||
return Ok(Some(Cmd::Send(attachment_paths, tpl)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Message template args.
|
||||
pub fn tpl_args<'a>() -> Vec<Arg<'a, 'a>> {
|
||||
vec![
|
||||
Arg::with_name("subject")
|
||||
.help("Overrides the Subject header")
|
||||
.short("s")
|
||||
.long("subject")
|
||||
.value_name("STRING"),
|
||||
Arg::with_name("from")
|
||||
.help("Overrides the From header")
|
||||
.short("f")
|
||||
.long("from")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name("to")
|
||||
.help("Overrides the To header")
|
||||
.short("t")
|
||||
.long("to")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name("cc")
|
||||
.help("Overrides the Cc header")
|
||||
.short("c")
|
||||
.long("cc")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name("bcc")
|
||||
.help("Overrides the Bcc header")
|
||||
.short("b")
|
||||
.long("bcc")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name("header")
|
||||
.help("Overrides a specific header")
|
||||
.short("h")
|
||||
.long("header")
|
||||
.value_name("KEY: VAL")
|
||||
.multiple(true),
|
||||
Arg::with_name("body")
|
||||
.help("Overrides the body")
|
||||
.short("B")
|
||||
.long("body")
|
||||
.value_name("STRING"),
|
||||
Arg::with_name("signature")
|
||||
.help("Overrides the signature")
|
||||
.short("S")
|
||||
.long("signature")
|
||||
.value_name("STRING"),
|
||||
]
|
||||
}
|
||||
|
||||
/// Message template subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name("template")
|
||||
.aliases(&["tpl"])
|
||||
.about("Generates a message template")
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand(
|
||||
SubCommand::with_name("new")
|
||||
.aliases(&["n"])
|
||||
.about("Generates a new message template")
|
||||
.args(&tpl_args()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("reply")
|
||||
.aliases(&["rep", "re", "r"])
|
||||
.about("Generates a reply message template")
|
||||
.arg(msg_args::seq_arg())
|
||||
.arg(msg_args::reply_all_arg())
|
||||
.args(&tpl_args()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("forward")
|
||||
.aliases(&["fwd", "fw", "f"])
|
||||
.about("Generates a forward message template")
|
||||
.arg(msg_args::seq_arg())
|
||||
.args(&tpl_args()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("save")
|
||||
.about("Saves a message based on the given template")
|
||||
.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::attachments_arg())
|
||||
.arg(Arg::with_name("template").raw(true)),
|
||||
)]
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//! Module related to message template handling.
|
||||
//!
|
||||
//! This module gathers all message template commands.
|
||||
|
||||
use anyhow::Result;
|
||||
use atty::Stream;
|
||||
use std::io::{self, BufRead};
|
||||
|
||||
use crate::{
|
||||
backends::Backend,
|
||||
config::AccountConfig,
|
||||
msg::{Msg, TplOverride},
|
||||
output::PrinterService,
|
||||
smtp::SmtpService,
|
||||
};
|
||||
|
||||
/// Generate a new message template.
|
||||
pub fn new<'a, P: PrinterService>(
|
||||
opts: TplOverride<'a>,
|
||||
account: &'a AccountConfig,
|
||||
printer: &'a mut P,
|
||||
) -> Result<()> {
|
||||
let tpl = Msg::default().to_tpl(opts, account)?;
|
||||
printer.print_struct(tpl)
|
||||
}
|
||||
|
||||
/// Generate a reply message template.
|
||||
pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
all: bool,
|
||||
opts: TplOverride<'a>,
|
||||
mbox: &str,
|
||||
config: &'a AccountConfig,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let tpl = backend
|
||||
.get_msg(mbox, seq)?
|
||||
.into_reply(all, config)?
|
||||
.to_tpl(opts, config)?;
|
||||
printer.print_struct(tpl)
|
||||
}
|
||||
|
||||
/// Generate a forward message template.
|
||||
pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
opts: TplOverride<'a>,
|
||||
mbox: &str,
|
||||
config: &'a AccountConfig,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
) -> Result<()> {
|
||||
let tpl = backend
|
||||
.get_msg(mbox, seq)?
|
||||
.into_forward(config)?
|
||||
.to_tpl(opts, config)?;
|
||||
printer.print_struct(tpl)
|
||||
}
|
||||
|
||||
/// Saves a message based on a template.
|
||||
pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
attachments_paths: Vec<&str>,
|
||||
tpl: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&mut B>,
|
||||
) -> Result<()> {
|
||||
let tpl = if atty::is(Stream::Stdin) || printer.is_json() {
|
||||
tpl.replace("\r", "")
|
||||
} else {
|
||||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
};
|
||||
let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
|
||||
let raw_msg = msg.into_sendable_msg(config)?.formatted();
|
||||
backend.add_msg(mbox, &raw_msg, "seen")?;
|
||||
printer.print_struct("Template successfully saved")
|
||||
}
|
||||
|
||||
/// Sends a message based on a template.
|
||||
pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
mbox: &str,
|
||||
account: &AccountConfig,
|
||||
attachments_paths: Vec<&str>,
|
||||
tpl: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&mut B>,
|
||||
smtp: &mut S,
|
||||
) -> Result<()> {
|
||||
let tpl = if atty::is(Stream::Stdin) || printer.is_json() {
|
||||
tpl.replace("\r", "")
|
||||
} else {
|
||||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
};
|
||||
let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
|
||||
let sent_msg = smtp.send(account, &msg)?;
|
||||
backend.add_msg(mbox, &sent_msg, "seen")?;
|
||||
printer.print_struct("Template successfully sent")
|
||||
}
|
||||
Reference in New Issue
Block a user