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:
Clément DOUIN
2022-03-16 09:57:24 +01:00
parent ac8628c08c
commit b2cffd22f1
67 changed files with 226 additions and 252 deletions
+64
View File
@@ -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)
}
+13
View File
@@ -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
}
}
+109
View File
@@ -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()),
)]
}
+55
View File
@@ -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
))
}
+472
View File
@@ -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
+379
View File
@@ -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(())
}
+15
View File
@@ -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))
}
+146
View File
@@ -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"))
}
+195
View File
@@ -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)),
)]
}
+109
View File
@@ -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")
}