build: release v1.0.0

Refs: #514
This commit is contained in:
Clément DOUIN
2024-12-09 12:04:15 +01:00
committed by GitHub
parent 6e658fef33
commit ce0b2dd8d3
50 changed files with 1021 additions and 788 deletions
-111
View File
@@ -1,111 +0,0 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
#[cfg(feature = "imap")]
use email::imap::ImapContextBuilder;
#[cfg(feature = "maildir")]
use email::maildir::MaildirContextBuilder;
#[cfg(feature = "notmuch")]
use email::notmuch::NotmuchContextBuilder;
#[cfg(feature = "sendmail")]
use email::sendmail::SendmailContextBuilder;
#[cfg(feature = "smtp")]
use email::smtp::SmtpContextBuilder;
use email::{backend::BackendBuilder, config::Config};
use pimalaya_tui::{
himalaya::config::{Backend, SendingBackend},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{account::arg::name::OptionalAccountNameArg, config::TomlConfig};
/// Check up the given account.
///
/// This command performs a checkup of the given account. It checks if
/// the configuration is valid, if backend can be created and if
/// sessions work as expected.
#[derive(Debug, Parser)]
pub struct AccountCheckUpCommand {
#[command(flatten)]
pub account: OptionalAccountNameArg,
}
impl AccountCheckUpCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing check up account command");
printer.log("Checking configuration integrity…\n")?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let account_config = Arc::new(account_config);
match toml_account_config.backend {
#[cfg(feature = "maildir")]
Some(Backend::Maildir(mdir_config)) => {
printer.log("Checking Maildir integrity…\n")?;
let ctx = MaildirContextBuilder::new(account_config.clone(), Arc::new(mdir_config));
BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await?;
}
#[cfg(feature = "imap")]
Some(Backend::Imap(imap_config)) => {
printer.log("Checking IMAP integrity…\n")?;
let ctx = ImapContextBuilder::new(account_config.clone(), Arc::new(imap_config))
.with_pool_size(1);
BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await?;
}
#[cfg(feature = "notmuch")]
Some(Backend::Notmuch(notmuch_config)) => {
printer.log("Checking Notmuch integrity…\n")?;
let ctx =
NotmuchContextBuilder::new(account_config.clone(), Arc::new(notmuch_config));
BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await?;
}
_ => (),
}
let sending_backend = toml_account_config
.message
.and_then(|msg| msg.send)
.and_then(|send| send.backend);
match sending_backend {
#[cfg(feature = "smtp")]
Some(SendingBackend::Smtp(smtp_config)) => {
printer.log("Checking SMTP integrity…\n")?;
let ctx = SmtpContextBuilder::new(account_config.clone(), Arc::new(smtp_config));
BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await?;
}
#[cfg(feature = "sendmail")]
Some(SendingBackend::Sendmail(sendmail_config)) => {
printer.log("Checking Sendmail integrity…\n")?;
let ctx =
SendmailContextBuilder::new(account_config.clone(), Arc::new(sendmail_config));
BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await?;
}
_ => (),
}
printer.out("Checkup successfully completed!\n")
}
}
+30 -108
View File
@@ -1,130 +1,52 @@
use std::path::PathBuf;
use clap::Parser;
use color_eyre::Result;
#[cfg(feature = "imap")]
use email::imap::config::ImapAuthConfig;
#[cfg(feature = "smtp")]
use email::smtp::config::SmtpAuthConfig;
#[cfg(any(
feature = "imap",
feature = "smtp",
feature = "pgp-gpg",
feature = "pgp-commands",
feature = "pgp-native",
))]
use pimalaya_tui::terminal::prompt;
use pimalaya_tui::terminal::{cli::printer::Printer, config::TomlConfig as _};
use tracing::info;
#[cfg(any(feature = "imap", feature = "smtp"))]
use tracing::{debug, warn};
use crate::{account::arg::name::AccountNameArg, config::TomlConfig};
/// Configure an account.
/// Configure the given account.
///
/// This command is mostly used to define or reset passwords managed
/// by your global keyring. If you do not use the keyring system, you
/// can skip this command.
/// This command allows you to configure an existing account or to
/// create a new one, using the wizard. The `wizard` cargo feature is
/// required.
#[derive(Debug, Parser)]
pub struct AccountConfigureCommand {
#[command(flatten)]
pub account: AccountNameArg,
/// Reset keyring passwords.
///
/// This argument will force passwords to be prompted again, then
/// saved to your global keyring.
#[arg(long, short)]
pub reset: bool,
}
impl AccountConfigureCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing configure account command");
#[cfg(feature = "wizard")]
pub async fn execute(
self,
mut config: TomlConfig,
config_path: Option<&PathBuf>,
) -> Result<()> {
use pimalaya_tui::{himalaya::wizard, terminal::config::TomlConfig as _};
use tracing::info;
let account = &self.account.name;
let (_, toml_account_config) = config.to_toml_account_config(Some(account))?;
info!("executing account configure command");
if self.reset {
#[cfg(feature = "imap")]
{
let reset = match toml_account_config.imap_auth_config() {
Some(ImapAuthConfig::Password(config)) => config.reset().await,
#[cfg(feature = "oauth2")]
Some(ImapAuthConfig::OAuth2(config)) => config.reset().await,
_ => Ok(()),
};
let path = match config_path {
Some(path) => path.clone(),
None => TomlConfig::default_path()?,
};
if let Err(err) = reset {
warn!("error while resetting imap secrets: {err}");
debug!("error while resetting imap secrets: {err:?}");
}
}
let account_name = Some(self.account.name.as_str());
#[cfg(feature = "smtp")]
{
let reset = match toml_account_config.smtp_auth_config() {
Some(SmtpAuthConfig::Password(config)) => config.reset().await,
#[cfg(feature = "oauth2")]
Some(SmtpAuthConfig::OAuth2(config)) => config.reset().await,
_ => Ok(()),
};
let account_config = config
.accounts
.remove(&self.account.name)
.unwrap_or_default();
if let Err(err) = reset {
warn!("error while resetting smtp secrets: {err}");
debug!("error while resetting smtp secrets: {err:?}");
}
}
wizard::edit(path, config, account_name, account_config).await?;
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
if let Some(config) = &toml_account_config.pgp {
config.reset().await?;
}
}
Ok(())
}
#[cfg(feature = "imap")]
match toml_account_config.imap_auth_config() {
Some(ImapAuthConfig::Password(config)) => {
config
.configure(|| Ok(prompt::password("IMAP password")?))
.await
}
#[cfg(feature = "oauth2")]
Some(ImapAuthConfig::OAuth2(config)) => {
config
.configure(|| Ok(prompt::secret("IMAP OAuth 2.0 client secret")?))
.await
}
_ => Ok(()),
}?;
#[cfg(feature = "smtp")]
match toml_account_config.smtp_auth_config() {
Some(SmtpAuthConfig::Password(config)) => {
config
.configure(|| Ok(prompt::password("SMTP password")?))
.await
}
#[cfg(feature = "oauth2")]
Some(SmtpAuthConfig::OAuth2(config)) => {
config
.configure(|| Ok(prompt::secret("SMTP OAuth 2.0 client secret")?))
.await
}
_ => Ok(()),
}?;
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
if let Some(config) = &toml_account_config.pgp {
config
.configure(&toml_account_config.email, || {
Ok(prompt::password("PGP secret key password")?)
})
.await?;
}
printer.out(format!(
"Account {account} successfully {}configured!\n",
if self.reset { "re" } else { "" }
))
#[cfg(not(feature = "wizard"))]
pub async fn execute(self, _: TomlConfig, _: Option<&PathBuf>) -> Result<()> {
color_eyre::eyre::bail!("This command requires the `wizard` cargo feature to work");
}
}
+233
View File
@@ -0,0 +1,233 @@
use std::{
io::{stdout, Write},
sync::Arc,
};
use clap::Parser;
use color_eyre::{Result, Section};
#[cfg(all(feature = "keyring", feature = "imap"))]
use email::imap::config::ImapAuthConfig;
#[cfg(feature = "imap")]
use email::imap::ImapContextBuilder;
#[cfg(feature = "maildir")]
use email::maildir::MaildirContextBuilder;
#[cfg(feature = "notmuch")]
use email::notmuch::NotmuchContextBuilder;
#[cfg(feature = "sendmail")]
use email::sendmail::SendmailContextBuilder;
#[cfg(all(feature = "keyring", feature = "smtp"))]
use email::smtp::config::SmtpAuthConfig;
#[cfg(feature = "smtp")]
use email::smtp::SmtpContextBuilder;
use email::{backend::BackendBuilder, config::Config};
#[cfg(feature = "keyring")]
use pimalaya_tui::terminal::prompt;
use pimalaya_tui::{
himalaya::config::{Backend, SendingBackend},
terminal::config::TomlConfig as _,
};
use crate::{account::arg::name::OptionalAccountNameArg, config::TomlConfig};
/// Diagnose and fix the given account.
///
/// This command diagnoses the given account and can even try to fix
/// it. It mostly checks if the configuration is valid, if backends
/// can be instanciated and if sessions work as expected.
#[derive(Debug, Parser)]
pub struct AccountDoctorCommand {
#[command(flatten)]
pub account: OptionalAccountNameArg,
/// Try to fix the given account.
///
/// This argument can be used to (re)configure keyring entries for
/// example.
#[arg(long, short)]
pub fix: bool,
}
impl AccountDoctorCommand {
pub async fn execute(self, config: &TomlConfig) -> Result<()> {
let mut stdout = stdout();
if let Some(name) = self.account.name.as_ref() {
print!("Checking TOML configuration integrity for account {name}");
} else {
print!("Checking TOML configuration integrity for default account… ");
}
stdout.flush()?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let account_config = Arc::new(account_config);
println!("OK");
#[cfg(feature = "keyring")]
if self.fix {
if prompt::bool("Would you like to reset existing keyring entries?", false)? {
print!("Resetting keyring entries… ");
stdout.flush()?;
#[cfg(feature = "imap")]
match toml_account_config.imap_auth_config() {
Some(ImapAuthConfig::Password(config)) => config.reset().await?,
#[cfg(feature = "oauth2")]
Some(ImapAuthConfig::OAuth2(config)) => config.reset().await?,
_ => (),
}
#[cfg(feature = "smtp")]
match toml_account_config.smtp_auth_config() {
Some(SmtpAuthConfig::Password(config)) => config.reset().await?,
#[cfg(feature = "oauth2")]
Some(SmtpAuthConfig::OAuth2(config)) => config.reset().await?,
_ => (),
}
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
if let Some(config) = &toml_account_config.pgp {
config.reset().await?;
}
println!("OK");
}
#[cfg(feature = "imap")]
match toml_account_config.imap_auth_config() {
Some(ImapAuthConfig::Password(config)) => {
config
.configure(|| Ok(prompt::password("IMAP password")?))
.await?;
}
#[cfg(feature = "oauth2")]
Some(ImapAuthConfig::OAuth2(config)) => {
config
.configure(|| Ok(prompt::secret("IMAP OAuth 2.0 client secret")?))
.await?;
}
_ => (),
};
#[cfg(feature = "smtp")]
match toml_account_config.smtp_auth_config() {
Some(SmtpAuthConfig::Password(config)) => {
config
.configure(|| Ok(prompt::password("SMTP password")?))
.await?;
}
#[cfg(feature = "oauth2")]
Some(SmtpAuthConfig::OAuth2(config)) => {
config
.configure(|| Ok(prompt::secret("SMTP OAuth 2.0 client secret")?))
.await?;
}
_ => (),
};
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
if let Some(config) = &toml_account_config.pgp {
config
.configure(&toml_account_config.email, || {
Ok(prompt::password("PGP secret key password")?)
})
.await?;
}
}
match toml_account_config.backend {
#[cfg(feature = "maildir")]
Some(Backend::Maildir(mdir_config)) => {
print!("Checking Maildir integrity… ");
stdout.flush()?;
let ctx = MaildirContextBuilder::new(account_config.clone(), Arc::new(mdir_config));
BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await?;
println!("OK");
}
#[cfg(feature = "imap")]
Some(Backend::Imap(imap_config)) => {
print!("Checking IMAP integrity… ");
stdout.flush()?;
let ctx = ImapContextBuilder::new(account_config.clone(), Arc::new(imap_config))
.with_pool_size(1);
let res = BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await;
if self.fix {
res?;
} else {
res.note("Run with --fix to (re)configure your account.")?;
}
println!("OK");
}
#[cfg(feature = "notmuch")]
Some(Backend::Notmuch(notmuch_config)) => {
print!("Checking Notmuch integrity… ");
stdout.flush()?;
let ctx =
NotmuchContextBuilder::new(account_config.clone(), Arc::new(notmuch_config));
BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await?;
println!("OK");
}
_ => (),
}
let sending_backend = toml_account_config
.message
.and_then(|msg| msg.send)
.and_then(|send| send.backend);
match sending_backend {
#[cfg(feature = "smtp")]
Some(SendingBackend::Smtp(smtp_config)) => {
print!("Checking SMTP integrity… ");
stdout.flush()?;
let ctx = SmtpContextBuilder::new(account_config.clone(), Arc::new(smtp_config));
let res = BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await;
if self.fix {
res?;
} else {
res.note("Run with --fix to (re)configure your account.")?;
}
println!("OK");
}
#[cfg(feature = "sendmail")]
Some(SendingBackend::Sendmail(sendmail_config)) => {
print!("Checking Sendmail integrity… ");
stdout.flush()?;
let ctx =
SendmailContextBuilder::new(account_config.clone(), Arc::new(sendmail_config));
BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await?;
println!("OK");
}
_ => (),
}
Ok(())
}
}
+7 -7
View File
@@ -8,18 +8,19 @@ use tracing::info;
use crate::config::TomlConfig;
/// List all accounts.
/// List all existing accounts.
///
/// This command lists all accounts defined in your TOML configuration
/// file.
/// This command lists all the accounts defined in your TOML
/// configuration file.
#[derive(Debug, Parser)]
pub struct AccountListCommand {
/// The maximum width the table should not exceed.
///
/// This argument will force the table not to exceed the given
/// width in pixels. Columns may shrink with ellipsis in order to
/// width, in pixels. Columns may shrink with ellipsis in order to
/// fit the width.
#[arg(long, short = 'w', name = "table_max_width", value_name = "PIXELS")]
#[arg(long = "max-width", short = 'w')]
#[arg(name = "table_max_width", value_name = "PIXELS")]
pub table_max_width: Option<u16>,
}
@@ -35,7 +36,6 @@ impl AccountListCommand {
.with_some_backends_color(config.account_list_table_backends_color())
.with_some_default_color(config.account_list_table_default_color());
printer.out(table)?;
Ok(())
printer.out(table)
}
}
+17 -17
View File
@@ -1,7 +1,9 @@
mod check_up;
mod configure;
mod doctor;
mod list;
use std::path::PathBuf;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
@@ -9,33 +11,31 @@ use pimalaya_tui::terminal::cli::printer::Printer;
use crate::config::TomlConfig;
use self::{
check_up::AccountCheckUpCommand, configure::AccountConfigureCommand, list::AccountListCommand,
configure::AccountConfigureCommand, doctor::AccountDoctorCommand, list::AccountListCommand,
};
/// Manage accounts.
/// Configure, list and diagnose your accounts.
///
/// An account is a set of settings, identified by an account
/// name. Settings are directly taken from your TOML configuration
/// file. This subcommand allows you to manage them.
/// An account is a group of settings, identified by a unique
/// name. This subcommand allows you to manage your accounts.
#[derive(Debug, Subcommand)]
pub enum AccountSubcommand {
#[command(alias = "checkup")]
CheckUp(AccountCheckUpCommand),
#[command(alias = "cfg")]
Configure(AccountConfigureCommand),
#[command(alias = "lst")]
Doctor(AccountDoctorCommand),
List(AccountListCommand),
}
impl AccountSubcommand {
#[allow(unused)]
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
pub async fn execute(
self,
printer: &mut impl Printer,
config: TomlConfig,
config_path: Option<&PathBuf>,
) -> Result<()> {
match self {
Self::CheckUp(cmd) => cmd.execute(printer, config).await,
Self::Configure(cmd) => cmd.execute(printer, config).await,
Self::List(cmd) => cmd.execute(printer, config).await,
Self::Configure(cmd) => cmd.execute(config, config_path).await,
Self::Doctor(cmd) => cmd.execute(&config).await,
Self::List(cmd) => cmd.execute(printer, &config).await,
}
}
}
+1 -1
View File
@@ -123,7 +123,7 @@ impl HimalayaCommand {
match self {
Self::Account(cmd) => {
let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await
cmd.execute(printer, config, config_paths.first()).await
}
Self::Folder(cmd) => {
let config = TomlConfig::from_paths_or_default(config_paths).await?;
+3 -2
View File
@@ -1,12 +1,13 @@
use std::io;
use clap::{value_parser, CommandFactory, Parser};
use clap_complete::Shell;
use color_eyre::Result;
use std::io;
use tracing::info;
use crate::cli::Cli;
/// Print completion script for a shell to stdout.
/// Print completion script for the given shell to stdout.
///
/// This command allows you to generate completion script for a given
/// shell. The script is printed to the standard output. If you want
+7 -8
View File
@@ -18,12 +18,12 @@ use crate::{
folder::arg::name::FolderNameOptionalFlag,
};
/// List all envelopes.
/// Search and sort envelopes as a list.
///
/// This command allows you to list all envelopes included in the
/// given folder.
/// This command allows you to list envelopes included in the given
/// folder, matching the given query.
#[derive(Debug, Parser)]
pub struct ListEnvelopesCommand {
pub struct EnvelopeListCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
@@ -123,7 +123,7 @@ pub struct ListEnvelopesCommand {
pub query: Option<Vec<String>>,
}
impl Default for ListEnvelopesCommand {
impl Default for EnvelopeListCommand {
fn default() -> Self {
Self {
folder: Default::default(),
@@ -136,7 +136,7 @@ impl Default for ListEnvelopesCommand {
}
}
impl ListEnvelopesCommand {
impl EnvelopeListCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing list envelopes command");
@@ -213,7 +213,6 @@ impl ListEnvelopesCommand {
.with_some_sender_color(toml_account_config.envelope_list_table_sender_color())
.with_some_date_color(toml_account_config.envelope_list_table_date_color());
printer.out(table)?;
Ok(())
printer.out(table)
}
}
+4 -4
View File
@@ -7,9 +7,9 @@ use pimalaya_tui::terminal::cli::printer::Printer;
use crate::config::TomlConfig;
use self::{list::ListEnvelopesCommand, thread::ThreadEnvelopesCommand};
use self::{list::EnvelopeListCommand, thread::EnvelopeThreadCommand};
/// Manage envelopes.
/// List, search and sort your envelopes.
///
/// An envelope is a small representation of a message. It contains an
/// identifier (given by the backend), some flags as well as few
@@ -18,10 +18,10 @@ use self::{list::ListEnvelopesCommand, thread::ThreadEnvelopesCommand};
#[derive(Debug, Subcommand)]
pub enum EnvelopeSubcommand {
#[command(alias = "lst")]
List(ListEnvelopesCommand),
List(EnvelopeListCommand),
#[command()]
Thread(ThreadEnvelopesCommand),
Thread(EnvelopeThreadCommand),
}
impl EnvelopeSubcommand {
+9 -8
View File
@@ -17,12 +17,12 @@ use crate::{
folder::arg::name::FolderNameOptionalFlag,
};
/// Thread all envelopes.
/// Search and sort envelopes as a thread.
///
/// This command allows you to thread all envelopes included in the
/// given folder.
/// This command allows you to thread envelopes included in the given
/// folder, matching the given query.
#[derive(Debug, Parser)]
pub struct ThreadEnvelopesCommand {
pub struct EnvelopeThreadCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
@@ -33,11 +33,14 @@ pub struct ThreadEnvelopesCommand {
#[arg(long, short)]
pub id: Option<usize>,
/// The list envelopes filter and sort query.
///
/// See `envelope list --help` for more information.
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
pub query: Option<Vec<String>>,
}
impl ThreadEnvelopesCommand {
impl EnvelopeThreadCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing thread envelopes command");
@@ -102,9 +105,7 @@ impl ThreadEnvelopesCommand {
let tree = EnvelopesTree::new(account_config, envelopes);
printer.out(tree)?;
Ok(())
printer.out(tree)
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ use crate::{
folder::arg::name::FolderNameOptionalFlag,
};
/// Add flag(s) to an envelope.
/// Add flag(s) to the given envelope.
///
/// This command allows you to attach the given flag(s) to the given
/// envelope(s).
+2 -3
View File
@@ -10,12 +10,11 @@ use crate::config::TomlConfig;
use self::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand};
/// Manage flags.
/// Add, change and remove your envelopes flags.
///
/// A flag is a tag associated to an envelope. Existing flags are
/// seen, answered, flagged, deleted, draft. Other flags are
/// considered custom, which are not always supported (the
/// synchronization does not take care of them yet).
/// considered custom, which are not always supported.
#[derive(Debug, Subcommand)]
pub enum FlagSubcommand {
#[command(arg_required_else_help = true)]
+1 -1
View File
@@ -16,7 +16,7 @@ use crate::{
folder::arg::name::FolderNameOptionalFlag,
};
/// Remove flag(s) from an envelope.
/// Remove flag(s) from a given envelope.
///
/// This command allows you to remove the given flag(s) from the given
/// envelope(s).
+1 -1
View File
@@ -16,7 +16,7 @@ use crate::{
folder::arg::name::FolderNameOptionalFlag,
};
/// Replace flag(s) of an envelope.
/// Replace flag(s) of a given envelope.
///
/// This command allows you to replace existing flags of the given
/// envelope(s) with the given flag(s).
@@ -14,7 +14,7 @@ use crate::{
folder::arg::name::FolderNameOptionalFlag,
};
/// Download all attachments for the given message.
/// Download all attachments found in the given message.
///
/// This command allows you to download all attachments found for the
/// given message to your downloads directory.
@@ -69,14 +69,14 @@ impl AttachmentDownloadCommand {
let attachments = email.attachments()?;
if attachments.is_empty() {
printer.log(format!("No attachment found for message {id}!"))?;
printer.log(format!("No attachment found for message {id}!\n"))?;
continue;
} else {
emails_count += 1;
}
printer.log(format!(
"{} attachment(s) found for message {id}!",
"{} attachment(s) found for message {id}!\n",
attachments.len()
))?;
@@ -86,7 +86,7 @@ impl AttachmentDownloadCommand {
.unwrap_or_else(|| Uuid::new_v4().to_string())
.into();
let filepath = account_config.get_download_file_path(&filename)?;
printer.log(format!("Downloading {:?}", filepath))?;
printer.log(format!("Downloading {:?}\n", filepath))?;
fs::write(&filepath, &attachment.body)
.with_context(|| format!("cannot save attachment at {filepath:?}"))?;
attachments_count += 1;
@@ -94,10 +94,10 @@ impl AttachmentDownloadCommand {
}
match attachments_count {
0 => printer.out("No attachment found!"),
1 => printer.out("Downloaded 1 attachment!"),
0 => printer.out("No attachment found!\n"),
1 => printer.out("Downloaded 1 attachment!\n"),
n => printer.out(format!(
"Downloaded {} attachment(s) from {} messages(s)!",
"Downloaded {} attachment(s) from {} messages(s)!\n",
n, emails_count,
)),
}
+2 -2
View File
@@ -8,14 +8,14 @@ use crate::config::TomlConfig;
use self::download::AttachmentDownloadCommand;
/// Manage attachments.
/// Download your message attachments.
///
/// A message body can be composed of multiple MIME parts. An
/// attachment is the representation of a binary part of a message
/// body.
#[derive(Debug, Subcommand)]
pub enum AttachmentSubcommand {
#[command(arg_required_else_help = true)]
#[command(arg_required_else_help = true, alias = "dl")]
Download(AttachmentDownloadCommand),
}
+2 -1
View File
@@ -16,7 +16,8 @@ use crate::{
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
};
/// Copy a message from a source folder to a target folder.
/// Copy the message associated to the given envelope id(s) to the
/// given target folder.
#[derive(Debug, Parser)]
pub struct MessageCopyCommand {
#[command(flatten)]
+1 -1
View File
@@ -14,7 +14,7 @@ use crate::{
folder::arg::name::FolderNameOptionalFlag,
};
/// Mark as deleted a message from a folder.
/// Mark as deleted the message associated to the given envelope id(s).
///
/// This command does not really delete the message: if the given
/// folder points to the trash folder, it adds the "deleted" flag to
+103
View File
@@ -0,0 +1,103 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::{eyre::eyre, Result};
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, editor},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameOptionalFlag,
};
/// Edit the message associated to the given envelope id.
///
/// This command allows you to edit the given message using the
/// editor defined in your environment variable $EDITOR. When the
/// edition process finishes, you can choose between saving or sending
/// the final message.
#[derive(Debug, Parser)]
pub struct MessageEditCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelope: EnvelopeIdArg,
/// 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")]
pub headers: Vec<String>,
/// Edit the message on place.
///
/// If set, the original message being edited will be removed at
/// the end of the command. Useful when you need, for example, to
/// edit a draft, send it then remove it from the Drafts folder.
#[arg(long, short = 'p')]
pub on_place: bool,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageEditCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing edit message command");
let folder = &self.folder.name;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let account_config = Arc::new(account_config);
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
|builder| {
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
.with_delete_messages(BackendFeatureSource::Context)
},
)
.build()
.await?;
let id = self.envelope.id;
let tpl = backend
.get_messages(folder, &[id])
.await?
.first()
.ok_or(eyre!("cannot find message"))?
.to_read_tpl(&account_config, |mut tpl| {
if !self.headers.is_empty() {
tpl = tpl.with_show_only_headers(&self.headers);
}
tpl
})
.await?;
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await?;
if self.on_place {
backend.delete_messages(folder, &[id]).await?;
}
Ok(())
}
}
+155
View File
@@ -0,0 +1,155 @@
use std::{
env::temp_dir,
fs,
io::{stdout, Write},
path::PathBuf,
sync::Arc,
};
use clap::Parser;
use color_eyre::{eyre::eyre, Result};
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{himalaya::backend::BackendBuilder, terminal::config::TomlConfig as _};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameOptionalFlag,
};
/// Export the message associated to the given envelope id.
///
/// This command allows you to export a message. A message can be
/// fully exported in one single file, or exported in multiple files
/// (one per MIME part found in the message). This is useful, for
/// example, to read a HTML message.
#[derive(Debug, Parser)]
pub struct MessageExportCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelope: EnvelopeIdArg,
/// Export the full raw message as one unique .eml file.
///
/// The raw message represents the headers and the body 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 = 'F')]
pub full: bool,
/// Try to open the exported message, when applicable.
///
/// This argument only works with full message export, or when
/// HTML or plain text is present in the export.
#[arg(long, short = 'O')]
pub open: bool,
/// Where the message should be exported to.
///
/// The destination should point to a valid directory. If `--full`
/// is given, it can also point to a .eml file.
#[arg(long, short, alias = "dest")]
pub destination: Option<PathBuf>,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageExportCommand {
pub async fn execute(self, config: &TomlConfig) -> Result<()> {
info!("executing export message command");
let folder = &self.folder.name;
let id = &self.envelope.id;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let account_config = Arc::new(account_config);
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
|builder| {
builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let msgs = backend.get_messages(folder, &[*id]).await?;
let msg = msgs.first().ok_or(eyre!("cannot find message {id}"))?;
if self.full {
let bytes = msg.raw()?;
match self.destination {
Some(mut dest) if dest.is_dir() => {
dest.push(format!("{id}.eml"));
fs::write(&dest, bytes)?;
let dest = dest.display();
println!("Message {id} successfully exported at {dest}!");
}
Some(dest) => {
fs::write(&dest, bytes)?;
let dest = dest.display();
println!("Message {id} successfully exported at {dest}!");
}
None => {
stdout().write_all(bytes)?;
}
};
} else {
let dest = match self.destination {
Some(dest) if dest.is_dir() => {
let dest = msg.download_parts(&dest)?;
let d = dest.display();
println!("Message {id} successfully exported in {d}!");
dest
}
Some(dest) if dest.is_file() => {
let dest = dest.parent().unwrap_or(&dest);
let dest = msg.download_parts(&dest)?;
let d = dest.display();
println!("Message {id} successfully exported in {d}!");
dest
}
Some(dest) => {
return Err(eyre!("Destination {} does not exist!", dest.display()));
}
None => {
let dest = temp_dir();
let dest = msg.download_parts(&dest)?;
let d = dest.display();
println!("Message {id} successfully exported in {d}!");
dest
}
};
if self.open {
let index_html = dest.join("index.html");
if index_html.exists() {
return Ok(open::that(index_html)?);
}
let plain_txt = dest.join("plain.txt");
if plain_txt.exists() {
return Ok(open::that(plain_txt)?);
}
println!("--open was passed but nothing to open, ignoring");
}
}
Ok(())
}
}
+1 -2
View File
@@ -17,7 +17,7 @@ use crate::{
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
};
/// Forward a message.
/// Forward the message associated to the given envelope id.
///
/// This command allows you to forward the given message using the
/// editor defined in your environment variable $EDITOR. When the
@@ -65,7 +65,6 @@ impl MessageForwardCommand {
.with_send_message(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
+1 -1
View File
@@ -12,7 +12,7 @@ use url::Url;
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
/// Parse and edit a message from a mailto URL string.
/// Parse and edit a message from the given mailto URL string.
///
/// This command allows you to edit a message from the mailto format
/// using the editor defined in your environment variable
+15 -7
View File
@@ -1,5 +1,7 @@
pub mod copy;
pub mod delete;
pub mod edit;
pub mod export;
pub mod forward;
pub mod mailto;
pub mod r#move;
@@ -17,13 +19,14 @@ use pimalaya_tui::terminal::cli::printer::Printer;
use crate::config::TomlConfig;
use self::{
copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand,
mailto::MessageMailtoCommand, r#move::MessageMoveCommand, read::MessageReadCommand,
reply::MessageReplyCommand, save::MessageSaveCommand, send::MessageSendCommand,
thread::MessageThreadCommand, write::MessageWriteCommand,
copy::MessageCopyCommand, delete::MessageDeleteCommand, edit::MessageEditCommand,
export::MessageExportCommand, forward::MessageForwardCommand, mailto::MessageMailtoCommand,
r#move::MessageMoveCommand, read::MessageReadCommand, reply::MessageReplyCommand,
save::MessageSaveCommand, send::MessageSendCommand, thread::MessageThreadCommand,
write::MessageWriteCommand,
};
/// Manage messages.
/// Read, write, send, copy, move and delete your messages.
///
/// A message is the content of an email. It is composed of headers
/// (located at the top of the message) and a body (located at the
@@ -34,19 +37,22 @@ pub enum MessageSubcommand {
#[command(arg_required_else_help = true)]
Read(MessageReadCommand),
#[command(arg_required_else_help = true)]
Export(MessageExportCommand),
#[command(arg_required_else_help = true)]
Thread(MessageThreadCommand),
#[command(aliases = ["add", "create", "new", "compose"])]
Write(MessageWriteCommand),
#[command()]
Reply(MessageReplyCommand),
#[command(aliases = ["fwd", "fd"])]
Forward(MessageForwardCommand),
#[command()]
Edit(MessageEditCommand),
Mailto(MessageMailtoCommand),
Save(MessageSaveCommand),
@@ -71,10 +77,12 @@ 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::Export(cmd) => cmd.execute(config).await,
Self::Thread(cmd) => cmd.execute(printer, config).await,
Self::Write(cmd) => cmd.execute(printer, config).await,
Self::Reply(cmd) => cmd.execute(printer, config).await,
Self::Forward(cmd) => cmd.execute(printer, config).await,
Self::Edit(cmd) => cmd.execute(printer, config).await,
Self::Mailto(cmd) => cmd.execute(printer, config).await,
Self::Save(cmd) => cmd.execute(printer, config).await,
Self::Send(cmd) => cmd.execute(printer, config).await,
+2 -1
View File
@@ -17,7 +17,8 @@ use crate::{
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
};
/// Move a message from a source folder to a target folder.
/// Move the message associated to the given envelope id(s) to the
/// given target folder.
#[derive(Debug, Parser)]
pub struct MessageMoveCommand {
#[command(flatten)]
+15 -46
View File
@@ -3,7 +3,6 @@ use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use mml::message::FilterParts;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
@@ -16,11 +15,12 @@ use crate::{
folder::arg::name::FolderNameOptionalFlag,
};
/// Read a message.
/// Read a human-friendly version of the message associated to the
/// given envelope id(s).
///
/// This command allows you to read a message. When reading a message,
/// the "seen" flag is automatically applied to the corresponding
/// envelope. To prevent this behaviour, use the --preview flag.
/// envelope. To prevent this behaviour, use the "--preview" flag.
#[derive(Debug, Parser)]
pub struct MessageReadCommand {
#[command(flatten)]
@@ -34,31 +34,10 @@ pub struct MessageReadCommand {
#[arg(long, short)]
pub preview: bool,
/// Read the raw version of the given message.
///
/// The raw message represents the headers and the body 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,
@@ -69,7 +48,6 @@ pub struct MessageReadCommand {
/// 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<String>,
@@ -99,6 +77,7 @@ impl MessageReadCommand {
builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
.with_peek_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
@@ -117,28 +96,18 @@ impl MessageReadCommand {
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()?));
} else {
let tpl = 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);
}
let tpl = 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?;
bodies.push_str(&tpl);
}
tpl
})
.await?;
bodies.push_str(&tpl);
glue = "\n\n";
}
+1 -1
View File
@@ -17,7 +17,7 @@ use crate::{
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg},
};
/// Reply to a message.
/// Reply to the message associated to the given envelope id.
///
/// This command allows you to reply to the given message using the
/// editor defined in your environment variable $EDITOR. When the
+1 -1
View File
@@ -16,7 +16,7 @@ use crate::{
folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg,
};
/// Save a message to a folder.
/// Save the given raw message to the given folder.
///
/// This command allows you to add a raw message to the given folder.
#[derive(Debug, Parser)]
+1 -1
View File
@@ -13,7 +13,7 @@ use tracing::info;
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig, message::arg::MessageRawArg};
/// Send a message.
/// Send the given raw message.
///
/// This command allows you to send a raw message and to save a copy
/// to your send folder.
+14 -45
View File
@@ -3,7 +3,6 @@ use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use mml::message::FilterParts;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
@@ -17,7 +16,8 @@ use crate::{
folder::arg::name::FolderNameOptionalFlag,
};
/// Thread a message.
/// Read human-friendly version of messages associated to the
/// given envelope id's thread.
///
/// This command allows you to thread a message. When threading a message,
/// the "seen" flag is automatically applied to the corresponding
@@ -35,31 +35,10 @@ pub struct MessageThreadCommand {
#[arg(long, short)]
pub preview: bool,
/// Thread the raw version of the given message.
///
/// The raw message represents the headers and the body 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,
/// Thread only body of text/html parts.
///
/// This argument is useful when you need to thread 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,
/// Thread 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,
@@ -70,7 +49,6 @@ pub struct MessageThreadCommand {
/// 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<String>,
@@ -100,6 +78,7 @@ impl MessageThreadCommand {
builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
.with_peek_messages(BackendFeatureSource::Context)
.with_thread_envelopes(BackendFeatureSource::Context)
},
)
@@ -130,29 +109,19 @@ impl MessageThreadCommand {
bodies.push_str(glue);
bodies.push_str(&format!("-------- Message {} --------\n\n", ids[i + 1]));
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()?));
} else {
let tpl = 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);
}
let tpl = 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?;
bodies.push_str(&tpl);
}
tpl
})
.await?;
bodies.push_str(&tpl);
glue = "\n\n";
}
+1 -1
View File
@@ -18,7 +18,7 @@ use crate::{
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
};
/// Write a new message.
/// Compose a new message, from scratch.
///
/// This command allows you to write a new message using the editor
/// defined in your environment variable $EDITOR. When the edition
+2 -3
View File
@@ -15,15 +15,14 @@ use self::{
send::TemplateSendCommand, write::TemplateWriteCommand,
};
/// Manage templates.
/// Generate, save and send message templates.
///
/// A template is an editable version of a message (headers +
/// body). It uses a specific language called MML that allows you to
/// attach file or encrypt content. This subcommand allows you manage
/// them.
///
/// You can learn more about MML at
/// <https://crates.io/crates/mml-lib>.
/// Learn more about MML at: <https://crates.io/crates/mml-lib>.
#[derive(Debug, Subcommand)]
pub enum TemplateSubcommand {
#[command(aliases = ["add", "create", "new", "compose"])]
+3 -3
View File
@@ -16,12 +16,12 @@ use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Create a new folder.
/// Create the given folder.
///
/// This command allows you to create a new folder using the given
/// name.
#[derive(Debug, Parser)]
pub struct AddFolderCommand {
pub struct FolderAddCommand {
#[command(flatten)]
pub folder: FolderNameArg,
@@ -29,7 +29,7 @@ pub struct AddFolderCommand {
pub account: AccountNameFlag,
}
impl AddFolderCommand {
impl FolderAddCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing create folder command");
+1 -1
View File
@@ -16,7 +16,7 @@ use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Delete a folder.
/// Delete the given folder.
///
/// All emails from the given folder are definitely deleted. The
/// folder is also deleted after execution of the command.
+1 -1
View File
@@ -15,7 +15,7 @@ use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Expunge a folder.
/// Expunge the given folder.
///
/// The concept of expunging is similar to the IMAP one: it definitely
/// deletes emails from the given folder that contain the "deleted"
+3 -2
View File
@@ -28,9 +28,10 @@ pub struct FolderListCommand {
/// The maximum width the table should not exceed.
///
/// This argument will force the table not to exceed the given
/// width in pixels. Columns may shrink with ellipsis in order to
/// width, in pixels. Columns may shrink with ellipsis in order to
/// fit the width.
#[arg(long, short = 'w', name = "table_max_width", value_name = "PIXELS")]
#[arg(long = "max-width", short = 'w')]
#[arg(name = "table_max_width", value_name = "PIXELS")]
pub table_max_width: Option<u16>,
}
+5 -5
View File
@@ -11,18 +11,18 @@ use pimalaya_tui::terminal::cli::printer::Printer;
use crate::config::TomlConfig;
use self::{
add::AddFolderCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand,
add::FolderAddCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand,
list::FolderListCommand, purge::FolderPurgeCommand,
};
/// Manage folders.
/// Create, list and purge your folders (as known as mailboxes).
///
/// A folder (as known as mailbox, or directory) contains one or more
/// emails. This subcommand allows you to manage them.
/// A folder (as known as mailbox, or directory) is a messages
/// container. This subcommand allows you to manage them.
#[derive(Debug, Subcommand)]
pub enum FolderSubcommand {
#[command(visible_alias = "create", alias = "new")]
Add(AddFolderCommand),
Add(FolderAddCommand),
#[command(alias = "lst")]
List(FolderListCommand),
+1 -1
View File
@@ -13,7 +13,7 @@ use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Purge a folder.
/// Purge the given folder.
///
/// All emails from the given folder are definitely deleted. The
/// purged folder will remain empty after execution of the command.
+2 -2
View File
@@ -1,7 +1,7 @@
use clap::Parser;
use color_eyre::Result;
use himalaya::{
cli::Cli, config::TomlConfig, envelope::command::list::ListEnvelopesCommand,
cli::Cli, config::TomlConfig, envelope::command::list::EnvelopeListCommand,
message::command::mailto::MessageMailtoCommand,
};
use pimalaya_tui::terminal::{
@@ -37,7 +37,7 @@ async fn main() -> Result<()> {
Some(cmd) => cmd.execute(&mut printer, cli.config_paths.as_ref()).await,
None => {
let config = TomlConfig::from_paths_or_default(cli.config_paths.as_ref()).await?;
ListEnvelopesCommand::default()
EnvelopeListCommand::default()
.execute(&mut printer, &config)
.await
}
+9 -6
View File
@@ -1,14 +1,15 @@
use std::{fs, path::PathBuf};
use clap::{CommandFactory, Parser};
use clap_mangen::Man;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use shellexpand_utils::{canonicalize, expand};
use std::{fs, path::PathBuf};
use tracing::info;
use crate::cli::Cli;
/// Generate manual pages to a directory.
/// Generate manual pages to the given directory.
///
/// This command allows you to generate manual pages (following the
/// man page format) to the given directory. If the directory does not
@@ -34,7 +35,7 @@ impl ManualGenerateCommand {
Man::new(cmd).render(&mut buffer)?;
fs::create_dir_all(&self.dir)?;
printer.log(format!("Generating man page for command {cmd_name}"))?;
printer.log(format!("Generating man page for command {cmd_name}\n"))?;
fs::write(self.dir.join(format!("{}.1", cmd_name)), buffer)?;
for subcmd in subcmds {
@@ -43,7 +44,9 @@ impl ManualGenerateCommand {
let mut buffer = Vec::new();
Man::new(subcmd).render(&mut buffer)?;
printer.log(format!("Generating man page for subcommand {subcmd_name}"))?;
printer.log(format!(
"Generating man page for subcommand {subcmd_name}\n"
))?;
fs::write(
self.dir.join(format!("{}-{}.1", cmd_name, subcmd_name)),
buffer,
@@ -51,8 +54,8 @@ impl ManualGenerateCommand {
}
printer.log(format!(
"{subcmds_len} man page(s) successfully generated in {:?}!",
self.dir
"{subcmds_len} man page(s) successfully generated in {}!\n",
self.dir.display()
))?;
Ok(())