refactor account with clap derive api

This commit is contained in:
Clément DOUIN 2023-12-06 18:09:49 +01:00
parent d2308221d7
commit abe4c7f4ea
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
21 changed files with 1056 additions and 1029 deletions

13
Cargo.lock generated
View file

@ -483,6 +483,7 @@ dependencies = [
"anstyle",
"clap_lex",
"strsim 0.10.0",
"terminal_size 0.3.0",
]
[[package]]
@ -2094,7 +2095,7 @@ dependencies = [
"shellexpand-utils 0.1.0",
"tempfile",
"termcolor",
"terminal_size",
"terminal_size 0.1.17",
"tokio",
"toml 0.7.8",
"toml_edit 0.19.15",
@ -4104,6 +4105,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "terminal_size"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
dependencies = [
"rustix",
"windows-sys 0.48.0",
]
[[package]]
name = "thiserror"
version = "1.0.50"

View file

@ -59,11 +59,11 @@ version = "0.2"
version = "0.4.24"
[dependencies.clap]
version = "4.0"
features = ["derive"]
version = "4.4"
features = ["derive", "wrap_help"]
[dependencies.clap_complete]
version = "4.0"
version = "4.4"
[dependencies.clap_mangen]
version = "0.2"

View file

@ -1,160 +0,0 @@
//! This module provides arguments related to the user account config.
use anyhow::Result;
use clap::{Arg, ArgAction, ArgMatches, Command};
use email::folder::sync::FolderSyncStrategy;
use log::info;
use std::collections::HashSet;
use crate::{folder, ui::table};
const ARG_ACCOUNT: &str = "account";
const ARG_DRY_RUN: &str = "dry-run";
const ARG_RESET: &str = "reset";
const CMD_ACCOUNT: &str = "account";
const CMD_CONFIGURE: &str = "configure";
const CMD_LIST: &str = "list";
const CMD_SYNC: &str = "sync";
type DryRun = bool;
type Reset = bool;
/// Represents the account commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd {
/// Represents the list accounts command.
List(table::args::MaxTableWidth),
/// Represents the sync account command.
Sync(Option<FolderSyncStrategy>, DryRun),
/// Configure the current selected account.
Configure(Reset),
}
/// Represents the account command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNT) {
if let Some(m) = m.subcommand_matches(CMD_CONFIGURE) {
info!("configure account subcommand matched");
let reset = parse_reset_flag(m);
Some(Cmd::Configure(reset))
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
info!("list accounts subcommand matched");
let max_table_width = table::args::parse_max_width(m);
Some(Cmd::List(max_table_width))
} else if let Some(m) = m.subcommand_matches(CMD_SYNC) {
info!("sync account subcommand matched");
let dry_run = parse_dry_run_arg(m);
let include = folder::args::parse_include_arg(m);
let exclude = folder::args::parse_exclude_arg(m);
let folders_strategy = if let Some(folder) = folder::args::parse_global_source_arg(m) {
Some(FolderSyncStrategy::Include(HashSet::from_iter([
folder.to_owned()
])))
} else if !include.is_empty() {
Some(FolderSyncStrategy::Include(include.to_owned()))
} else if !exclude.is_empty() {
Some(FolderSyncStrategy::Exclude(exclude))
} else if folder::args::parse_all_arg(m) {
Some(FolderSyncStrategy::All)
} else {
None
};
Some(Cmd::Sync(folders_strategy, dry_run))
} else {
None
}
} else {
None
};
Ok(cmd)
}
/// Represents the account subcommand.
pub fn subcmd() -> Command {
Command::new(CMD_ACCOUNT)
.about("Subcommand to manage accounts")
.long_about("Subcommand to manage accounts like configure, list or sync")
.aliases(["accounts", "acc"])
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new(CMD_CONFIGURE)
.about("Configure the given account")
.aliases(["config", "conf", "cfg"])
.arg(reset_flag())
.arg(folder::args::source_arg(
"Define the account to be configured",
)),
)
.subcommand(
Command::new(CMD_LIST)
.about("List all accounts")
.long_about("List all accounts that are set up in the configuration file")
.arg(table::args::max_width()),
)
.subcommand(
Command::new(CMD_SYNC)
.about("Synchronize the given account locally")
.arg(folder::args::all_arg("Synchronize all folders"))
.arg(folder::args::include_arg(
"Synchronize only the given folders",
))
.arg(folder::args::exclude_arg(
"Synchronize all folders except the given ones",
))
.arg(dry_run()),
)
}
/// Represents the user account name argument. This argument allows
/// the user to select a different account than the default one.
pub fn global_args() -> impl IntoIterator<Item = Arg> {
[Arg::new(ARG_ACCOUNT)
.help("Override the default account")
.long_help(
"Override the default account
The given account will be used by default for all other commands (when applicable).",
)
.long("account")
.short('a')
.global(true)
.value_name("name")]
}
/// Represents the user account name argument parser.
pub fn parse_global_arg(matches: &ArgMatches) -> Option<&str> {
matches.get_one::<String>(ARG_ACCOUNT).map(String::as_str)
}
/// Represents the user account sync dry run flag. This flag allows
/// the user to see the changes of a sync without applying them.
pub fn dry_run() -> Arg {
Arg::new(ARG_DRY_RUN)
.help("Do not apply changes of the synchronization")
.long_help(
"Do not apply changes of the synchronization.
Changes can be visualized with the RUST_LOG=trace environment variable.",
)
.short('d')
.long("dry-run")
.action(ArgAction::SetTrue)
}
/// Represents the user account sync dry run flag parser.
pub fn parse_dry_run_arg(m: &ArgMatches) -> bool {
m.get_flag(ARG_DRY_RUN)
}
pub fn reset_flag() -> Arg {
Arg::new(ARG_RESET)
.help("Reset the configuration")
.short('r')
.long("reset")
.action(ArgAction::SetTrue)
}
pub fn parse_reset_flag(m: &ArgMatches) -> bool {
m.get_flag(ARG_RESET)
}

View file

@ -0,0 +1,115 @@
use anyhow::Result;
use clap::Parser;
#[cfg(feature = "imap")]
use email::imap::config::ImapAuthConfig;
#[cfg(feature = "smtp")]
use email::smtp::config::SmtpAuthConfig;
use log::{debug, info, warn};
use crate::{
config::{
wizard::{prompt_passwd, prompt_secret},
TomlConfig,
},
printer::Printer,
};
/// Configure the given account
#[derive(Debug, Parser)]
pub struct Command {
/// The name of the account that needs to be configured
///
/// The account names are taken from the table at the root level
/// of your TOML configuration file.
#[arg(value_name = "NAME")]
pub account_name: String,
/// Force the account to reconfigure, even if it is already
/// configured
#[arg(long, short)]
pub force: bool,
}
impl Command {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing account configure command");
let (_, account_config) =
config.into_toml_account_config(Some(self.account_name.as_str()))?;
if self.force {
#[cfg(feature = "imap")]
if let Some(ref config) = account_config.imap {
let reset = match &config.auth {
ImapAuthConfig::Passwd(config) => config.reset(),
ImapAuthConfig::OAuth2(config) => config.reset(),
};
if let Err(err) = reset {
warn!("error while resetting imap secrets: {err}");
debug!("error while resetting imap secrets: {err:?}");
}
}
#[cfg(feature = "smtp")]
if let Some(ref config) = account_config.smtp {
let reset = match &config.auth {
SmtpAuthConfig::Passwd(config) => config.reset(),
SmtpAuthConfig::OAuth2(config) => config.reset(),
};
if let Err(err) = reset {
warn!("error while resetting smtp secrets: {err}");
debug!("error while resetting smtp secrets: {err:?}");
}
}
#[cfg(feature = "pgp")]
if let Some(ref config) = account_config.pgp {
account_config.pgp.reset().await?;
}
}
#[cfg(feature = "imap")]
if let Some(ref config) = account_config.imap {
match &config.auth {
ImapAuthConfig::Passwd(config) => {
config.configure(|| prompt_passwd("IMAP password")).await
}
ImapAuthConfig::OAuth2(config) => {
config
.configure(|| prompt_secret("IMAP OAuth 2.0 client secret"))
.await
}
}?;
}
#[cfg(feature = "smtp")]
if let Some(ref config) = account_config.smtp {
match &config.auth {
SmtpAuthConfig::Passwd(config) => {
config.configure(|| prompt_passwd("SMTP password")).await
}
SmtpAuthConfig::OAuth2(config) => {
config
.configure(|| prompt_secret("SMTP OAuth 2.0 client secret"))
.await
}
}?;
}
#[cfg(feature = "pgp")]
if let Some(ref config) = config.pgp {
config
.pgp
.configure(&config.email, || prompt_passwd("PGP secret key password"))
.await?;
}
printer.print(format!(
"Account {} successfully {}configured!",
self.account_name,
if self.force { "re" } else { "" }
))?;
Ok(())
}
}

141
src/account/command/list.rs Normal file
View file

@ -0,0 +1,141 @@
use anyhow::Result;
use clap::Parser;
use log::info;
use crate::{
account::Accounts,
config::TomlConfig,
printer::{PrintTableOpts, Printer},
};
/// List all accounts
#[derive(Debug, Parser)]
pub struct Command {
/// Define a maximum width for the table
#[arg(long, short = 'w', name = "PIXELS")]
pub max_width: Option<usize>,
}
impl Command {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing account list command");
let accounts: Accounts = config.accounts.iter().into();
printer.print_table(
Box::new(accounts),
PrintTableOpts {
format: config
.email_reading_format
.as_ref()
.unwrap_or(&Default::default()),
max_width: self.max_width,
},
)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use email::{account::config::AccountConfig, imap::config::ImapConfig};
use std::{collections::HashMap, fmt::Debug, io};
use termcolor::ColorSpec;
use crate::{
account::TomlAccountConfig,
backend::BackendKind,
printer::{Print, PrintTable, WriteColor},
};
use super::*;
#[test]
fn it_should_match_cmds_accounts() {
#[derive(Debug, Default, Clone)]
struct StringWriter {
content: String,
}
impl io::Write for StringWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.content
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.content = String::default();
Ok(())
}
}
impl termcolor::WriteColor for StringWriter {
fn supports_color(&self) -> bool {
false
}
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
io::Result::Ok(())
}
fn reset(&mut self) -> io::Result<()> {
io::Result::Ok(())
}
}
impl WriteColor for StringWriter {}
#[derive(Debug, Default)]
struct PrinterServiceTest {
pub writer: StringWriter,
}
impl Printer for PrinterServiceTest {
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
&mut self,
data: Box<T>,
opts: PrintTableOpts,
) -> Result<()> {
data.print_table(&mut self.writer, opts)?;
Ok(())
}
fn print_log<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
unimplemented!()
}
fn print<T: Debug + Print + serde::Serialize>(&mut self, _data: T) -> Result<()> {
unimplemented!()
}
fn is_json(&self) -> bool {
unimplemented!()
}
}
let mut printer = PrinterServiceTest::default();
let config = AccountConfig::default();
let deserialized_config = TomlConfig {
accounts: HashMap::from_iter([(
"account-1".into(),
TomlAccountConfig {
default: Some(true),
backend: Some(BackendKind::Imap),
imap: Some(ImapConfig::default()),
..Default::default()
},
)]),
..TomlConfig::default()
};
assert!(list(None, &config, &deserialized_config, &mut printer).is_ok());
assert_eq!(
concat![
"\n",
"NAME │BACKEND │DEFAULT \n",
"account-1 │imap │yes \n",
"\n"
],
printer.writer.content
);
}
}

View file

@ -0,0 +1,34 @@
mod configure;
mod list;
mod sync;
use anyhow::Result;
use clap::Subcommand;
use crate::{config::TomlConfig, printer::Printer};
/// Subcommand to manage accounts
#[derive(Debug, Subcommand)]
pub enum Command {
/// Configure the given account
#[command(alias = "cfg")]
Configure(configure::Command),
/// List all exsting accounts
#[command(alias = "lst")]
List(list::Command),
/// Synchronize the given account locally
#[command()]
Sync(sync::Command),
}
impl Command {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self {
Self::Configure(cmd) => cmd.execute(printer, config).await,
Self::List(cmd) => cmd.execute(printer, config).await,
Self::Sync(cmd) => cmd.execute(printer, config).await,
}
}
}

233
src/account/command/sync.rs Normal file
View file

@ -0,0 +1,233 @@
use anyhow::Result;
use clap::{ArgAction, Parser};
use email::{
account::sync::{AccountSyncBuilder, AccountSyncProgressEvent},
folder::sync::FolderSyncStrategy,
};
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
use log::info;
use once_cell::sync::Lazy;
use std::{
collections::{HashMap, HashSet},
sync::Mutex,
};
use crate::{backend::BackendBuilder, config::TomlConfig, printer::Printer};
const MAIN_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap()
});
const SUB_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(
" {prefix:.bold} — {wide_msg:.dim} \n {wide_bar:.black/black} {percent}% ",
)
.unwrap()
});
const SUB_PROGRESS_DONE_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(" {prefix:.bold} \n {wide_bar:.green} {percent}% ").unwrap()
});
#[derive(Debug, Parser)]
pub struct Command {
/// The name of the account that needs to be synchronized
///
/// The account names are taken from the table at the root level
/// of your TOML configuration file.
#[arg(value_name = "ACCOUNT")]
pub account_name: String,
/// Run the synchronization without applying changes
///
/// Instead, a report will be printed to stdout containing all the
/// changes the synchronization plan to do.
#[arg(long, short)]
pub dry_run: bool,
#[arg(long, short = 'f', value_name = "FOLDER", action = ArgAction::Append, conflicts_with = "exclude_folder", conflicts_with = "all_folders")]
pub include_folder: Vec<String>,
#[arg(long, short = 'x', value_name = "FOLDER", action = ArgAction::Append, conflicts_with = "include_folder", conflicts_with = "all_folders")]
pub exclude_folder: Vec<String>,
#[arg(
long,
short = 'A',
conflicts_with = "include_folder",
conflicts_with = "exclude_folder"
)]
pub all_folders: bool,
}
impl Command {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing account sync command");
let included_folders = HashSet::from_iter(self.include_folder);
let excluded_folders = HashSet::from_iter(self.exclude_folder);
let strategy = if !included_folders.is_empty() {
Some(FolderSyncStrategy::Include(included_folders))
} else if !excluded_folders.is_empty() {
Some(FolderSyncStrategy::Exclude(excluded_folders))
} else if self.all_folders {
Some(FolderSyncStrategy::All)
} else {
None
};
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(Some(self.account_name.as_str()), true)?;
let backend_builder =
BackendBuilder::new(toml_account_config, account_config.clone(), false).await?;
let sync_builder = AccountSyncBuilder::new(backend_builder.into())
.await?
.with_some_folders_strategy(strategy)
.with_dry_run(self.dry_run);
if self.dry_run {
let report = sync_builder.sync().await?;
let mut hunks_count = report.folders_patch.len();
if !report.folders_patch.is_empty() {
printer.print_log("Folders patch:")?;
for (hunk, _) in report.folders_patch {
printer.print_log(format!(" - {hunk}"))?;
}
printer.print_log("")?;
}
if !report.emails_patch.is_empty() {
printer.print_log("Envelopes patch:")?;
for (hunk, _) in report.emails_patch {
hunks_count += 1;
printer.print_log(format!(" - {hunk}"))?;
}
printer.print_log("")?;
}
printer.print(format!(
"Estimated patch length for account to be synchronized: {hunks_count}",
))?;
} else if printer.is_json() {
sync_builder.sync().await?;
printer.print("Account successfully synchronized!")?;
} else {
let multi = MultiProgress::new();
let sub_progresses = Mutex::new(HashMap::new());
let main_progress = multi.add(
ProgressBar::new(100)
.with_style(MAIN_PROGRESS_STYLE.clone())
.with_message("Synchronizing folders…"),
);
// Force the progress bar to show
main_progress.set_position(0);
let report = sync_builder
.with_on_progress(move |evt| {
use AccountSyncProgressEvent::*;
Ok(match evt {
ApplyFolderPatches(..) => {
main_progress.inc(3);
}
ApplyEnvelopePatches(patches) => {
let mut envelopes_progresses = sub_progresses.lock().unwrap();
let patches_len =
patches.values().fold(0, |sum, patch| sum + patch.len());
main_progress.set_length((110 * patches_len / 100) as u64);
main_progress.set_position((5 * patches_len / 100) as u64);
main_progress.set_message("Synchronizing envelopes…");
for (folder, patch) in patches {
let progress = ProgressBar::new(patch.len() as u64)
.with_style(SUB_PROGRESS_STYLE.clone())
.with_prefix(folder.clone())
.with_finish(ProgressFinish::AndClear);
let progress = multi.add(progress);
envelopes_progresses.insert(folder, progress.clone());
}
}
ApplyEnvelopeHunk(hunk) => {
main_progress.inc(1);
let mut progresses = sub_progresses.lock().unwrap();
if let Some(progress) = progresses.get_mut(hunk.folder()) {
progress.inc(1);
if progress.position() == (progress.length().unwrap() - 1) {
progress.set_style(SUB_PROGRESS_DONE_STYLE.clone())
} else {
progress.set_message(format!("{hunk}"));
}
}
}
ApplyEnvelopeCachePatch(_patch) => {
main_progress.set_length(100);
main_progress.set_position(95);
main_progress.set_message("Saving cache database…");
}
ExpungeFolders(folders) => {
let mut progresses = sub_progresses.lock().unwrap();
for progress in progresses.values() {
progress.finish_and_clear()
}
progresses.clear();
main_progress.set_position(100);
main_progress
.set_message(format!("Expunging {} folders…", folders.len()));
}
_ => (),
})
})
.sync()
.await?;
let folders_patch_err = report
.folders_patch
.iter()
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
.collect::<Vec<_>>();
if !folders_patch_err.is_empty() {
printer.print_log("")?;
printer.print_log("Errors occurred while applying the folders patch:")?;
folders_patch_err
.iter()
.try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?;
}
if let Some(err) = report.folders_cache_patch.1 {
printer.print_log("")?;
printer.print_log(format!(
"Error occurred while applying the folder cache patch: {err}"
))?;
}
let envelopes_patch_err = report
.emails_patch
.iter()
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
.collect::<Vec<_>>();
if !envelopes_patch_err.is_empty() {
printer.print_log("")?;
printer.print_log("Errors occurred while applying the envelopes patch:")?;
for (hunk, err) in folders_patch_err {
printer.print_log(format!(" - {hunk}: {err}"))?;
}
}
if let Some(err) = report.emails_cache_patch.1 {
printer.print_log("")?;
printer.print_log(format!(
"Error occurred while applying the envelopes cache patch: {err}"
))?;
}
printer.print("Account successfully synchronized!")?;
}
Ok(())
}
}

View file

@ -1,401 +0,0 @@
//! Account handlers module.
//!
//! This module gathers all account actions triggered by the CLI.
use anyhow::Result;
use email::account::{
config::AccountConfig,
sync::{AccountSyncBuilder, AccountSyncProgressEvent},
};
#[cfg(feature = "imap")]
use email::imap::config::ImapAuthConfig;
#[cfg(feature = "smtp")]
use email::smtp::config::SmtpAuthConfig;
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
use log::{debug, info, trace, warn};
use once_cell::sync::Lazy;
use std::{collections::HashMap, sync::Mutex};
use crate::{
account::Accounts,
backend::BackendContextBuilder,
config::{
wizard::{prompt_passwd, prompt_secret},
TomlConfig,
},
printer::{PrintTableOpts, Printer},
};
use super::TomlAccountConfig;
const MAIN_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap()
});
const SUB_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(
" {prefix:.bold} — {wide_msg:.dim} \n {wide_bar:.black/black} {percent}% ",
)
.unwrap()
});
const SUB_PROGRESS_DONE_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(" {prefix:.bold} \n {wide_bar:.green} {percent}% ").unwrap()
});
/// Configure the current selected account
pub async fn configure(config: &TomlAccountConfig, reset: bool) -> Result<()> {
info!("entering the configure account handler");
if reset {
#[cfg(feature = "imap")]
if let Some(ref config) = config.imap {
let reset = match &config.auth {
ImapAuthConfig::Passwd(config) => config.reset(),
ImapAuthConfig::OAuth2(config) => config.reset(),
};
if let Err(err) = reset {
warn!("error while resetting imap secrets: {err}");
debug!("error while resetting imap secrets: {err:?}");
}
}
#[cfg(feature = "smtp")]
if let Some(ref config) = config.smtp {
let reset = match &config.auth {
SmtpAuthConfig::Passwd(config) => config.reset(),
SmtpAuthConfig::OAuth2(config) => config.reset(),
};
if let Err(err) = reset {
warn!("error while resetting smtp secrets: {err}");
debug!("error while resetting smtp secrets: {err:?}");
}
}
#[cfg(feature = "pgp")]
if let Some(ref config) = config.pgp {
config.pgp.reset().await?;
}
}
#[cfg(feature = "imap")]
if let Some(ref config) = config.imap {
match &config.auth {
ImapAuthConfig::Passwd(config) => {
config.configure(|| prompt_passwd("IMAP password")).await
}
ImapAuthConfig::OAuth2(config) => {
config
.configure(|| prompt_secret("IMAP OAuth 2.0 client secret"))
.await
}
}?;
}
#[cfg(feature = "smtp")]
if let Some(ref config) = config.smtp {
match &config.auth {
SmtpAuthConfig::Passwd(config) => {
config.configure(|| prompt_passwd("SMTP password")).await
}
SmtpAuthConfig::OAuth2(config) => {
config
.configure(|| prompt_secret("SMTP OAuth 2.0 client secret"))
.await
}
}?;
}
#[cfg(feature = "pgp")]
if let Some(ref config) = config.pgp {
config
.pgp
.configure(&config.email, || prompt_passwd("PGP secret key password"))
.await?;
}
println!(
"Account successfully {}configured!",
if reset { "re" } else { "" }
);
Ok(())
}
/// Lists all accounts.
pub fn list<'a, P: Printer>(
max_width: Option<usize>,
config: &AccountConfig,
deserialized_config: &TomlConfig,
printer: &mut P,
) -> Result<()> {
info!("entering the list accounts handler");
let accounts: Accounts = deserialized_config.accounts.iter().into();
trace!("accounts: {:?}", accounts);
printer.print_table(
Box::new(accounts),
PrintTableOpts {
format: &config.email_reading_format,
max_width,
},
)?;
info!("<< account list handler");
Ok(())
}
/// Synchronizes the account defined using argument `-a|--account`. If
/// no account given, synchronizes the default one.
pub async fn sync<P: Printer>(
printer: &mut P,
sync_builder: AccountSyncBuilder<BackendContextBuilder>,
dry_run: bool,
) -> Result<()> {
info!("entering the sync accounts handler");
trace!("dry run: {dry_run}");
if dry_run {
let report = sync_builder.sync().await?;
let mut hunks_count = report.folders_patch.len();
if !report.folders_patch.is_empty() {
printer.print_log("Folders patch:")?;
for (hunk, _) in report.folders_patch {
printer.print_log(format!(" - {hunk}"))?;
}
printer.print_log("")?;
}
if !report.emails_patch.is_empty() {
printer.print_log("Envelopes patch:")?;
for (hunk, _) in report.emails_patch {
hunks_count += 1;
printer.print_log(format!(" - {hunk}"))?;
}
printer.print_log("")?;
}
printer.print(format!(
"Estimated patch length for account to be synchronized: {hunks_count}",
))?;
} else if printer.is_json() {
sync_builder.sync().await?;
printer.print("Account successfully synchronized!")?;
} else {
let multi = MultiProgress::new();
let sub_progresses = Mutex::new(HashMap::new());
let main_progress = multi.add(
ProgressBar::new(100)
.with_style(MAIN_PROGRESS_STYLE.clone())
.with_message("Synchronizing folders…"),
);
// Force the progress bar to show
main_progress.set_position(0);
let report = sync_builder
.with_on_progress(move |evt| {
use AccountSyncProgressEvent::*;
Ok(match evt {
ApplyFolderPatches(..) => {
main_progress.inc(3);
}
ApplyEnvelopePatches(patches) => {
let mut envelopes_progresses = sub_progresses.lock().unwrap();
let patches_len = patches.values().fold(0, |sum, patch| sum + patch.len());
main_progress.set_length((110 * patches_len / 100) as u64);
main_progress.set_position((5 * patches_len / 100) as u64);
main_progress.set_message("Synchronizing envelopes…");
for (folder, patch) in patches {
let progress = ProgressBar::new(patch.len() as u64)
.with_style(SUB_PROGRESS_STYLE.clone())
.with_prefix(folder.clone())
.with_finish(ProgressFinish::AndClear);
let progress = multi.add(progress);
envelopes_progresses.insert(folder, progress.clone());
}
}
ApplyEnvelopeHunk(hunk) => {
main_progress.inc(1);
let mut progresses = sub_progresses.lock().unwrap();
if let Some(progress) = progresses.get_mut(hunk.folder()) {
progress.inc(1);
if progress.position() == (progress.length().unwrap() - 1) {
progress.set_style(SUB_PROGRESS_DONE_STYLE.clone())
} else {
progress.set_message(format!("{hunk}"));
}
}
}
ApplyEnvelopeCachePatch(_patch) => {
main_progress.set_length(100);
main_progress.set_position(95);
main_progress.set_message("Saving cache database…");
}
ExpungeFolders(folders) => {
let mut progresses = sub_progresses.lock().unwrap();
for progress in progresses.values() {
progress.finish_and_clear()
}
progresses.clear();
main_progress.set_position(100);
main_progress.set_message(format!("Expunging {} folders…", folders.len()));
}
_ => (),
})
})
.sync()
.await?;
let folders_patch_err = report
.folders_patch
.iter()
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
.collect::<Vec<_>>();
if !folders_patch_err.is_empty() {
printer.print_log("")?;
printer.print_log("Errors occurred while applying the folders patch:")?;
folders_patch_err
.iter()
.try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?;
}
if let Some(err) = report.folders_cache_patch.1 {
printer.print_log("")?;
printer.print_log(format!(
"Error occurred while applying the folder cache patch: {err}"
))?;
}
let envelopes_patch_err = report
.emails_patch
.iter()
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
.collect::<Vec<_>>();
if !envelopes_patch_err.is_empty() {
printer.print_log("")?;
printer.print_log("Errors occurred while applying the envelopes patch:")?;
for (hunk, err) in folders_patch_err {
printer.print_log(format!(" - {hunk}: {err}"))?;
}
}
if let Some(err) = report.emails_cache_patch.1 {
printer.print_log("")?;
printer.print_log(format!(
"Error occurred while applying the envelopes cache patch: {err}"
))?;
}
printer.print("Account successfully synchronized!")?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use email::{account::config::AccountConfig, imap::config::ImapConfig};
use std::{collections::HashMap, fmt::Debug, io};
use termcolor::ColorSpec;
use crate::{
account::TomlAccountConfig,
backend::BackendKind,
printer::{Print, PrintTable, WriteColor},
};
use super::*;
#[test]
fn it_should_match_cmds_accounts() {
#[derive(Debug, Default, Clone)]
struct StringWriter {
content: String,
}
impl io::Write for StringWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.content
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.content = String::default();
Ok(())
}
}
impl termcolor::WriteColor for StringWriter {
fn supports_color(&self) -> bool {
false
}
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
io::Result::Ok(())
}
fn reset(&mut self) -> io::Result<()> {
io::Result::Ok(())
}
}
impl WriteColor for StringWriter {}
#[derive(Debug, Default)]
struct PrinterServiceTest {
pub writer: StringWriter,
}
impl Printer for PrinterServiceTest {
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
&mut self,
data: Box<T>,
opts: PrintTableOpts,
) -> Result<()> {
data.print_table(&mut self.writer, opts)?;
Ok(())
}
fn print_log<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
unimplemented!()
}
fn print<T: Debug + Print + serde::Serialize>(&mut self, _data: T) -> Result<()> {
unimplemented!()
}
fn is_json(&self) -> bool {
unimplemented!()
}
}
let mut printer = PrinterServiceTest::default();
let config = AccountConfig::default();
let deserialized_config = TomlConfig {
accounts: HashMap::from_iter([(
"account-1".into(),
TomlAccountConfig {
default: Some(true),
backend: Some(BackendKind::Imap),
imap: Some(ImapConfig::default()),
..Default::default()
},
)]),
..TomlConfig::default()
};
assert!(list(None, &config, &deserialized_config, &mut printer).is_ok());
assert_eq!(
concat![
"\n",
"NAME │BACKEND │DEFAULT \n",
"account-1 │imap │yes \n",
"\n"
],
printer.writer.content
);
}
}

View file

@ -1,6 +1,5 @@
pub mod args;
pub mod command;
pub mod config;
pub mod handlers;
pub(crate) mod wizard;
use anyhow::Result;
@ -45,7 +44,7 @@ impl Table for Account {
fn head() -> Row {
Row::new()
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
.cell(Cell::new("BACKEND").bold().underline().white())
.cell(Cell::new("BACKENDS").bold().underline().white())
.cell(Cell::new("DEFAULT").bold().underline().white())
}

113
src/cli.rs Normal file
View file

@ -0,0 +1,113 @@
use std::path::PathBuf;
use anyhow::Result;
use clap::{Parser, Subcommand};
use crate::{
account::command as account,
completion::command as completion,
config::{self, TomlConfig},
man::command as man,
output::{ColorFmt, OutputFmt},
printer::Printer,
};
#[derive(Parser, Debug)]
#[command(
name = "himalaya",
author,
version,
about,
propagate_version = true,
infer_subcommands = true
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
/// Override the default configuration file path
///
/// The given path is shell-expanded then canonicalized (if
/// 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)]
pub config: Option<PathBuf>,
/// Customize the output format
///
/// The output format determine how to display commands output to
/// the terminal.
///
/// The possible values are:
///
/// - json: output will be in a form of a JSON-compatible object
///
/// - 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(),
)]
pub output: OutputFmt,
/// Control when to use colors
///
/// The default setting is 'auto', which means himalaya will try
/// to guess when to use colors. For example, if himalaya is
/// printing to a terminal, then it will use colors, but if it is
/// redirected to a file or a pipe, then it will suppress color
/// output. himalaya will suppress color output in some other
/// circumstances as well. For example, if the TERM environment
/// variable is not set or set to 'dumb', then himalaya will not
/// use colors.
///
/// The possible values are:
///
/// - never: colors will never be used
///
/// - always: colors will always be used regardless of where output is sent
///
/// - 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(),
)]
pub color: ColorFmt,
}
/// Top-level CLI commands.
#[derive(Subcommand, Debug)]
pub enum Command {
/// Subcommand to manage accounts
#[command(subcommand)]
Account(account::Command),
/// Generate all man pages to the given directory
#[command(arg_required_else_help = true, alias = "mans")]
Man(man::Command),
/// Print completion script for the given shell to stdout
#[command(arg_required_else_help = true, aliases = ["completions", "compl", "comp"])]
Completion(completion::Command),
}
impl Command {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self {
Self::Account(cmd) => cmd.execute(printer, config).await,
Self::Man(cmd) => cmd.execute(printer).await,
Self::Completion(cmd) => cmd.execute(printer).await,
}
}
}

View file

@ -1,10 +1,32 @@
use clap::{value_parser, Parser};
use anyhow::Result;
use clap::{value_parser, CommandFactory, Parser};
use clap_complete::Shell;
use log::info;
use std::io;
use crate::{cli::Cli, printer::Printer};
/// Print completion script for the given shell to stdout
#[derive(Debug, Parser)]
pub struct Generate {
pub struct Command {
/// Shell that completion script should be generated for
#[arg(value_parser = value_parser!(Shell))]
pub shell: Shell,
}
impl Command {
pub async fn execute(self, printer: &mut impl Printer) -> Result<()> {
info!("executing completion generate command");
let mut cmd = Cli::command();
let name = cmd.get_name().to_string();
clap_complete::generate(self.shell, &mut cmd, name, &mut io::stdout());
printer.print(format!(
"Shell script successfully generated for shell {}!",
self.shell
))?;
Ok(())
}
}

View file

@ -1,18 +0,0 @@
use anyhow::Result;
use clap::Command;
use clap_complete::Shell;
use std::io::stdout;
use crate::printer::Printer;
pub fn generate(printer: &mut impl Printer, mut cmd: Command, shell: Shell) -> Result<()> {
let name = cmd.get_name().to_string();
clap_complete::generate(shell, &mut cmd, name, &mut stdout());
printer.print(format!(
"Shell script successfully generated for shell {shell}!"
))?;
Ok(())
}

View file

@ -1,2 +1 @@
pub mod command;
pub mod handler;

View file

@ -1,8 +1,3 @@
//! Deserialized config module.
//!
//! This module contains the raw deserialized representation of the
//! user configuration file.
pub mod args;
pub mod prelude;
pub mod wizard;
@ -16,6 +11,7 @@ use email::{
email::config::{EmailHooks, EmailTextPlainFormat},
};
use serde::{Deserialize, Serialize};
use shellexpand_utils::{canonicalize, expand};
use std::{
collections::HashMap,
fs,
@ -162,12 +158,10 @@ impl TomlConfig {
.filter(|p| p.exists())
}
/// Build account configurations from a given account name.
pub fn into_account_configs(
self,
pub fn into_toml_account_config(
&self,
account_name: Option<&str>,
disable_cache: bool,
) -> Result<(TomlAccountConfig, AccountConfig)> {
) -> Result<(String, TomlAccountConfig)> {
let (account_name, mut toml_account_config) = match account_name {
Some("default") | Some("") | None => self
.accounts
@ -200,6 +194,18 @@ impl TomlConfig {
.replace_undefined_keyring_entries(&account_name);
}
Ok((account_name, toml_account_config))
}
/// Build account configurations from a given account name.
pub fn into_account_configs(
self,
account_name: Option<&str>,
disable_cache: bool,
) -> Result<(TomlAccountConfig, AccountConfig)> {
let (account_name, mut toml_account_config) =
self.into_toml_account_config(account_name)?;
if let Some(true) = toml_account_config.sync {
if !disable_cache {
toml_account_config.backend = Some(BackendKind::MaildirForSync);
@ -267,6 +273,15 @@ impl TomlConfig {
}
}
/// Parse a configuration file path as [`PathBuf`].
///
/// The path is shell-expanded then canonicalized (if applicable).
pub fn path_parser(path: &str) -> Result<PathBuf, String> {
expand::try_path(path)
.map(canonicalize::path)
.map_err(|err| err.to_string())
}
#[cfg(test)]
mod tests {
use email::{

View file

@ -121,11 +121,6 @@ pub fn all_arg(help: &'static str) -> Arg {
.conflicts_with(ARG_EXCLUDE)
}
/// Represents the all folders argument parser.
pub fn parse_all_arg(m: &ArgMatches) -> bool {
m.get_flag(ARG_ALL)
}
/// Represents the folders to include argument.
pub fn include_arg(help: &'static str) -> Arg {
Arg::new(ARG_INCLUDE)
@ -141,14 +136,6 @@ pub fn include_arg(help: &'static str) -> Arg {
.conflicts_with(ARG_EXCLUDE)
}
/// Represents the folders to include argument parser.
pub fn parse_include_arg(m: &ArgMatches) -> HashSet<String> {
m.get_many::<String>(ARG_INCLUDE)
.unwrap_or_default()
.map(ToOwned::to_owned)
.collect()
}
/// Represents the folders to exclude argument.
pub fn exclude_arg(help: &'static str) -> Arg {
Arg::new(ARG_EXCLUDE)

View file

@ -1,6 +1,7 @@
pub mod account;
pub mod backend;
pub mod cache;
pub mod cli;
pub mod completion;
pub mod config;
pub mod email;

View file

@ -1,361 +1,7 @@
use ::email::account::{config::DEFAULT_INBOX_FOLDER, sync::AccountSyncBuilder};
use anyhow::{anyhow, Context, Result};
use clap::{Command, CommandFactory, Parser, Subcommand};
use anyhow::Result;
use clap::Parser;
use env_logger::{Builder as LoggerBuilder, Env, DEFAULT_FILTER_ENV};
use log::{debug, warn};
use std::env;
use url::Url;
use himalaya::{
account,
backend::{Backend, BackendBuilder},
cache, completion,
config::{self, TomlConfig},
envelope, flag, folder, man, message, output,
printer::StdoutPrinter,
template,
};
fn _create_app() -> Command {
Command::new(env!("CARGO_PKG_NAME"))
.version(env!("CARGO_PKG_VERSION"))
.about(env!("CARGO_PKG_DESCRIPTION"))
.author(env!("CARGO_PKG_AUTHORS"))
.propagate_version(true)
.infer_subcommands(true)
.args(config::args::global_args())
.args(account::args::global_args())
.args(folder::args::global_args())
.args(cache::args::global_args())
.args(output::args::global_args())
.subcommand(account::args::subcmd())
.subcommand(folder::args::subcmd())
.subcommand(envelope::args::subcmd())
.subcommand(flag::args::subcmd())
.subcommand(message::args::subcmd())
.subcommand(template::args::subcmd())
}
#[tokio::main]
async fn _old_main() -> Result<()> {
#[cfg(not(target_os = "windows"))]
if let Err((_, err)) = coredump::register_panic_handler() {
warn!("cannot register custom panic handler: {err}");
debug!("cannot register custom panic handler: {err:?}");
}
let default_env_filter = env_logger::DEFAULT_FILTER_ENV;
env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off"));
// check mailto command before app initialization
let raw_args: Vec<String> = env::args().collect();
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
let url = Url::parse(&raw_args[1])?;
let (toml_account_config, account_config) = TomlConfig::from_default_paths()
.await?
.into_account_configs(None, false)?;
let backend_builder =
BackendBuilder::new(toml_account_config, account_config.clone(), true).await?;
let backend = backend_builder.build().await?;
let mut printer = StdoutPrinter::default();
return message::handlers::mailto(&account_config, &backend, &mut printer, &url).await;
}
let app = _create_app();
let m = app.get_matches();
let some_config_path = config::args::parse_global_arg(&m);
let some_account_name = account::args::parse_global_arg(&m);
let disable_cache = cache::args::parse_disable_cache_arg(&m);
let folder = folder::args::parse_global_source_arg(&m);
let toml_config = TomlConfig::from_some_path_or_default(some_config_path).await?;
let mut printer = StdoutPrinter::try_from(&m)?;
// FIXME
// #[cfg(feature = "imap")]
// if let BackendConfig::Imap(imap_config) = &account_config.backend {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// match imap::args::matches(&m)? {
// Some(imap::args::Cmd::Notify(keepalive)) => {
// let backend =
// ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?;
// imap::handlers::notify(&mut backend, &folder, keepalive).await?;
// return Ok(());
// }
// Some(imap::args::Cmd::Watch(keepalive)) => {
// let backend =
// ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?;
// imap::handlers::watch(&mut backend, &folder, keepalive).await?;
// return Ok(());
// }
// _ => (),
// }
// }
match account::args::matches(&m)? {
Some(account::args::Cmd::List(max_width)) => {
let (_, account_config) = toml_config
.clone()
.into_account_configs(some_account_name, disable_cache)?;
return account::handlers::list(max_width, &account_config, &toml_config, &mut printer);
}
Some(account::args::Cmd::Sync(strategy, dry_run)) => {
let (toml_account_config, account_config) = toml_config
.clone()
.into_account_configs(some_account_name, true)?;
let backend_builder =
BackendBuilder::new(toml_account_config, account_config.clone(), false).await?;
let sync_builder = AccountSyncBuilder::new(backend_builder.into())
.await?
.with_some_folders_strategy(strategy)
.with_dry_run(dry_run);
return account::handlers::sync(&mut printer, sync_builder, dry_run).await;
}
Some(account::args::Cmd::Configure(reset)) => {
let (toml_account_config, _) = toml_config
.clone()
.into_account_configs(some_account_name, disable_cache)?;
return account::handlers::configure(&toml_account_config, reset).await;
}
_ => (),
}
let (toml_account_config, account_config) = toml_config
.clone()
.into_account_configs(some_account_name, disable_cache)?;
// checks folder commands
match folder::args::matches(&m)? {
Some(folder::args::Cmd::Create) => {
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
let folder = folder
.ok_or_else(|| anyhow!("the folder argument is missing"))
.context("cannot create folder")?;
return folder::handlers::create(&mut printer, &backend, &folder).await;
}
Some(folder::args::Cmd::List(max_width)) => {
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
return folder::handlers::list(&account_config, &mut printer, &backend, max_width)
.await;
}
Some(folder::args::Cmd::Expunge) => {
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
return folder::handlers::expunge(&mut printer, &backend, &folder).await;
}
Some(folder::args::Cmd::Delete) => {
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
return folder::handlers::delete(&mut printer, &backend, &folder).await;
}
_ => (),
}
match envelope::args::matches(&m)? {
Some(envelope::args::Cmd::List(max_width, page_size, page)) => {
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
return envelope::handlers::list(
&account_config,
&mut printer,
&backend,
&folder,
max_width,
page_size,
page,
)
.await;
}
_ => (),
}
match flag::args::matches(&m)? {
Some(flag::args::Cmd::Set(ids, ref flags)) => {
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await;
}
Some(flag::args::Cmd::Add(ids, ref flags)) => {
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await;
}
Some(flag::args::Cmd::Remove(ids, ref flags)) => {
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await;
}
_ => (),
}
match message::args::matches(&m)? {
Some(message::args::Cmd::Attachments(ids)) => {
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
return message::handlers::attachments(
&account_config,
&mut printer,
&backend,
&folder,
ids,
)
.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?;
return message::handlers::forward(
&account_config,
&mut printer,
&backend,
&folder,
id,
headers,
body,
)
.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?;
return message::handlers::reply(
&account_config,
&mut printer,
&backend,
&folder,
id,
all,
headers,
body,
)
.await;
}
Some(message::args::Cmd::Save(raw_email)) => {
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
return message::handlers::save(&mut printer, &backend, &folder, raw_email).await;
}
Some(message::args::Cmd::Send(raw_email)) => {
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
return message::handlers::send(&account_config, &mut printer, &backend, raw_email)
.await;
}
Some(message::args::Cmd::Write(headers, body)) => {
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
return message::handlers::write(
&account_config,
&mut printer,
&backend,
headers,
body,
)
.await;
}
_ => (),
}
match template::args::matches(&m)? {
Some(template::args::Cmd::Forward(id, headers, body)) => {
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
return template::handlers::forward(
&account_config,
&mut printer,
&backend,
&folder,
id,
headers,
body,
)
.await;
}
Some(template::args::Cmd::Write(headers, body)) => {
return template::handlers::write(&account_config, &mut printer, headers, body).await;
}
Some(template::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(), false).await?;
return template::handlers::reply(
&account_config,
&mut printer,
&backend,
&folder,
id,
all,
headers,
body,
)
.await;
}
Some(template::args::Cmd::Save(template)) => {
let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
return template::handlers::save(
&account_config,
&mut printer,
&backend,
&folder,
template,
)
.await;
}
Some(template::args::Cmd::Send(template)) => {
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
return template::handlers::send(&account_config, &mut printer, &backend, template)
.await;
}
_ => (),
}
Ok(())
}
#[derive(Parser, Debug)]
#[command(name= "himalaya", author, version, about, long_about = None, propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
Man(man::command::Generate),
#[command(aliases = ["completions", "compl", "comp"])]
Completion(completion::command::Generate),
}
use himalaya::{cli::Cli, config::TomlConfig, printer::StdoutPrinter};
#[tokio::main]
async fn main() -> Result<()> {
@ -364,12 +10,305 @@ async fn main() -> Result<()> {
.format_timestamp(None)
.init();
let mut printer = StdoutPrinter::default();
let cli = Cli::parse();
match Cli::parse().command {
Commands::Man(cmd) => man::handler::generate(&mut printer, Cli::command(), cmd.dir),
Commands::Completion(cmd) => {
completion::handler::generate(&mut printer, Cli::command(), cmd.shell)
}
}
let mut printer = StdoutPrinter::new(cli.output, cli.color);
let config = TomlConfig::from_some_path_or_default(cli.config.as_ref()).await?;
cli.command.execute(&mut printer, &config).await
}
// fn create_app() -> clap::Command {
// clap::Command::new(env!("CARGO_PKG_NAME"))
// .version(env!("CARGO_PKG_VERSION"))
// .about(env!("CARGO_PKG_DESCRIPTION"))
// .author(env!("CARGO_PKG_AUTHORS"))
// .propagate_version(true)
// .infer_subcommands(true)
// .args(folder::args::global_args())
// .args(cache::args::global_args())
// .args(output::args::global_args())
// .subcommand(folder::args::subcmd())
// .subcommand(envelope::args::subcmd())
// .subcommand(flag::args::subcmd())
// .subcommand(message::args::subcmd())
// .subcommand(template::args::subcmd())
// }
// #[tokio::main]
// async fn main() -> Result<()> {
// #[cfg(not(target_os = "windows"))]
// if let Err((_, err)) = coredump::register_panic_handler() {
// warn!("cannot register custom panic handler: {err}");
// debug!("cannot register custom panic handler: {err:?}");
// }
// let default_env_filter = env_logger::DEFAULT_FILTER_ENV;
// env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off"));
// // check mailto command before app initialization
// let raw_args: Vec<String> = env::args().collect();
// if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
// let url = Url::parse(&raw_args[1])?;
// let (toml_account_config, account_config) = TomlConfig::from_default_paths()
// .await?
// .into_account_configs(None, false)?;
// let backend_builder =
// BackendBuilder::new(toml_account_config, account_config.clone(), true).await?;
// let backend = backend_builder.build().await?;
// let mut printer = StdoutPrinter::default();
// return message::handlers::mailto(&account_config, &backend, &mut printer, &url).await;
// }
// let app = _create_app();
// let m = app.get_matches();
// let some_config_path = config::args::parse_global_arg(&m);
// let some_account_name = account::command::parse_global_arg(&m);
// let disable_cache = cache::args::parse_disable_cache_arg(&m);
// let folder = folder::args::parse_global_source_arg(&m);
// let toml_config = TomlConfig::from_some_path_or_default(some_config_path).await?;
// let mut printer = StdoutPrinter::try_from(&m)?;
// #[cfg(feature = "imap")]
// if let BackendConfig::Imap(imap_config) = &account_config.backend {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// match imap::args::matches(&m)? {
// Some(imap::args::Cmd::Notify(keepalive)) => {
// let backend =
// ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?;
// imap::handlers::notify(&mut backend, &folder, keepalive).await?;
// return Ok(());
// }
// Some(imap::args::Cmd::Watch(keepalive)) => {
// let backend =
// ImapBackend::new(account_config.clone(), imap_config.clone(), None).await?;
// imap::handlers::watch(&mut backend, &folder, keepalive).await?;
// return Ok(());
// }
// _ => (),
// }
// }
// let (toml_account_config, account_config) = toml_config
// .clone()
// .into_account_configs(some_account_name, disable_cache)?;
// // checks folder commands
// match folder::args::matches(&m)? {
// Some(folder::args::Cmd::Create) => {
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
// let folder = folder
// .ok_or_else(|| anyhow!("the folder argument is missing"))
// .context("cannot create folder")?;
// return folder::handlers::create(&mut printer, &backend, &folder).await;
// }
// Some(folder::args::Cmd::List(max_width)) => {
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
// return folder::handlers::list(&account_config, &mut printer, &backend, max_width)
// .await;
// }
// Some(folder::args::Cmd::Expunge) => {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
// return folder::handlers::expunge(&mut printer, &backend, &folder).await;
// }
// Some(folder::args::Cmd::Delete) => {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
// return folder::handlers::delete(&mut printer, &backend, &folder).await;
// }
// _ => (),
// }
// match envelope::args::matches(&m)? {
// Some(envelope::args::Cmd::List(max_width, page_size, page)) => {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
// return envelope::handlers::list(
// &account_config,
// &mut printer,
// &backend,
// &folder,
// max_width,
// page_size,
// page,
// )
// .await;
// }
// _ => (),
// }
// match flag::args::matches(&m)? {
// Some(flag::args::Cmd::Set(ids, ref flags)) => {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
// return flag::handlers::set(&mut printer, &backend, &folder, ids, flags).await;
// }
// Some(flag::args::Cmd::Add(ids, ref flags)) => {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
// return flag::handlers::add(&mut printer, &backend, &folder, ids, flags).await;
// }
// Some(flag::args::Cmd::Remove(ids, ref flags)) => {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
// return flag::handlers::remove(&mut printer, &backend, &folder, ids, flags).await;
// }
// _ => (),
// }
// match message::args::matches(&m)? {
// Some(message::args::Cmd::Attachments(ids)) => {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
// return message::handlers::attachments(
// &account_config,
// &mut printer,
// &backend,
// &folder,
// ids,
// )
// .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?;
// return message::handlers::forward(
// &account_config,
// &mut printer,
// &backend,
// &folder,
// id,
// headers,
// body,
// )
// .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?;
// return message::handlers::reply(
// &account_config,
// &mut printer,
// &backend,
// &folder,
// id,
// all,
// headers,
// body,
// )
// .await;
// }
// Some(message::args::Cmd::Save(raw_email)) => {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
// return message::handlers::save(&mut printer, &backend, &folder, raw_email).await;
// }
// Some(message::args::Cmd::Send(raw_email)) => {
// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
// return message::handlers::send(&account_config, &mut printer, &backend, raw_email)
// .await;
// }
// Some(message::args::Cmd::Write(headers, body)) => {
// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
// return message::handlers::write(
// &account_config,
// &mut printer,
// &backend,
// headers,
// body,
// )
// .await;
// }
// _ => (),
// }
// match template::args::matches(&m)? {
// Some(template::args::Cmd::Forward(id, headers, body)) => {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
// return template::handlers::forward(
// &account_config,
// &mut printer,
// &backend,
// &folder,
// id,
// headers,
// body,
// )
// .await;
// }
// Some(template::args::Cmd::Write(headers, body)) => {
// return template::handlers::write(&account_config, &mut printer, headers, body).await;
// }
// Some(template::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(), false).await?;
// return template::handlers::reply(
// &account_config,
// &mut printer,
// &backend,
// &folder,
// id,
// all,
// headers,
// body,
// )
// .await;
// }
// Some(template::args::Cmd::Save(template)) => {
// let folder = folder.unwrap_or(DEFAULT_INBOX_FOLDER);
// let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
// return template::handlers::save(
// &account_config,
// &mut printer,
// &backend,
// &folder,
// template,
// )
// .await;
// }
// Some(template::args::Cmd::Send(template)) => {
// let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
// return template::handlers::send(&account_config, &mut printer, &backend, template)
// .await;
// }
// _ => (),
// }
// Ok(())
// }

View file

@ -1,16 +1,58 @@
use anyhow::Result;
use clap::Parser;
use clap::{CommandFactory, Parser};
use clap_mangen::Man;
use log::info;
use shellexpand_utils::{canonicalize, expand};
use std::path::PathBuf;
use std::{fs, path::PathBuf};
use crate::{cli::Cli, printer::Printer};
/// Generate all man pages to the given directory
#[derive(Debug, Parser)]
pub struct Generate {
pub struct Command {
/// Directory where man files should be generated in
#[arg(value_parser = dir_parser)]
pub dir: PathBuf,
}
impl Command {
pub async fn execute(self, printer: &mut impl Printer) -> Result<()> {
info!("executing man generate command");
let cmd = Cli::command();
let cmd_name = cmd.get_name().to_string();
let subcmds = cmd.get_subcommands().cloned().collect::<Vec<_>>();
let subcmds_len = subcmds.len() + 1;
let mut buffer = Vec::new();
Man::new(cmd).render(&mut buffer)?;
fs::create_dir_all(&self.dir)?;
printer.print_log(format!("Generating man page for command {cmd_name}"))?;
fs::write(self.dir.join(format!("{}.1", cmd_name)), buffer)?;
for subcmd in subcmds {
let subcmd_name = subcmd.get_name().to_string();
let mut buffer = Vec::new();
Man::new(subcmd).render(&mut buffer)?;
printer.print_log(format!("Generating man page for subcommand {subcmd_name}"))?;
fs::write(
self.dir.join(format!("{}-{}.1", cmd_name, subcmd_name)),
buffer,
)?;
}
printer.print(format!(
"{subcmds_len} man page(s) successfully generated in {:?}!",
self.dir
))?;
Ok(())
}
}
/// Parse the given [`str`] as [`PathBuf`].
///
/// The path is first shell expanded, then canonicalized (if

View file

@ -1,35 +0,0 @@
use anyhow::Result;
use clap::Command;
use clap_mangen::Man;
use std::{fs, path::PathBuf};
use crate::printer::Printer;
pub fn generate(printer: &mut impl Printer, cmd: Command, dir: PathBuf) -> Result<()> {
let cmd_name = cmd.get_name().to_string();
let subcmds = cmd.get_subcommands().cloned().collect::<Vec<_>>();
let subcmds_len = subcmds.len() + 1;
let mut buffer = Vec::new();
Man::new(cmd).render(&mut buffer)?;
fs::create_dir_all(&dir)?;
printer.print_log(format!("Generating man page for command {cmd_name}"))?;
fs::write(dir.join(format!("{}.1", cmd_name)), buffer)?;
for subcmd in subcmds {
let subcmd_name = subcmd.get_name().to_string();
let mut buffer = Vec::new();
Man::new(subcmd).render(&mut buffer)?;
printer.print_log(format!("Generating man page for subcommand {subcmd_name}"))?;
fs::write(dir.join(format!("{}-{}.1", cmd_name, subcmd_name)), buffer)?;
}
printer.print(format!(
"Successfully generated {subcmds_len} man page(s) in {dir:?}!"
))?;
Ok(())
}

View file

@ -1,2 +1 @@
pub mod command;
pub mod handler;

View file

@ -1,22 +1,18 @@
use anyhow::{anyhow, Error, Result};
use atty::Stream;
use clap::ValueEnum;
use serde::Serialize;
use std::{fmt, str::FromStr};
use termcolor::ColorChoice;
/// Represents the available output formats.
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
pub enum OutputFmt {
#[default]
Plain,
Json,
}
impl Default for OutputFmt {
fn default() -> Self {
Self::Plain
}
}
impl FromStr for OutputFmt {
type Err = Error;
@ -52,20 +48,15 @@ impl<T: Serialize> OutputJson<T> {
}
/// Represent the available color configs.
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, ValueEnum)]
pub enum ColorFmt {
Never,
Always,
Ansi,
#[default]
Auto,
}
impl Default for ColorFmt {
fn default() -> Self {
Self::Auto
}
}
impl FromStr for ColorFmt {
type Err = Error;