From a47902af7d9ea9a9ca43bbd23c252f8eb8ba400c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Thu, 7 Dec 2023 12:19:45 +0100 Subject: [PATCH] refactor message with clap derive api (part 1) --- src/backend/mod.rs | 21 ++- src/cache/mod.rs | 6 +- src/cli.rs | 44 ++++--- src/email/envelope/arg/ids.rs | 9 ++ src/email/envelope/arg/mod.rs | 1 + src/email/envelope/flag/arg/ids_and_flags.rs | 21 ++- src/email/envelope/flag/args.rs | 110 ---------------- src/email/envelope/flag/command/add.rs | 4 +- src/email/envelope/flag/command/remove.rs | 4 +- src/email/envelope/flag/command/set.rs | 4 +- src/email/envelope/flag/handlers.rs | 37 ------ src/email/envelope/flag/mod.rs | 2 - src/email/envelope/mod.rs | 1 + src/email/message/command/copy.rs | 52 ++++++++ src/email/message/command/delete.rs | 44 +++++++ src/email/message/command/mod.rs | 58 ++++++++ src/email/message/command/move_.rs | 52 ++++++++ src/email/message/command/read.rs | 117 ++++++++++++++++ src/email/message/command/save.rs | 61 +++++++++ src/email/message/command/send.rs | 65 +++++++++ src/email/message/handlers.rs | 132 ------------------- src/email/message/mod.rs | 7 +- src/email/mod.rs | 3 +- src/folder/arg/name.rs | 16 +++ src/lib.rs | 3 +- src/main.rs | 30 ----- 26 files changed, 539 insertions(+), 365 deletions(-) create mode 100644 src/email/envelope/arg/ids.rs create mode 100644 src/email/envelope/arg/mod.rs delete mode 100644 src/email/envelope/flag/args.rs delete mode 100644 src/email/envelope/flag/handlers.rs create mode 100644 src/email/message/command/copy.rs create mode 100644 src/email/message/command/delete.rs create mode 100644 src/email/message/command/mod.rs create mode 100644 src/email/message/command/move_.rs create mode 100644 src/email/message/command/read.rs create mode 100644 src/email/message/command/save.rs create mode 100644 src/email/message/command/send.rs diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 4084e0b..b659b8f 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -723,28 +723,35 @@ impl Backend { Ok(envelopes) } - pub async fn add_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { + pub async fn add_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> { let backend_kind = self.toml_account_config.add_flags_kind(); let id_mapper = self.build_id_mapper(folder, backend_kind)?; let ids = Id::multiple(id_mapper.get_ids(ids)?); self.backend.add_flags(folder, &ids, flags).await } - pub async fn set_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { + pub async fn set_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> { let backend_kind = self.toml_account_config.set_flags_kind(); let id_mapper = self.build_id_mapper(folder, backend_kind)?; let ids = Id::multiple(id_mapper.get_ids(ids)?); self.backend.set_flags(folder, &ids, flags).await } - pub async fn remove_flags(&self, folder: &str, ids: &[&str], flags: &Flags) -> Result<()> { + pub async fn remove_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> { let backend_kind = self.toml_account_config.remove_flags_kind(); let id_mapper = self.build_id_mapper(folder, backend_kind)?; let ids = Id::multiple(id_mapper.get_ids(ids)?); self.backend.remove_flags(folder, &ids, flags).await } - pub async fn get_messages(&self, folder: &str, ids: &[&str]) -> Result { + pub async fn peek_messages(&self, folder: &str, ids: &[usize]) -> Result { + let backend_kind = self.toml_account_config.get_messages_kind(); + let id_mapper = self.build_id_mapper(folder, backend_kind)?; + let ids = Id::multiple(id_mapper.get_ids(ids)?); + self.backend.peek_messages(folder, &ids).await + } + + pub async fn get_messages(&self, folder: &str, ids: &[usize]) -> Result { let backend_kind = self.toml_account_config.get_messages_kind(); let id_mapper = self.build_id_mapper(folder, backend_kind)?; let ids = Id::multiple(id_mapper.get_ids(ids)?); @@ -755,7 +762,7 @@ impl Backend { &self, from_folder: &str, to_folder: &str, - ids: &[&str], + ids: &[usize], ) -> Result<()> { let backend_kind = self.toml_account_config.move_messages_kind(); let id_mapper = self.build_id_mapper(from_folder, backend_kind)?; @@ -769,7 +776,7 @@ impl Backend { &self, from_folder: &str, to_folder: &str, - ids: &[&str], + ids: &[usize], ) -> Result<()> { let backend_kind = self.toml_account_config.move_messages_kind(); let id_mapper = self.build_id_mapper(from_folder, backend_kind)?; @@ -779,7 +786,7 @@ impl Backend { .await } - pub async fn delete_messages(&self, folder: &str, ids: &[&str]) -> Result<()> { + pub async fn delete_messages(&self, folder: &str, ids: &[usize]) -> Result<()> { let backend_kind = self.toml_account_config.delete_messages_kind(); let id_mapper = self.build_id_mapper(folder, backend_kind)?; let ids = Id::multiple(id_mapper.get_ids(ids)?); diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 4b51a20..ed6d4ad 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -122,9 +122,9 @@ impl IdMapper { pub fn get_id(&self, alias: A) -> Result where - A: AsRef, + A: ToString, { - let alias = alias.as_ref(); + let alias = alias.to_string(); let alias = alias .parse::() .context(format!("cannot parse id mapper alias {alias}"))?; @@ -158,7 +158,7 @@ impl IdMapper { pub fn get_ids(&self, aliases: I) -> Result> where - A: AsRef, + A: ToString, I: IntoIterator, { aliases diff --git a/src/cli.rs b/src/cli.rs index 5745387..4ae7964 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,6 +10,7 @@ use crate::{ flag::command::FlagSubcommand, folder::command::FolderSubcommand, manual::command::ManualGenerateCommand, + message::command::MessageSubcommand, output::{ColorFmt, OutputFmt}, printer::Printer, }; @@ -33,7 +34,8 @@ pub struct Cli { /// applicable). If the path does not point to a valid file, the /// wizard will propose to assist you in the creation of the /// configuration file. - #[arg(long, short, value_name = "PATH", global = true, value_parser = config::path_parser)] + #[arg(long, short, global = true)] + #[arg(value_name = "PATH", value_parser = config::path_parser)] pub config: Option, /// Customize the output format @@ -47,14 +49,8 @@ pub struct Cli { /// /// - plain: output will be in a form of either a plain text or /// table, depending on the command - #[arg( - long, - short, - value_name = "FORMAT", - global = true, - value_enum, - default_value_t = Default::default(), - )] + #[arg(long, short, global = true)] + #[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())] pub output: OutputFmt, /// Control when to use colors @@ -77,41 +73,46 @@ pub struct Cli { /// - ansi: like 'always', but emits ANSI escapes (even in a Windows console) /// /// - auto: himalaya tries to be smart - #[arg( - long, - short = 'C', - value_name = "MODE", - global = true, - value_enum, - default_value_t = Default::default(), - )] + #[arg(long, short = 'C', global = true)] + #[arg(value_name = "MODE", value_enum, default_value_t = Default::default())] pub color: ColorFmt, } #[derive(Subcommand, Debug)] pub enum HimalayaCommand { /// Subcommand to manage accounts - #[command(subcommand, alias = "accounts")] + #[command(subcommand)] + #[command(alias = "accounts")] Account(AccountSubcommand), /// Subcommand to manage folders - #[command(subcommand, alias = "folders")] + #[command(subcommand)] + #[command(alias = "folders")] Folder(FolderSubcommand), /// Subcommand to manage envelopes - #[command(subcommand, alias = "envelopes")] + #[command(subcommand)] + #[command(alias = "envelopes")] Envelope(EnvelopeSubcommand), /// Subcommand to manage flags - #[command(subcommand, alias = "flags")] + #[command(subcommand)] + #[command(alias = "flags")] Flag(FlagSubcommand), + /// Subcommand to manage messages + #[command(subcommand)] + #[command(alias = "messages", alias = "msgs", alias = "msg")] + Message(MessageSubcommand), + /// Generate manual pages to a directory #[command(arg_required_else_help = true)] + #[command(alias = "manuals", alias = "mans")] Manual(ManualGenerateCommand), /// Print completion script for a shell to stdout #[command(arg_required_else_help = true)] + #[command(alias = "completions")] Completion(CompletionGenerateCommand), } @@ -122,6 +123,7 @@ impl HimalayaCommand { Self::Folder(cmd) => cmd.execute(printer, config).await, 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::Manual(cmd) => cmd.execute(printer).await, Self::Completion(cmd) => cmd.execute(printer).await, } diff --git a/src/email/envelope/arg/ids.rs b/src/email/envelope/arg/ids.rs new file mode 100644 index 0000000..8fee631 --- /dev/null +++ b/src/email/envelope/arg/ids.rs @@ -0,0 +1,9 @@ +use clap::Parser; + +/// The envelopes ids arguments parser +#[derive(Debug, Parser)] +pub struct EnvelopeIdsArgs { + /// The list of envelopes ids + #[arg(value_name = "ID", required = true)] + pub ids: Vec, +} diff --git a/src/email/envelope/arg/mod.rs b/src/email/envelope/arg/mod.rs new file mode 100644 index 0000000..ff6b00d --- /dev/null +++ b/src/email/envelope/arg/mod.rs @@ -0,0 +1 @@ +pub mod ids; diff --git a/src/email/envelope/flag/arg/ids_and_flags.rs b/src/email/envelope/flag/arg/ids_and_flags.rs index f1eb40f..fe0e146 100644 --- a/src/email/envelope/flag/arg/ids_and_flags.rs +++ b/src/email/envelope/flag/arg/ids_and_flags.rs @@ -15,31 +15,28 @@ pub struct IdsAndFlagsArgs { #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] pub enum IdOrFlag { - Id(String), + Id(usize), Flag(Flag), } impl From<&str> for IdOrFlag { fn from(value: &str) -> Self { - value - .parse::() - .map(|_| Self::Id(value.to_owned())) - .unwrap_or_else(|err| { - let flag = Flag::from(value); - debug!("cannot parse {value} as usize, parsing it as flag {flag}"); - debug!("{err:?}"); - Self::Flag(flag) - }) + value.parse::().map(Self::Id).unwrap_or_else(|err| { + let flag = Flag::from(value); + debug!("cannot parse {value} as usize, parsing it as flag {flag}"); + debug!("{err:?}"); + Self::Flag(flag) + }) } } -pub fn to_tuple<'a>(ids_and_flags: &'a [IdOrFlag]) -> (Vec<&'a str>, Flags) { +pub fn into_tuple(ids_and_flags: &[IdOrFlag]) -> (Vec, Flags) { ids_and_flags.iter().fold( (Vec::default(), Flags::default()), |(mut ids, mut flags), arg| { match arg { IdOrFlag::Id(id) => { - ids.push(id.as_str()); + ids.push(*id); } IdOrFlag::Flag(flag) => { flags.insert(flag.to_owned()); diff --git a/src/email/envelope/flag/args.rs b/src/email/envelope/flag/args.rs deleted file mode 100644 index 7eb528a..0000000 --- a/src/email/envelope/flag/args.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! Email flag CLI module. -//! -//! This module contains the command matcher, the subcommands and the -//! arguments related to the email flag domain. - -use ::email::flag::{Flag, Flags}; -use anyhow::Result; -use clap::{Arg, ArgMatches, Command}; -use log::{debug, info}; - -use crate::message; - -const ARG_FLAGS: &str = "flag"; - -const CMD_ADD: &str = "add"; -const CMD_REMOVE: &str = "remove"; -const CMD_SET: &str = "set"; - -pub(crate) const CMD_FLAG: &str = "flags"; - -/// Represents the flag commands. -#[derive(Debug, PartialEq, Eq)] -pub enum Cmd<'a> { - Add(message::args::Ids<'a>, Flags), - Remove(message::args::Ids<'a>, Flags), - Set(message::args::Ids<'a>, Flags), -} - -/// Represents the flag command matcher. -pub fn matches(m: &ArgMatches) -> Result> { - let cmd = if let Some(m) = m.subcommand_matches(CMD_FLAG) { - if let Some(m) = m.subcommand_matches(CMD_ADD) { - debug!("add flags command matched"); - let ids = message::args::parse_ids_arg(m); - let flags = parse_flags_arg(m); - Some(Cmd::Add(ids, flags)) - } else if let Some(m) = m.subcommand_matches(CMD_REMOVE) { - info!("remove flags command matched"); - let ids = message::args::parse_ids_arg(m); - let flags = parse_flags_arg(m); - Some(Cmd::Remove(ids, flags)) - } else if let Some(m) = m.subcommand_matches(CMD_SET) { - debug!("set flags command matched"); - let ids = message::args::parse_ids_arg(m); - let flags = parse_flags_arg(m); - Some(Cmd::Set(ids, flags)) - } else { - None - } - } else { - None - }; - - Ok(cmd) -} - -/// Represents the flag subcommand. -pub fn subcmd() -> Command { - Command::new(CMD_FLAG) - .about("Subcommand to manage flags") - .long_about("Subcommand to manage flags like add, set or remove") - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand( - Command::new(CMD_ADD) - .about("Adds flags to an email") - .arg(message::args::ids_arg()) - .arg(flags_arg()), - ) - .subcommand( - Command::new(CMD_REMOVE) - .aliases(["delete", "del", "d"]) - .about("Removes flags from an email") - .arg(message::args::ids_arg()) - .arg(flags_arg()), - ) - .subcommand( - Command::new(CMD_SET) - .aliases(["change", "c"]) - .about("Sets flags of an email") - .arg(message::args::ids_arg()) - .arg(flags_arg()), - ) -} - -/// Represents the flags argument. -pub fn flags_arg() -> Arg { - Arg::new(ARG_FLAGS) - .value_name("FLAGS") - .help("The flags") - .long_help( - "The list of flags. -It can be one of: seen, answered, flagged, deleted, or draft. -Other flags are considered custom.", - ) - .num_args(1..) - .required(true) - .last(true) -} - -/// Represents the flags argument parser. -pub fn parse_flags_arg(matches: &ArgMatches) -> Flags { - Flags::from_iter( - matches - .get_many::(ARG_FLAGS) - .unwrap_or_default() - .map(String::as_str) - .map(Flag::from), - ) -} diff --git a/src/email/envelope/flag/command/add.rs b/src/email/envelope/flag/command/add.rs index c09538e..63d7dd3 100644 --- a/src/email/envelope/flag/command/add.rs +++ b/src/email/envelope/flag/command/add.rs @@ -7,7 +7,7 @@ use crate::{ backend::Backend, cache::arg::disable::DisableCacheFlag, config::TomlConfig, - flag::arg::ids_and_flags::{to_tuple, IdsAndFlagsArgs}, + flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, folder::arg::name::FolderNameArg, printer::Printer, }; @@ -40,7 +40,7 @@ impl FlagAddCommand { config.clone().into_account_configs(account, cache)?; let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - let (ids, flags) = to_tuple(&self.args.ids_and_flags); + let (ids, flags) = into_tuple(&self.args.ids_and_flags); backend.add_flags(folder, &ids, &flags).await?; printer.print(format!("Flag(s) {flags} successfully added!")) diff --git a/src/email/envelope/flag/command/remove.rs b/src/email/envelope/flag/command/remove.rs index fae6c22..59583f5 100644 --- a/src/email/envelope/flag/command/remove.rs +++ b/src/email/envelope/flag/command/remove.rs @@ -7,7 +7,7 @@ use crate::{ backend::Backend, cache::arg::disable::DisableCacheFlag, config::TomlConfig, - flag::arg::ids_and_flags::{to_tuple, IdsAndFlagsArgs}, + flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, folder::arg::name::FolderNameArg, printer::Printer, }; @@ -40,7 +40,7 @@ impl FlagRemoveCommand { config.clone().into_account_configs(account, cache)?; let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - let (ids, flags) = to_tuple(&self.args.ids_and_flags); + let (ids, flags) = into_tuple(&self.args.ids_and_flags); backend.remove_flags(folder, &ids, &flags).await?; printer.print(format!("Flag(s) {flags} successfully removed!")) diff --git a/src/email/envelope/flag/command/set.rs b/src/email/envelope/flag/command/set.rs index f93edae..cb04482 100644 --- a/src/email/envelope/flag/command/set.rs +++ b/src/email/envelope/flag/command/set.rs @@ -7,7 +7,7 @@ use crate::{ backend::Backend, cache::arg::disable::DisableCacheFlag, config::TomlConfig, - flag::arg::ids_and_flags::{to_tuple, IdsAndFlagsArgs}, + flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, folder::arg::name::FolderNameArg, printer::Printer, }; @@ -40,7 +40,7 @@ impl FlagSetCommand { config.clone().into_account_configs(account, cache)?; let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; - let (ids, flags) = to_tuple(&self.args.ids_and_flags); + let (ids, flags) = into_tuple(&self.args.ids_and_flags); backend.set_flags(folder, &ids, &flags).await?; printer.print(format!("Flag(s) {flags} successfully set!")) diff --git a/src/email/envelope/flag/handlers.rs b/src/email/envelope/flag/handlers.rs deleted file mode 100644 index dc3812c..0000000 --- a/src/email/envelope/flag/handlers.rs +++ /dev/null @@ -1,37 +0,0 @@ -use anyhow::Result; -use email::flag::Flags; - -use crate::{backend::Backend, printer::Printer}; - -pub async fn add( - printer: &mut P, - backend: &Backend, - folder: &str, - ids: Vec<&str>, - flags: &Flags, -) -> Result<()> { - backend.add_flags(folder, &ids, flags).await?; - printer.print(format!("Flag(s) {flags} successfully added!")) -} - -pub async fn set( - printer: &mut P, - backend: &Backend, - folder: &str, - ids: Vec<&str>, - flags: &Flags, -) -> Result<()> { - backend.set_flags(folder, &ids, flags).await?; - printer.print(format!("Flag(s) {flags} successfully set!")) -} - -pub async fn remove( - printer: &mut P, - backend: &Backend, - folder: &str, - ids: Vec<&str>, - flags: &Flags, -) -> Result<()> { - backend.remove_flags(folder, &ids, flags).await?; - printer.print(format!("Flag(s) {flags} successfully removed!")) -} diff --git a/src/email/envelope/flag/mod.rs b/src/email/envelope/flag/mod.rs index bd3e0ec..75db6d3 100644 --- a/src/email/envelope/flag/mod.rs +++ b/src/email/envelope/flag/mod.rs @@ -1,8 +1,6 @@ pub mod arg; -pub mod args; pub mod command; pub mod config; -pub mod handlers; use serde::Serialize; use std::{collections::HashSet, ops}; diff --git a/src/email/envelope/mod.rs b/src/email/envelope/mod.rs index 9b025e7..ab856b9 100644 --- a/src/email/envelope/mod.rs +++ b/src/email/envelope/mod.rs @@ -1,3 +1,4 @@ +pub mod arg; pub mod command; pub mod config; pub mod flag; diff --git a/src/email/message/command/copy.rs b/src/email/message/command/copy.rs new file mode 100644 index 0000000..d99e4c2 --- /dev/null +++ b/src/email/message/command/copy.rs @@ -0,0 +1,52 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + envelope::arg::ids::EnvelopeIdsArgs, + folder::arg::name::{SourceFolderNameArg, TargetFolderNameArg}, + printer::Printer, +}; + +/// Copy a message from a source folder to a target folder +#[derive(Debug, Parser)] +pub struct MessageCopyCommand { + #[command(flatten)] + pub source_folder: SourceFolderNameArg, + + #[command(flatten)] + pub target_folder: TargetFolderNameArg, + + #[command(flatten)] + pub envelopes: EnvelopeIdsArgs, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageCopyCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message copy command"); + + let from_folder = &self.source_folder.name; + let to_folder = &self.target_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(), false).await?; + + let ids = &self.envelopes.ids; + backend.copy_messages(from_folder, to_folder, ids).await?; + + printer.print("Message(s) successfully copied from {from_folder} to {to_folder}!") + } +} diff --git a/src/email/message/command/delete.rs b/src/email/message/command/delete.rs new file mode 100644 index 0000000..55dece0 --- /dev/null +++ b/src/email/message/command/delete.rs @@ -0,0 +1,44 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameArg, + printer::Printer, +}; + +/// Delete a message from a folder +#[derive(Debug, Parser)] +pub struct MessageDeleteCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub envelopes: EnvelopeIdsArgs, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageDeleteCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message delete 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(), false).await?; + + let ids = &self.envelopes.ids; + backend.delete_messages(folder, ids).await?; + + printer.print("Message(s) successfully deleted from {from_folder} to {to_folder}!") + } +} diff --git a/src/email/message/command/mod.rs b/src/email/message/command/mod.rs new file mode 100644 index 0000000..903c88a --- /dev/null +++ b/src/email/message/command/mod.rs @@ -0,0 +1,58 @@ +pub mod copy; +pub mod delete; +pub mod move_; +pub mod read; +pub mod save; +pub mod send; + +use anyhow::Result; +use clap::Subcommand; + +use crate::{config::TomlConfig, printer::Printer}; + +use self::{ + copy::MessageCopyCommand, delete::MessageDeleteCommand, move_::MessageMoveCommand, + read::MessageReadCommand, save::MessageSaveCommand, send::MessageSendCommand, +}; + +/// Subcommand to manage messages +#[derive(Debug, Subcommand)] +pub enum MessageSubcommand { + /// Read a message + #[command(arg_required_else_help = true)] + Read(MessageReadCommand), + + /// Save a message to a folder + #[command(arg_required_else_help = true)] + #[command(alias = "add", alias = "create")] + Save(MessageSaveCommand), + + /// Send a message + #[command(arg_required_else_help = true)] + Send(MessageSendCommand), + + /// Copy a message from a source folder to a target folder + #[command(arg_required_else_help = true)] + Copy(MessageCopyCommand), + + /// Move a message from a source folder to a target folder + #[command(arg_required_else_help = true)] + Move(MessageMoveCommand), + + /// Delete a message from a folder + #[command(arg_required_else_help = true)] + Delete(MessageDeleteCommand), +} + +impl MessageSubcommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + match self { + Self::Read(cmd) => cmd.execute(printer, config).await, + Self::Save(cmd) => cmd.execute(printer, config).await, + Self::Send(cmd) => cmd.execute(printer, config).await, + Self::Copy(cmd) => cmd.execute(printer, config).await, + Self::Move(cmd) => cmd.execute(printer, config).await, + Self::Delete(cmd) => cmd.execute(printer, config).await, + } + } +} diff --git a/src/email/message/command/move_.rs b/src/email/message/command/move_.rs new file mode 100644 index 0000000..69b19ac --- /dev/null +++ b/src/email/message/command/move_.rs @@ -0,0 +1,52 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, + backend::Backend, + cache::arg::disable::DisableCacheFlag, + config::TomlConfig, + envelope::arg::ids::EnvelopeIdsArgs, + folder::arg::name::{SourceFolderNameArg, TargetFolderNameArg}, + printer::Printer, +}; + +/// Move a message from a source folder to a target folder +#[derive(Debug, Parser)] +pub struct MessageMoveCommand { + #[command(flatten)] + pub source_folder: SourceFolderNameArg, + + #[command(flatten)] + pub target_folder: TargetFolderNameArg, + + #[command(flatten)] + pub envelopes: EnvelopeIdsArgs, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageMoveCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message move command"); + + let from_folder = &self.source_folder.name; + let to_folder = &self.target_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(), false).await?; + + let ids = &self.envelopes.ids; + backend.move_messages(from_folder, to_folder, ids).await?; + + printer.print("Message(s) successfully moved from {from_folder} to {to_folder}!") + } +} diff --git a/src/email/message/command/read.rs b/src/email/message/command/read.rs new file mode 100644 index 0000000..373547a --- /dev/null +++ b/src/email/message/command/read.rs @@ -0,0 +1,117 @@ +use anyhow::Result; +use clap::Parser; +use log::info; +use mml::message::FilterParts; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameArg, + printer::Printer, +}; + +/// Read a message from a folder +#[derive(Debug, Parser)] +pub struct MessageReadCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub envelopes: EnvelopeIdsArgs, + + /// Read the raw version of the message + /// + /// The raw message represents the message as it is on the + /// backend, unedited: not decoded nor decrypted. This is useful + /// for debugging faulty messages, but also for + /// saving/sending/transfering messages. + #[arg(long, short)] + #[arg(conflicts_with = "no_headers")] + #[arg(conflicts_with = "headers")] + pub raw: bool, + + /// Read only body of text/html parts + /// + /// This argument is useful when you need to read the HTML version + /// of a message. Combined with --no-headers, you can write it to + /// a .html file and open it with your favourite browser. + #[arg(long)] + #[arg(conflicts_with = "raw")] + pub html: bool, + + /// Read only the body of the message + /// + /// All headers will be removed from the message. + #[arg(long)] + #[arg(conflicts_with = "raw")] + #[arg(conflicts_with = "headers")] + pub no_headers: bool, + + /// List of headers that should be visible at the top of the + /// message + /// + /// If a given header is not found in the message, it will not be + /// visible. If no header is given, defaults to the one set up in + /// your TOML configuration file. + #[arg(long = "header", short = 'H', value_name = "NAME")] + #[arg(conflicts_with = "raw")] + #[arg(conflicts_with = "no_headers")] + pub headers: Vec, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageReadCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message read 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(), false).await?; + + let ids = &self.envelopes.ids; + let emails = backend.get_messages(&folder, &ids).await?; + + let mut glue = ""; + let mut bodies = String::default(); + + for email in emails.to_vec() { + bodies.push_str(glue); + + if self.raw { + // emails do not always have valid utf8, uses "lossy" to + // display what can be displayed + bodies.push_str(&String::from_utf8_lossy(email.raw()?).into_owned()); + } else { + let tpl: String = email + .to_read_tpl(&account_config, |mut tpl| { + if self.no_headers { + tpl = tpl.with_hide_all_headers(); + } else if !self.headers.is_empty() { + tpl = tpl.with_show_only_headers(&self.headers); + } + + if self.html { + tpl = tpl.with_filter_parts(FilterParts::Only("text/html".into())); + } + + tpl + }) + .await? + .into(); + bodies.push_str(&tpl); + } + + glue = "\n\n"; + } + + printer.print(bodies) + } +} diff --git a/src/email/message/command/save.rs b/src/email/message/command/save.rs new file mode 100644 index 0000000..0fca634 --- /dev/null +++ b/src/email/message/command/save.rs @@ -0,0 +1,61 @@ +use anyhow::Result; +use atty::Stream; +use clap::Parser; +use log::info; +use std::io::{self, BufRead}; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer, +}; + +/// Save a message to a folder +#[derive(Debug, Parser)] +pub struct MessageSaveCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + /// The raw message to save + #[arg(value_name = "MESSAGE", raw = true)] + pub raw: String, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageSaveCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message save command"); + + let folder = &self.folder.name; + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + let raw_msg = &self.raw; + + 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 is_tty = atty::is(Stream::Stdin); + let is_json = printer.is_json(); + let raw_email = if is_tty || is_json { + raw_msg.replace("\r", "").replace("\n", "\r\n") + } else { + io::stdin() + .lock() + .lines() + .filter_map(Result::ok) + .collect::>() + .join("\r\n") + }; + + backend + .add_raw_message(folder, raw_email.as_bytes()) + .await?; + + printer.print("Message successfully saved to {folder}!") + } +} diff --git a/src/email/message/command/send.rs b/src/email/message/command/send.rs new file mode 100644 index 0000000..f8563eb --- /dev/null +++ b/src/email/message/command/send.rs @@ -0,0 +1,65 @@ +use anyhow::Result; +use atty::Stream; +use clap::Parser; +use email::flag::Flag; +use log::info; +use std::io::{self, BufRead}; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::DisableCacheFlag, + config::TomlConfig, printer::Printer, +}; + +/// Send a message from a folder +#[derive(Debug, Parser)] +pub struct MessageSendCommand { + /// The raw message to send + #[arg(value_name = "MESSAGE", raw = true)] + pub raw: String, + + #[command(flatten)] + pub cache: DisableCacheFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageSendCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing message send command"); + + let account = self.account.name.as_ref().map(String::as_str); + let cache = self.cache.disable; + let raw_msg = &self.raw; + + 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 folder = account_config.sent_folder_alias()?; + + let is_tty = atty::is(Stream::Stdin); + let is_json = printer.is_json(); + let raw_email = if is_tty || is_json { + raw_msg.replace("\r", "").replace("\n", "\r\n") + } else { + io::stdin() + .lock() + .lines() + .filter_map(Result::ok) + .collect::>() + .join("\r\n") + }; + + backend.send_raw_message(raw_email.as_bytes()).await?; + + if account_config.email_sending_save_copy.unwrap_or_default() { + backend + .add_raw_message_with_flag(&folder, raw_email.as_bytes(), Flag::Seen) + .await?; + + printer.print("Message successfully sent and saved to {folder}!") + } else { + printer.print("Message successfully sent!") + } + } +} diff --git a/src/email/message/handlers.rs b/src/email/message/handlers.rs index 01fa6f2..0918e9f 100644 --- a/src/email/message/handlers.rs +++ b/src/email/message/handlers.rs @@ -69,29 +69,6 @@ pub async fn attachments( } } -pub async fn copy( - printer: &mut P, - backend: &Backend, - from_folder: &str, - to_folder: &str, - ids: Vec<&str>, -) -> Result<()> { - backend - .copy_messages(&from_folder, &to_folder, &ids) - .await?; - printer.print("Email(s) successfully copied!") -} - -pub async fn delete( - printer: &mut P, - backend: &Backend, - folder: &str, - ids: Vec<&str>, -) -> Result<()> { - backend.delete_messages(&folder, &ids).await?; - printer.print("Email(s) successfully deleted!") -} - pub async fn forward( config: &AccountConfig, printer: &mut P, @@ -147,60 +124,6 @@ pub async fn mailto( editor::edit_tpl_with_editor(config, printer, backend, tpl).await } -pub async fn move_( - printer: &mut P, - backend: &Backend, - from_folder: &str, - to_folder: &str, - ids: Vec<&str>, -) -> Result<()> { - backend - .move_messages(&from_folder, &to_folder, &ids) - .await?; - printer.print("Email(s) successfully moved!") -} - -pub async fn read( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - folder: &str, - ids: Vec<&str>, - text_mime: &str, - raw: bool, - headers: Vec<&str>, -) -> Result<()> { - let emails = backend.get_messages(&folder, &ids).await?; - - let mut glue = ""; - let mut bodies = String::default(); - - for email in emails.to_vec() { - bodies.push_str(glue); - - if raw { - // emails do not always have valid utf8, uses "lossy" to - // display what can be displayed - bodies.push_str(&String::from_utf8_lossy(email.raw()?).into_owned()); - } else { - let tpl: String = email - .to_read_tpl(&config, |tpl| match text_mime { - "html" => tpl - .with_hide_all_headers() - .with_filter_parts(FilterParts::Only("text/html".into())), - _ => tpl.with_show_additional_headers(&headers), - }) - .await? - .into(); - bodies.push_str(&tpl); - } - - glue = "\n\n"; - } - - printer.print(bodies) -} - pub async fn reply( config: &AccountConfig, printer: &mut P, @@ -230,61 +153,6 @@ pub async fn reply( Ok(()) } -pub async fn save( - printer: &mut P, - backend: &Backend, - folder: &str, - raw_email: String, -) -> Result<()> { - let is_tty = atty::is(Stream::Stdin); - let is_json = printer.is_json(); - let raw_email = if is_tty || is_json { - raw_email.replace("\r", "").replace("\n", "\r\n") - } else { - io::stdin() - .lock() - .lines() - .filter_map(Result::ok) - .collect::>() - .join("\r\n") - }; - - backend - .add_raw_message(&folder, raw_email.as_bytes()) - .await?; - - Ok(()) -} - -pub async fn send( - config: &AccountConfig, - printer: &mut P, - backend: &Backend, - raw_email: String, -) -> Result<()> { - let folder = config.sent_folder_alias()?; - let is_tty = atty::is(Stream::Stdin); - let is_json = printer.is_json(); - let raw_email = if is_tty || is_json { - raw_email.replace("\r", "").replace("\n", "\r\n") - } else { - io::stdin() - .lock() - .lines() - .filter_map(Result::ok) - .collect::>() - .join("\r\n") - }; - trace!("raw email: {:?}", raw_email); - backend.send_raw_message(raw_email.as_bytes()).await?; - if config.email_sending_save_copy.unwrap_or_default() { - backend - .add_raw_message_with_flag(&folder, raw_email.as_bytes(), Flag::Seen) - .await?; - } - Ok(()) -} - pub async fn write( config: &AccountConfig, printer: &mut P, diff --git a/src/email/message/mod.rs b/src/email/message/mod.rs index c101d77..b0fb56e 100644 --- a/src/email/message/mod.rs +++ b/src/email/message/mod.rs @@ -1,4 +1,5 @@ -pub mod args; +// pub mod args; +pub mod command; pub mod config; -pub mod handlers; -pub mod template; +// pub mod handlers; +// pub mod template; diff --git a/src/email/mod.rs b/src/email/mod.rs index 7eae3d8..0e144c4 100644 --- a/src/email/mod.rs +++ b/src/email/mod.rs @@ -2,4 +2,5 @@ pub mod envelope; pub mod message; #[doc(inline)] -pub use self::{envelope::flag, message::template}; +// pub use self::{envelope::flag, message::template}; +pub use self::envelope::flag; diff --git a/src/folder/arg/name.rs b/src/folder/arg/name.rs index 9004fec..730c1a3 100644 --- a/src/folder/arg/name.rs +++ b/src/folder/arg/name.rs @@ -16,3 +16,19 @@ pub struct FolderNameOptionalArg { #[arg(name = "folder-name", value_name = "FOLDER", default_value = DEFAULT_INBOX_FOLDER)] pub name: String, } + +/// The source folder name argument parser +#[derive(Debug, Parser)] +pub struct SourceFolderNameArg { + /// The name of the source folder + #[arg(name = "from-folder-name", value_name = "FROM")] + pub name: String, +} + +/// The target folder name argument parser +#[derive(Debug, Parser)] +pub struct TargetFolderNameArg { + /// The name of the target folder + #[arg(name = "to-folder-name", value_name = "TO")] + pub name: String, +} diff --git a/src/lib.rs b/src/lib.rs index 0cb7c67..eea4ee3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,4 +22,5 @@ pub mod smtp; pub mod ui; #[doc(inline)] -pub use email::{envelope, flag, message, template}; +// pub use email::{envelope, flag, message, template}; +pub use email::{envelope, flag, message}; diff --git a/src/main.rs b/src/main.rs index cbba050..bac06bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,16 +61,6 @@ async fn main() -> Result<()> { // ) // .await; // } -// Some(message::args::Cmd::Copy(ids, to_folder)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return message::handlers::copy(&mut printer, &backend, &folder, to_folder, ids).await; -// } -// Some(message::args::Cmd::Delete(ids)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return message::handlers::delete(&mut printer, &backend, &folder, ids).await; -// } // Some(message::args::Cmd::Forward(id, headers, body)) => { // let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); // let backend = Backend::new(toml_account_config, account_config.clone(), true).await?; @@ -85,26 +75,6 @@ async fn main() -> Result<()> { // ) // .await; // } -// Some(message::args::Cmd::Move(ids, to_folder)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return message::handlers::move_(&mut printer, &backend, &folder, to_folder, ids).await; -// } -// Some(message::args::Cmd::Read(ids, text_mime, raw, headers)) => { -// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); -// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; -// return message::handlers::read( -// &account_config, -// &mut printer, -// &backend, -// &folder, -// ids, -// text_mime, -// raw, -// headers, -// ) -// .await; -// } // Some(message::args::Cmd::Reply(id, all, headers, body)) => { // let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER); // let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;