mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-16 04:17:56 +08:00
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
)),
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"])]
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
Reference in New Issue
Block a user