refactor attachment with clap derive api

This commit is contained in:
Clément DOUIN 2023-12-07 21:59:12 +01:00
parent b8ef771614
commit b28f12c367
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
7 changed files with 117 additions and 460 deletions

View file

@ -10,7 +10,7 @@ use crate::{
flag::command::FlagSubcommand,
folder::command::FolderSubcommand,
manual::command::ManualGenerateCommand,
message::command::MessageSubcommand,
message::{attachment::command::AttachmentSubcommand, command::MessageSubcommand},
output::{ColorFmt, OutputFmt},
printer::Printer,
};
@ -105,6 +105,10 @@ pub enum HimalayaCommand {
#[command(alias = "messages", alias = "msgs", alias = "msg")]
Message(MessageSubcommand),
/// Manage attachments
#[command(subcommand)]
Attachment(AttachmentSubcommand),
/// Generate manual pages to a directory
#[command(arg_required_else_help = true)]
#[command(alias = "manuals", alias = "mans")]
@ -124,6 +128,7 @@ impl HimalayaCommand {
Self::Envelope(cmd) => cmd.execute(printer, config).await,
Self::Flag(cmd) => cmd.execute(printer, config).await,
Self::Message(cmd) => cmd.execute(printer, config).await,
Self::Attachment(cmd) => cmd.execute(printer, config).await,
Self::Manual(cmd) => cmd.execute(printer).await,
Self::Completion(cmd) => cmd.execute(printer).await,
}

View file

@ -1,356 +0,0 @@
//! Email CLI module.
//!
//! This module contains the command matcher, the subcommands and the
//! arguments related to the email domain.
use anyhow::Result;
use clap::{Arg, ArgAction, ArgMatches, Command};
use crate::template;
const ARG_CRITERIA: &str = "criterion";
const ARG_HEADERS: &str = "headers";
const ARG_ID: &str = "id";
const ARG_IDS: &str = "ids";
const ARG_MIME_TYPE: &str = "mime-type";
const ARG_QUERY: &str = "query";
const ARG_RAW: &str = "raw";
const ARG_REPLY_ALL: &str = "reply-all";
const CMD_ATTACHMENTS: &str = "attachments";
const CMD_COPY: &str = "copy";
const CMD_DELETE: &str = "delete";
const CMD_FORWARD: &str = "forward";
const CMD_MESSAGE: &str = "message";
const CMD_MOVE: &str = "move";
const CMD_READ: &str = "read";
const CMD_REPLY: &str = "reply";
const CMD_SAVE: &str = "save";
const CMD_SEND: &str = "send";
const CMD_WRITE: &str = "write";
pub type All = bool;
pub type Criteria = String;
pub type Folder<'a> = &'a str;
pub type Headers<'a> = Vec<&'a str>;
pub type Id<'a> = &'a str;
pub type Ids<'a> = Vec<&'a str>;
pub type Query = String;
pub type Raw = bool;
pub type RawEmail = String;
pub type TextMime<'a> = &'a str;
/// Represents the email commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> {
Attachments(Ids<'a>),
Copy(Ids<'a>, Folder<'a>),
Delete(Ids<'a>),
Forward(
Id<'a>,
template::args::Headers<'a>,
template::args::Body<'a>,
),
Move(Ids<'a>, Folder<'a>),
Read(Ids<'a>, TextMime<'a>, Raw, Headers<'a>),
Reply(
Id<'a>,
All,
template::args::Headers<'a>,
template::args::Body<'a>,
),
Save(RawEmail),
Send(RawEmail),
Write(template::args::Headers<'a>, template::args::Body<'a>),
}
/// Email command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_MESSAGE) {
if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) {
let ids = parse_ids_arg(m);
Some(Cmd::Attachments(ids))
} else if let Some(m) = m.subcommand_matches(CMD_COPY) {
let ids = parse_ids_arg(m);
let folder = "INBOX";
Some(Cmd::Copy(ids, folder))
} else if let Some(m) = m.subcommand_matches(CMD_DELETE) {
let ids = parse_ids_arg(m);
Some(Cmd::Delete(ids))
} else if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
let id = parse_id_arg(m);
let headers = template::args::parse_headers_arg(m);
let body = template::args::parse_body_arg(m);
Some(Cmd::Forward(id, headers, body))
} else if let Some(m) = m.subcommand_matches(CMD_MOVE) {
let ids = parse_ids_arg(m);
let folder = "INBOX";
Some(Cmd::Move(ids, folder))
} else if let Some(m) = m.subcommand_matches(CMD_READ) {
let ids = parse_ids_arg(m);
let mime = parse_mime_type_arg(m);
let raw = parse_raw_flag(m);
let headers = parse_headers_arg(m);
Some(Cmd::Read(ids, mime, raw, headers))
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
let id = parse_id_arg(m);
let all = parse_reply_all_flag(m);
let headers = template::args::parse_headers_arg(m);
let body = template::args::parse_body_arg(m);
Some(Cmd::Reply(id, all, headers, body))
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
let email = parse_raw_arg(m);
Some(Cmd::Save(email))
} else if let Some(m) = m.subcommand_matches(CMD_SEND) {
let email = parse_raw_arg(m);
Some(Cmd::Send(email))
} else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
let headers = template::args::parse_headers_arg(m);
let body = template::args::parse_body_arg(m);
Some(Cmd::Write(headers, body))
} else {
None
}
} else {
None
};
Ok(cmd)
}
/// Represents the email subcommands.
pub fn subcmd() -> Command {
Command::new(CMD_MESSAGE)
.about("Subcommand to manage messages")
.long_about("Subcommand to manage messages like read, write, reply or send")
.aliases(["msg"])
.subcommand_required(true)
.arg_required_else_help(true)
.subcommands([
Command::new(CMD_ATTACHMENTS)
.about("Downloads all emails attachments")
.arg(ids_arg()),
Command::new(CMD_WRITE)
.about("Write a new email")
.aliases(["new", "n"])
.args(template::args::args()),
Command::new(CMD_SEND)
.about("Send a raw email")
.arg(raw_arg()),
Command::new(CMD_SAVE)
.about("Save a raw email")
.arg(raw_arg()),
Command::new(CMD_READ)
.about("Read text bodies of emails")
.arg(mime_type_arg())
.arg(raw_flag())
.arg(headers_arg())
.arg(ids_arg()),
Command::new(CMD_REPLY)
.about("Answer to an email")
.arg(reply_all_flag())
.args(template::args::args())
.arg(id_arg()),
Command::new(CMD_FORWARD)
.aliases(["fwd", "f"])
.about("Forward an email")
.args(template::args::args())
.arg(id_arg()),
Command::new(CMD_COPY)
.alias("cp")
.about("Copy emails to the given folder")
// .arg(folder::args::target_arg())
.arg(ids_arg()),
Command::new(CMD_MOVE)
.alias("mv")
.about("Move emails to the given folder")
// .arg(folder::args::target_arg())
.arg(ids_arg()),
Command::new(CMD_DELETE)
.aliases(["remove", "rm"])
.about("Delete emails")
.arg(ids_arg()),
])
}
/// Represents the email id argument.
pub fn id_arg() -> Arg {
Arg::new(ARG_ID)
.help("Specifies the target email")
.value_name("ID")
.required(true)
}
/// Represents the email id argument parser.
pub fn parse_id_arg(matches: &ArgMatches) -> &str {
matches.get_one::<String>(ARG_ID).unwrap()
}
/// Represents the email ids argument.
pub fn ids_arg() -> Arg {
Arg::new(ARG_IDS)
.help("Email ids")
.value_name("IDS")
.num_args(1..)
.required(true)
}
/// Represents the email ids argument parser.
pub fn parse_ids_arg(matches: &ArgMatches) -> Vec<&str> {
matches
.get_many::<String>(ARG_IDS)
.unwrap()
.map(String::as_str)
.collect()
}
/// Represents the email sort criteria argument.
pub fn criteria_arg<'a>() -> Arg {
Arg::new(ARG_CRITERIA)
.help("Email sorting preferences")
.long("criterion")
.short('c')
.value_name("CRITERION:ORDER")
.action(ArgAction::Append)
.value_parser([
"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",
])
}
/// Represents the email sort criteria argument parser.
pub fn parse_criteria_arg(matches: &ArgMatches) -> String {
matches
.get_many::<String>(ARG_CRITERIA)
.unwrap_or_default()
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.join(" ")
}
/// Represents the email reply all argument.
pub fn reply_all_flag() -> Arg {
Arg::new(ARG_REPLY_ALL)
.help("Include all recipients")
.long("all")
.short('A')
.action(ArgAction::SetTrue)
}
/// Represents the email reply all argument parser.
pub fn parse_reply_all_flag(matches: &ArgMatches) -> bool {
matches.get_flag(ARG_REPLY_ALL)
}
/// Represents the email headers argument.
pub fn headers_arg() -> Arg {
Arg::new(ARG_HEADERS)
.help("Shows additional headers with the email")
.long("header")
.short('H')
.value_name("STRING")
.action(ArgAction::Append)
}
/// Represents the email headers argument parser.
pub fn parse_headers_arg(m: &ArgMatches) -> Vec<&str> {
m.get_many::<String>(ARG_HEADERS)
.unwrap_or_default()
.map(String::as_str)
.collect::<Vec<_>>()
}
/// Represents the raw flag.
pub fn raw_flag() -> Arg {
Arg::new(ARG_RAW)
.help("Returns raw version of email")
.long("raw")
.short('r')
.action(ArgAction::SetTrue)
}
/// Represents the raw flag parser.
pub fn parse_raw_flag(m: &ArgMatches) -> bool {
m.get_flag(ARG_RAW)
}
/// Represents the email raw argument.
pub fn raw_arg() -> Arg {
Arg::new(ARG_RAW).raw(true)
}
/// Represents the email raw argument parser.
pub fn parse_raw_arg(m: &ArgMatches) -> String {
m.get_one::<String>(ARG_RAW).cloned().unwrap_or_default()
}
/// Represents the email MIME type argument.
pub fn mime_type_arg() -> Arg {
Arg::new(ARG_MIME_TYPE)
.help("MIME type to use")
.short('t')
.long("mime-type")
.value_name("MIME")
.value_parser(["plain", "html"])
.default_value("plain")
}
/// Represents the email MIME type argument parser.
pub fn parse_mime_type_arg(matches: &ArgMatches) -> &str {
matches.get_one::<String>(ARG_MIME_TYPE).unwrap()
}
/// Represents the email query argument.
pub fn query_arg() -> Arg {
Arg::new(ARG_QUERY)
.long_help("The query system depends on the backend, see the wiki for more details")
.value_name("QUERY")
.num_args(1..)
.required(true)
}
/// Represents the email query argument parser.
pub fn parse_query_arg(matches: &ArgMatches) -> String {
matches
.get_many::<String>(ARG_QUERY)
.unwrap_or_default()
.fold((false, vec![]), |(escape, mut cmds), cmd| {
match (cmd.as_str(), 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(" ")
}

View file

@ -0,0 +1,85 @@
use anyhow::{Context, Result};
use clap::Parser;
use log::info;
use std::fs;
use uuid::Uuid;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag,
config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameArg,
printer::Printer,
};
/// Download attachments of a message
#[derive(Debug, Parser)]
pub struct AttachmentDownloadCommand {
#[command(flatten)]
pub folder: FolderNameArg,
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[command(flatten)]
pub cache: DisableCacheFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl AttachmentDownloadCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing attachment download command");
let folder = &self.folder.name;
let account = self.account.name.as_ref().map(String::as_str);
let cache = self.cache.disable;
let (toml_account_config, account_config) =
config.clone().into_account_configs(account, cache)?;
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
let ids = &self.envelopes.ids;
let emails = backend.get_messages(&folder, ids).await?;
let mut emails_count = 0;
let mut attachments_count = 0;
let mut ids = ids.iter();
for email in emails.to_vec() {
let id = ids.next().unwrap();
let attachments = email.attachments()?;
if attachments.is_empty() {
printer.print_log(format!("No attachment found for message {id}!"))?;
continue;
} else {
emails_count += 1;
}
printer.print_log(format!(
"{} attachment(s) found for message {id}!",
attachments.len()
))?;
for attachment in attachments {
let filename = attachment
.filename
.unwrap_or_else(|| Uuid::new_v4().to_string());
let filepath = account_config.download_fpath(&filename)?;
printer.print_log(format!("Downloading {:?}", filepath))?;
fs::write(&filepath, &attachment.body)
.with_context(|| format!("cannot save attachment at {filepath:?}"))?;
attachments_count += 1;
}
}
match attachments_count {
0 => printer.print("No attachment found!"),
1 => printer.print("Downloaded 1 attachment!"),
n => printer.print(format!(
"Downloaded {} attachment(s) from {} messages(s)!",
n, emails_count,
)),
}
}
}

View file

@ -0,0 +1,24 @@
pub mod download;
use anyhow::Result;
use clap::Subcommand;
use crate::{config::TomlConfig, printer::Printer};
use self::download::AttachmentDownloadCommand;
/// Subcommand dedicated to attachments
#[derive(Debug, Subcommand)]
pub enum AttachmentSubcommand {
/// Download all attachments of one or more messages
#[command(arg_required_else_help = true)]
Download(AttachmentDownloadCommand),
}
impl AttachmentSubcommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self {
Self::Download(cmd) => cmd.execute(printer, config).await,
}
}
}

View file

@ -0,0 +1 @@
pub mod command;

View file

@ -1,101 +0,0 @@
use anyhow::{anyhow, Context, Result};
use atty::Stream;
use email::{
account::config::AccountConfig, envelope::Id, flag::Flag, message::Message,
template::FilterParts,
};
use log::trace;
use mail_builder::MessageBuilder;
use std::{
fs,
io::{self, BufRead},
};
use url::Url;
use uuid::Uuid;
use crate::{backend::Backend, printer::Printer, ui::editor};
pub async fn attachments<P: Printer>(
config: &AccountConfig,
printer: &mut P,
backend: &Backend,
folder: &str,
ids: Vec<&str>,
) -> Result<()> {
let emails = backend.get_messages(&folder, &ids).await?;
let mut index = 0;
let mut emails_count = 0;
let mut attachments_count = 0;
let mut ids = ids.iter();
for email in emails.to_vec() {
let id = ids.next().unwrap();
let attachments = email.attachments()?;
index = index + 1;
if attachments.is_empty() {
printer.print_log(format!("No attachment found for email #{}", id))?;
continue;
} else {
emails_count = emails_count + 1;
}
printer.print_log(format!(
"{} attachment(s) found for email #{}…",
attachments.len(),
id
))?;
for attachment in attachments {
let filename = attachment
.filename
.unwrap_or_else(|| Uuid::new_v4().to_string());
let filepath = config.download_fpath(&filename)?;
printer.print_log(format!("Downloading {:?}", filepath))?;
fs::write(&filepath, &attachment.body).context("cannot download attachment")?;
attachments_count = attachments_count + 1;
}
}
match attachments_count {
0 => printer.print("No attachment found!"),
1 => printer.print("Downloaded 1 attachment!"),
n => printer.print(format!(
"Downloaded {} attachment(s) from {} email(s)!",
n, emails_count,
)),
}
}
/// Parses and edits a message from a [mailto] URL string.
///
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
pub async fn mailto<P: Printer>(
config: &AccountConfig,
backend: &Backend,
printer: &mut P,
url: &Url,
) -> Result<()> {
let mut builder = MessageBuilder::new().to(url.path());
for (key, val) in url.query_pairs() {
match key.to_lowercase().as_bytes() {
b"cc" => builder = builder.cc(val.to_string()),
b"bcc" => builder = builder.bcc(val.to_string()),
b"subject" => builder = builder.subject(val),
b"body" => builder = builder.text_body(val),
_ => (),
}
}
let tpl = config
.generate_tpl_interpreter()
.with_show_only_headers(config.email_writing_headers())
.build()
.from_msg_builder(builder)
.await?;
editor::edit_tpl_with_editor(config, printer, backend, tpl).await
}

View file

@ -1,6 +1,5 @@
// pub mod args;
pub mod arg;
pub mod attachment;
pub mod command;
pub mod config;
// pub mod handlers;
// pub mod template;