improve modules structure

This commit is contained in:
Clément DOUIN
2026-03-09 11:34:26 +01:00
parent 83be576b80
commit fe0f5cec85
96 changed files with 289 additions and 3249 deletions
Generated
+19 -147
View File
@@ -91,17 +91,6 @@ dependencies = [
"object",
]
[[package]]
name = "ariadne"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "367fd0ad87307588d087544707bc5fbf4805ded96c7db922b70d368fa1cb5702"
dependencies = [
"concolor",
"unicode-width 0.1.14",
"yansi",
]
[[package]]
name = "autocfg"
version = "1.5.0"
@@ -136,12 +125,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.11.0"
@@ -168,12 +151,6 @@ dependencies = [
"syn",
]
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "bytes"
version = "1.11.1"
@@ -333,23 +310,6 @@ dependencies = [
"unicode-width 0.2.2",
]
[[package]]
name = "concolor"
version = "0.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "318d6c16e73b3a900eb212ad6a82fc7d298c5ab8184c7a9998646455bc474a16"
dependencies = [
"bitflags 1.3.2",
"concolor-query",
"is-terminal",
]
[[package]]
name = "concolor-query"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82a90734b3d5dcf656e7624cca6bce9c3a90ee11f900e80141a7427ccfb3d317"
[[package]]
name = "core-foundation"
version = "0.10.1"
@@ -372,7 +332,7 @@ version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.11.0",
"bitflags",
"crossterm_winapi",
"libc",
"mio",
@@ -389,7 +349,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [
"bitflags 2.11.0",
"bitflags",
"crossterm_winapi",
"document-features",
"parking_lot",
@@ -611,7 +571,7 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
dependencies = [
"bitflags 2.11.0",
"bitflags",
"libc",
"libgit2-sys",
"log",
@@ -641,18 +601,11 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "himalaya"
version = "1.2.0"
dependencies = [
"anyhow",
"ariadne",
"chrono",
"clap",
"comfy-table",
@@ -673,11 +626,9 @@ dependencies = [
"rustls-platform-verifier",
"secrecy",
"serde",
"serde_json",
"shellexpand",
"uds_windows",
"url",
"uuid",
]
[[package]]
@@ -909,17 +860,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
@@ -998,16 +938,6 @@ dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
@@ -1228,7 +1158,7 @@ version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [
"bitflags 2.11.0",
"bitflags",
"cfg-if",
"foreign-types",
"libc",
@@ -1254,6 +1184,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-src"
version = "300.5.5+3.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.111"
@@ -1262,6 +1201,7 @@ checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
@@ -1503,7 +1443,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.11.0",
"bitflags",
]
[[package]]
@@ -1603,7 +1543,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.11.0",
"bitflags",
"errno",
"libc",
"linux-raw-sys",
@@ -1685,12 +1625,6 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
version = "1.0.6"
@@ -1731,7 +1665,7 @@ version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.11.0",
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -2177,17 +2111,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
dependencies = [
"getrandom 0.4.2",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -2228,51 +2151,6 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
@@ -2301,7 +2179,7 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags 2.11.0",
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
@@ -2717,7 +2595,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags 2.11.0",
"bitflags",
"indexmap",
"log",
"serde",
@@ -2753,12 +2631,6 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "yoke"
version = "0.8.1"
+7 -16
View File
@@ -12,33 +12,26 @@ documentation = "https://github.com/pimalaya/himalaya"
repository = "https://github.com/pimalaya/himalaya"
[package.metadata.docs.rs]
features = ["imap", "maildir", "smtp", "sendmail", "oauth2", "wizard", "pgp-commands", "pgp-native"]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = ["smtp", "imap", "rustls-ring"]
smtp = ["dep:io-smtp"]
default = ["imap", "smtp", "rustls-ring"]
imap = ["dep:io-imap"]
smtp = ["dep:io-smtp"]
rustls-aws = ["dep:rustls-platform-verifier", "rustls/aws-lc-rs"]
rustls-ring = ["dep:rustls-platform-verifier", "rustls/ring"]
native-tls = ["dep:native-tls"]
# maildir = ["email-lib/maildir", "pimalaya-tui/maildir"]
# notmuch = ["email-lib/notmuch", "pimalaya-tui/notmuch"]
# smtp = ["email-lib/smtp", "pimalaya-tui/smtp"]
# sendmail = ["email-lib/sendmail", "pimalaya-tui/sendmail"]
# keyring = ["email-lib/keyring", "pimalaya-tui/keyring", "secret-lib/keyring"]
# oauth2 = ["email-lib/oauth2", "pimalaya-tui/oauth2", "keyring"]
# wizard = ["email-lib/autoconfig", "pimalaya-tui/wizard"]
# pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pimalaya-tui/pgp-commands"]
# pgp-gpg = ["email-lib/pgp-gpg", "mml-lib/pgp-gpg", "pimalaya-tui/pgp-gpg"]
# pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "pimalaya-tui/pgp-native"]
vendored = ["native-tls?/vendored"]
[build-dependencies]
pimalaya-toolbox = { version = "0.0.4", default-features = false, features = ["build"] }
[dependencies]
anyhow = "1"
ariadne = { version = "0.2", features = ["auto-color"] }
chrono = { version = "0.4", default-features = false }
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
comfy-table = "7"
@@ -59,10 +52,8 @@ rustls = { version = "0.23", default-features = false, optional = true }
rustls-platform-verifier = { version = "0.6", optional = true }
secrecy = "0.10"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
shellexpand = "3.1"
url = { version = "2.2", features = ["serde"] }
uuid = { version = "1.19", features = ["v4"] }
[target.'cfg(windows)'.dependencies]
uds_windows = "1"
+11 -7
View File
@@ -61,31 +61,35 @@ pub enum BackendCommand {
}
impl BackendCommand {
pub fn execute(
pub fn exec(
self,
printer: &mut impl Printer,
config_paths: &[PathBuf],
account_name: Option<&str>,
) -> Result<()> {
match self {
#[cfg(feature = "imap")]
Self::Imap(cmd) => {
let config = Config::from_paths_or_default(config_paths)?;
let (_, account_config) = config.get_account(account_name)?;
let (account_name, account_config) = config.get_account(account_name)?;
let Some(imap_config) = account_config.imap else {
bail!("IMAP config is missing for this account")
bail!("IMAP config is missing for account `{account_name}`")
};
cmd.execute(printer, imap_config)
cmd.exec(printer, imap_config)
}
#[cfg(feature = "smtp")]
Self::Smtp(cmd) => {
let config = Config::from_paths_or_default(config_paths)?;
let (_, account_config) = config.get_account(account_name)?;
let (account_name, account_config) = config.get_account(account_name)?;
let Some(smtp_config) = account_config.smtp else {
bail!("SMTP config is missing for this account")
bail!("SMTP config is missing for account `{account_name}`")
};
cmd.execute(printer, smtp_config)
cmd.exec(printer, smtp_config)
}
#[allow(unreachable_patterns)]
_ => bail!("No backend available"),
}
}
}
+77 -76
View File
@@ -6,16 +6,18 @@ use secrecy::SecretString;
use serde::{de::Visitor, Deserialize, Deserializer};
use url::Url;
/// Global configuration.
///
/// Represents the whole TOML user's configuration file.
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Config {
#[serde(alias = "name")]
pub display_name: Option<String>,
pub signature: Option<String>,
pub signature_delim: Option<String>,
pub downloads_dir: Option<PathBuf>,
pub accounts: HashMap<String, AccountConfig>,
// pub account: Option<AccountsConfig>,
}
impl TomlConfig for Config {
@@ -39,35 +41,62 @@ impl TomlConfig for Config {
}
}
/// The account configuration.
/// Account configuration.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct AccountConfig {
#[serde(default)]
pub default: bool,
pub imap: Option<ImapConfig>,
pub smtp: Option<SmtpConfig>,
#[serde(deserialize_with = "shell_expanded_string")]
pub email: String,
pub display_name: Option<String>,
pub signature: Option<String>,
pub signature_delim: Option<String>,
pub downloads_dir: Option<PathBuf>,
// pub backend: Option<Backend>,
// #[cfg(feature = "pgp")]
// pub pgp: Option<PgpConfig>,
// #[cfg(not(feature = "pgp"))]
// #[serde(default)]
// #[serde(skip_serializing, deserialize_with = "missing_pgp_feature")]
// pub pgp: Option<()>,
// pub folder: Option<FolderConfig>,
// pub envelope: Option<EnvelopeConfig>,
// pub message: Option<MessageConfig>,
// pub template: Option<TemplateConfig>,
pub imap: Option<ImapConfig>,
pub smtp: Option<SmtpConfig>,
}
/// The account configuration.
/// IMAP configuration.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ImapConfig {
pub url: Url,
#[serde(default)]
pub tls: TlsConfig,
#[serde(default)]
pub starttls: bool,
#[serde(default)]
pub sasl: SaslConfig,
#[serde(default)]
pub ext: ImapExtensionsConfig,
}
/// IMAP extensions configuration.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ImapExtensionsConfig {
#[serde(default)]
id: ImapIdExtensionConfig,
}
/// IMAP ID configuration.
///
/// https://www.rfc-editor.org/rfc/rfc2971.html
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ImapIdExtensionConfig {
/// Automatically sends the ID command straight after
/// authentication.
#[serde(default)]
send_after_auth: bool,
}
/// SMTP configuration.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct SmtpConfig {
@@ -81,44 +110,7 @@ pub struct SmtpConfig {
pub sasl: SaslConfig,
}
/// The account configuration.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ImapConfig {
pub url: Url,
#[serde(default)]
pub tls: TlsConfig,
#[serde(default)]
pub starttls: bool,
#[serde(default)]
pub sasl: SaslConfig,
/// The IMAP extensions configuration.
#[serde(default)]
pub extensions: ImapExtensionsConfig,
}
/// The IMAP configuration dedicated to extensions.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ImapExtensionsConfig {
#[serde(default)]
id: ImapIdExtensionConfig,
}
/// The IMAP configuration dedicated to the ID extension.
///
/// https://www.rfc-editor.org/rfc/rfc2971.html
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ImapIdExtensionConfig {
/// Automatically sends the ID command straight after
/// authentication.
#[serde(default)]
always_after_auth: bool,
}
/// SSL/TLS configuration.
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct TlsConfig {
@@ -128,6 +120,7 @@ pub struct TlsConfig {
pub cert: Option<PathBuf>,
}
/// SSL/TLS provider configuration.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub enum TlsProviderConfig {
@@ -135,12 +128,14 @@ pub enum TlsProviderConfig {
NativeTls,
}
/// Rustls configuration.
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct RustlsConfig {
pub crypto: Option<RustlsCryptoConfig>,
}
/// Rustls crypto provider configuration.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub enum RustlsCryptoConfig {
@@ -148,6 +143,7 @@ pub enum RustlsCryptoConfig {
Ring,
}
/// SASL configuration.
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct SaslConfig {
@@ -162,6 +158,7 @@ fn default_sasl_mechanisms() -> Vec<SaslMechanismConfig> {
vec![SaslMechanismConfig::Plain, SaslMechanismConfig::Login]
}
/// SASL mechanism configuration.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SaslMechanismConfig {
@@ -170,6 +167,31 @@ pub enum SaslMechanismConfig {
Anonymous,
}
/// SASL LOGIN configuration.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct SaslLoginConfig {
pub username: String,
pub password: SecretConfig,
}
/// SASL PLAIN configuration.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct SaslPlainConfig {
pub authzid: Option<String>,
pub authcid: String,
pub passwd: SecretConfig,
}
/// SASL ANONYMOUS configuration.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct SaslAnonymousConfig {
pub message: Option<String>,
}
/// Secret configuration.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SecretConfig {
@@ -208,27 +230,6 @@ impl SecretConfig {
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct SaslLoginConfig {
pub username: String,
pub password: SecretConfig,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct SaslPlainConfig {
pub authzid: Option<String>,
pub authcid: String,
pub passwd: SecretConfig,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct SaslAnonymousConfig {
pub message: Option<String>,
}
struct ShellExpandedStringVisitor;
impl<'de> Visitor<'de> for ShellExpandedStringVisitor {
-17
View File
@@ -1,17 +0,0 @@
use clap::Parser;
/// The envelope id argument parser.
#[derive(Debug, Parser)]
pub struct EnvelopeIdArg {
/// The envelope id.
#[arg(value_name = "ID", required = true)]
pub id: usize,
}
/// The envelopes ids arguments parser.
#[derive(Debug, Parser)]
pub struct EnvelopeIdsArgs {
/// The list of envelopes ids.
#[arg(value_name = "ID", required = true)]
pub ids: Vec<usize>,
}
-1
View File
@@ -1 +0,0 @@
pub mod ids;
-218
View File
@@ -1,218 +0,0 @@
use std::{process::exit, sync::Arc};
use ariadne::{Color, Label, Report, ReportKind, Source};
use clap::Parser;
use color_eyre::Result;
use email::{
backend::feature::BackendFeatureSource, config::Config, email::search_query,
envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery,
};
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, config::EnvelopesTable},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag,
};
/// Search and sort envelopes as a list.
///
/// This command allows you to list envelopes included in the given
/// folder, matching the given query.
#[derive(Debug, Parser)]
pub struct EnvelopeListCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
/// The page number.
///
/// The page number starts from 1 (which is the default). Giving a
/// page number to big will result in a out of bound error.
#[arg(long, short, value_name = "NUMBER", default_value = "1")]
pub page: usize,
/// The page size.
///
/// Determine the amount of envelopes a page should contain.
#[arg(long, short = 's', value_name = "NUMBER")]
pub page_size: Option<usize>,
#[command(flatten)]
pub account: AccountNameFlag,
/// 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
/// fit the width.
#[arg(long = "max-width", short = 'w')]
#[arg(name = "table_max_width", value_name = "PIXELS")]
pub table_max_width: Option<u16>,
/// The list envelopes filter and sort query.
///
/// The query can be a filter query, a sort query or both
/// together.
///
/// A filter query is composed of operators and conditions. There
/// is 3 operators and 8 conditions:
///
/// • not <condition> → filter envelopes that do not match the
/// condition
///
/// • <condition> and <condition> → filter envelopes that match
/// both conditions
///
/// • <condition> or <condition> → filter envelopes that match
/// one of the conditions
///
/// ◦ date <yyyy-mm-dd> → filter envelopes that match the given
/// date
///
/// ◦ before <yyyy-mm-dd> → filter envelopes with date strictly
/// before the given one
///
/// ◦ after <yyyy-mm-dd> → filter envelopes with date stricly
/// after the given one
///
/// ◦ from <pattern> → filter envelopes with senders matching the
/// given pattern
///
/// ◦ to <pattern> → filter envelopes with recipients matching
/// the given pattern
///
/// ◦ subject <pattern> → filter envelopes with subject matching
/// the given pattern
///
/// ◦ body <pattern> → filter envelopes with text bodies matching
/// the given pattern
///
/// ◦ flag <flag> → filter envelopes matching the given flag
///
/// A sort query starts by "order by", and is composed of kinds
/// and orders. There is 4 kinds and 2 orders:
///
/// • date [order] → sort envelopes by date
///
/// • from [order] → sort envelopes by sender
///
/// • to [order] → sort envelopes by recipient
///
/// • subject [order] → sort envelopes by subject
///
/// ◦ <kind> asc → sort envelopes by the given kind in ascending
/// order
///
/// ◦ <kind> desc → sort envelopes by the given kind in
/// descending order
///
/// Examples:
///
/// subject foo and body bar → filter envelopes containing "foo"
/// in their subject and "bar" in their text bodies
///
/// order by date desc subject → sort envelopes by descending date
/// (most recent first), then by ascending subject
///
/// subject foo and body bar order by date desc subject →
/// combination of the 2 previous examples
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
pub query: Option<Vec<String>>,
}
impl Default for EnvelopeListCommand {
fn default() -> Self {
Self {
folder: Default::default(),
page: 1,
page_size: Default::default(),
account: Default::default(),
query: Default::default(),
table_max_width: Default::default(),
}
}
}
impl EnvelopeListCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing list envelopes command");
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let toml_account_config = Arc::new(toml_account_config);
let folder = &self.folder.name;
let page = 1.max(self.page) - 1;
let page_size = self
.page_size
.unwrap_or_else(|| account_config.get_envelope_list_page_size());
let backend = BackendBuilder::new(
toml_account_config.clone(),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_list_envelopes(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let query = self
.query
.map(|query| query.join(" ").parse::<SearchEmailsQuery>());
let query = match query {
None => None,
Some(Ok(query)) => Some(query),
Some(Err(main_err)) => {
let source = "query";
let search_query::error::Error::ParseError(errs, query) = &main_err;
for err in errs {
Report::build(ReportKind::Error, source, err.span().start)
.with_message(main_err.to_string())
.with_label(
Label::new((source, err.span().into_range()))
.with_message(err.reason().to_string())
.with_color(Color::Red),
)
.finish()
.eprint((source, Source::from(&query)))
.unwrap();
}
exit(0)
}
};
let opts = ListEnvelopesOptions {
page,
page_size,
query,
};
let envelopes = backend.list_envelopes(folder, opts).await?;
let table = EnvelopesTable::from(envelopes)
.with_some_width(self.table_max_width)
.with_some_preset(toml_account_config.envelope_list_table_preset())
.with_some_unseen_char(toml_account_config.envelope_list_table_unseen_char())
.with_some_replied_char(toml_account_config.envelope_list_table_replied_char())
.with_some_flagged_char(toml_account_config.envelope_list_table_flagged_char())
.with_some_attachment_char(toml_account_config.envelope_list_table_attachment_char())
.with_some_id_color(toml_account_config.envelope_list_table_id_color())
.with_some_flags_color(toml_account_config.envelope_list_table_flags_color())
.with_some_subject_color(toml_account_config.envelope_list_table_subject_color())
.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)
}
}
-35
View File
@@ -1,35 +0,0 @@
pub mod list;
pub mod thread;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::config::TomlConfig;
use self::{list::EnvelopeListCommand, thread::EnvelopeThreadCommand};
/// 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
/// headers from the message itself. This subcommand allows you to
/// manage them.
#[derive(Debug, Subcommand)]
pub enum EnvelopeSubcommand {
#[command(alias = "lst")]
List(EnvelopeListCommand),
#[command()]
Thread(EnvelopeThreadCommand),
}
impl EnvelopeSubcommand {
#[allow(unused)]
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self {
Self::List(cmd) => cmd.execute(printer, config).await,
Self::Thread(cmd) => cmd.execute(printer, config).await,
}
}
}
-201
View File
@@ -1,201 +0,0 @@
use ariadne::{Label, Report, ReportKind, Source};
use clap::Parser;
use color_eyre::Result;
use email::{
backend::feature::BackendFeatureSource, config::Config, email::search_query,
envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery,
};
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, config::EnvelopesTree},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use std::{process::exit, sync::Arc};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag,
};
/// Search and sort envelopes as a thread.
///
/// This command allows you to thread envelopes included in the given
/// folder, matching the given query.
#[derive(Debug, Parser)]
pub struct EnvelopeThreadCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub account: AccountNameFlag,
/// Show only threads that contain the given envelope identifier.
#[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 EnvelopeThreadCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing thread envelopes command");
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 folder = &self.folder.name;
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
|builder| {
builder
.without_features()
.with_thread_envelopes(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let query = self
.query
.map(|query| query.join(" ").parse::<SearchEmailsQuery>());
let query = match query {
None => None,
Some(Ok(query)) => Some(query),
Some(Err(main_err)) => {
let source = "query";
let search_query::error::Error::ParseError(errs, query) = &main_err;
for err in errs {
Report::build(ReportKind::Error, source, err.span().start)
.with_message(main_err.to_string())
.with_label(
Label::new((source, err.span().into_range()))
.with_message(err.reason().to_string())
.with_color(ariadne::Color::Red),
)
.finish()
.eprint((source, Source::from(&query)))
.unwrap();
}
exit(0)
}
};
let opts = ListEnvelopesOptions {
page: 0,
page_size: 0,
query,
};
let envelopes = match self.id {
Some(id) => backend.thread_envelope(folder, id, opts).await,
None => backend.thread_envelopes(folder, opts).await,
}?;
let tree = EnvelopesTree::new(account_config, envelopes);
printer.out(tree)
}
}
// #[cfg(test)]
// mod test {
// use email::{account::config::AccountConfig, envelope::ThreadedEnvelope};
// use petgraph::graphmap::DiGraphMap;
// use super::write_tree;
// macro_rules! e {
// ($id:literal) => {
// ThreadedEnvelope {
// id: $id,
// message_id: $id,
// from: "",
// subject: "",
// date: Default::default(),
// }
// };
// }
// #[test]
// fn tree_1() {
// let config = AccountConfig::default();
// let mut buf = Vec::new();
// let mut graph = DiGraphMap::new();
// graph.add_edge(e!("0"), e!("1"), 0);
// graph.add_edge(e!("0"), e!("2"), 0);
// graph.add_edge(e!("0"), e!("3"), 0);
// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap();
// let buf = String::from_utf8_lossy(&buf);
// let expected = "
// 0
// ├─ 1
// ├─ 2
// └─ 3
// ";
// assert_eq!(expected.trim_start(), buf)
// }
// #[test]
// fn tree_2() {
// let config = AccountConfig::default();
// let mut buf = Vec::new();
// let mut graph = DiGraphMap::new();
// graph.add_edge(e!("0"), e!("1"), 0);
// graph.add_edge(e!("1"), e!("2"), 1);
// graph.add_edge(e!("1"), e!("3"), 1);
// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap();
// let buf = String::from_utf8_lossy(&buf);
// let expected = "
// 0
// └─ 1
// ├─ 2
// └─ 3
// ";
// assert_eq!(expected.trim_start(), buf)
// }
// #[test]
// fn tree_3() {
// let config = AccountConfig::default();
// let mut buf = Vec::new();
// let mut graph = DiGraphMap::new();
// graph.add_edge(e!("0"), e!("1"), 0);
// graph.add_edge(e!("1"), e!("2"), 1);
// graph.add_edge(e!("2"), e!("22"), 2);
// graph.add_edge(e!("1"), e!("3"), 1);
// graph.add_edge(e!("0"), e!("4"), 0);
// graph.add_edge(e!("4"), e!("5"), 1);
// graph.add_edge(e!("5"), e!("6"), 2);
// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap();
// let buf = String::from_utf8_lossy(&buf);
// let expected = "
// 0
// ├─ 1
// │ ├─ 2
// │ │ └─ 22
// │ └─ 3
// └─ 4
// └─ 5
// └─ 6
// ";
// assert_eq!(expected.trim_start(), buf)
// }
// }
@@ -1,48 +0,0 @@
use clap::Parser;
use email::flag::{Flag, Flags};
use tracing::debug;
/// The ids and/or flags arguments parser.
#[derive(Debug, Parser)]
pub struct IdsAndFlagsArgs {
/// The list of ids and/or flags.
///
/// Every argument that can be parsed as an integer is considered
/// an id, otherwise it is considered as a flag.
#[arg(value_name = "ID-OR-FLAG", required = true)]
pub ids_and_flags: Vec<IdOrFlag>,
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum IdOrFlag {
Id(usize),
Flag(Flag),
}
impl From<&str> for IdOrFlag {
fn from(value: &str) -> Self {
value.parse::<usize>().map(Self::Id).unwrap_or_else(|err| {
let flag = Flag::from(value);
debug!("cannot parse {value} as usize, parsing it as flag {flag}");
debug!("{err:?}");
Self::Flag(flag)
})
}
}
pub fn into_tuple(ids_and_flags: &[IdOrFlag]) -> (Vec<usize>, Flags) {
ids_and_flags.iter().fold(
(Vec::default(), Flags::default()),
|(mut ids, mut flags), arg| {
match arg {
IdOrFlag::Id(id) => {
ids.push(*id);
}
IdOrFlag::Flag(flag) => {
flags.insert(flag.to_owned());
}
};
(ids, flags)
},
)
}
-1
View File
@@ -1 +0,0 @@
pub mod ids_and_flags;
-64
View File
@@ -1,64 +0,0 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag,
config::TomlConfig,
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
folder::arg::name::FolderNameOptionalFlag,
};
/// Add flag(s) to the given envelope.
///
/// This command allows you to attach the given flag(s) to the given
/// envelope(s).
#[derive(Debug, Parser)]
pub struct FlagAddCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub args: IdsAndFlagsArgs,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl FlagAddCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing add flag(s) command");
let folder = &self.folder.name;
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_add_flags(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.add_flags(folder, &ids, &flags).await?;
printer.out(format!("Flag(s) {flags} successfully added!\n"))
}
}
-42
View File
@@ -1,42 +0,0 @@
mod add;
mod remove;
mod set;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::config::TomlConfig;
use self::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand};
/// 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.
#[derive(Debug, Subcommand)]
pub enum FlagSubcommand {
#[command(arg_required_else_help = true)]
#[command(alias = "create")]
Add(FlagAddCommand),
#[command(arg_required_else_help = true)]
#[command(aliases = ["update", "change", "replace"])]
Set(FlagSetCommand),
#[command(arg_required_else_help = true)]
#[command(aliases = ["rm", "delete", "del"])]
Remove(FlagRemoveCommand),
}
impl FlagSubcommand {
#[allow(unused)]
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self {
Self::Add(cmd) => cmd.execute(printer, config).await,
Self::Set(cmd) => cmd.execute(printer, config).await,
Self::Remove(cmd) => cmd.execute(printer, config).await,
}
}
}
-64
View File
@@ -1,64 +0,0 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag,
config::TomlConfig,
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
folder::arg::name::FolderNameOptionalFlag,
};
/// Remove flag(s) from a given envelope.
///
/// This command allows you to remove the given flag(s) from the given
/// envelope(s).
#[derive(Debug, Parser)]
pub struct FlagRemoveCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub args: IdsAndFlagsArgs,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl FlagRemoveCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing remove flag(s) command");
let folder = &self.folder.name;
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_remove_flags(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.remove_flags(folder, &ids, &flags).await?;
printer.out(format!("Flag(s) {flags} successfully removed!\n"))
}
}
-64
View File
@@ -1,64 +0,0 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag,
config::TomlConfig,
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
folder::arg::name::FolderNameOptionalFlag,
};
/// 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).
#[derive(Debug, Parser)]
pub struct FlagSetCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub args: IdsAndFlagsArgs,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl FlagSetCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing set flag(s) command");
let folder = &self.folder.name;
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_set_flags(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.set_flags(folder, &ids, &flags).await?;
printer.out(format!("Flag(s) {flags} successfully replaced!\n"))
}
}
-2
View File
@@ -1,2 +0,0 @@
pub mod arg;
pub mod command;
-3
View File
@@ -1,3 +0,0 @@
pub mod arg;
pub mod command;
pub mod flag;
-25
View File
@@ -1,25 +0,0 @@
use clap::Parser;
use std::ops::Deref;
/// The raw message body argument parser.
#[derive(Debug, Parser)]
pub struct MessageRawBodyArg {
/// Prefill the template with a custom body.
#[arg(trailing_var_arg = true)]
#[arg(name = "body_raw", value_name = "BODY")]
pub raw: Vec<String>,
}
impl MessageRawBodyArg {
pub fn raw(self) -> String {
self.raw.join(" ").replace('\r', "").replace('\n', "\r\n")
}
}
impl Deref for MessageRawBodyArg {
type Target = Vec<String>;
fn deref(&self) -> &Self::Target {
&self.raw
}
}
-20
View File
@@ -1,20 +0,0 @@
use clap::Parser;
/// The envelope id argument parser.
#[derive(Debug, Parser)]
pub struct HeaderRawArgs {
/// Prefill the template with custom headers.
///
/// A raw header should follow the pattern KEY:VAL.
#[arg(long = "header", short = 'H', required = false)]
#[arg(name = "header-raw", value_name = "KEY:VAL", value_parser = raw_header_parser)]
pub raw: Vec<(String, String)>,
}
pub fn raw_header_parser(raw_header: &str) -> Result<(String, String), String> {
if let Some((key, val)) = raw_header.split_once(':') {
Ok((key.trim().to_owned(), val.trim().to_owned()))
} else {
Err(format!("cannot parse raw header {raw_header:?}"))
}
}
-20
View File
@@ -1,20 +0,0 @@
use clap::Parser;
pub mod body;
pub mod header;
pub mod reply;
/// The raw message argument parser.
#[derive(Debug, Parser)]
pub struct MessageRawArg {
/// The raw message, including headers and body.
#[arg(trailing_var_arg = true)]
#[arg(name = "message_raw", value_name = "MESSAGE")]
pub raw: Vec<String>,
}
impl MessageRawArg {
pub fn raw(self) -> String {
self.raw.join(" ").replace('\r', "").replace('\n', "\r\n")
}
}
-12
View File
@@ -1,12 +0,0 @@
use clap::Parser;
/// The reply to all argument parser.
#[derive(Debug, Parser)]
pub struct MessageReplyAllArg {
/// Reply to all recipients.
///
/// This argument will add all recipients for the To and Cc
/// headers.
#[arg(long, short = 'A')]
pub all: bool,
}
@@ -1,113 +0,0 @@
use clap::Parser;
use color_eyre::{eyre::Context, Result};
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use std::{fs, path::PathBuf, sync::Arc};
use tracing::info;
use uuid::Uuid;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, dir_parser,
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag,
};
/// 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.
#[derive(Debug, Parser)]
pub struct AttachmentDownloadCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[command(flatten)]
pub account: AccountNameFlag,
/// Override the download directory.
///
/// If omitted, it uses the downloads directory from the config,
/// or `XDG_DOWNLOAD_DIR`.
#[arg(short = 'd', long, value_name = "PATH", value_parser = dir_parser)]
pub downloads_dir: Option<PathBuf>,
}
impl AttachmentDownloadCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing download attachment(s) command");
let folder = &self.folder.name;
let ids = &self.envelopes.ids;
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 emails = backend.get_messages(folder, ids).await?;
let mut emails_count = 0;
let mut attachments_count = 0;
let mut ids = ids.iter();
for email in emails.to_vec() {
let id = ids.next().unwrap();
let attachments = email.attachments()?;
if attachments.is_empty() {
printer.log(format!("No attachment found for message {id}!\n"))?;
continue;
} else {
emails_count += 1;
}
printer.log(format!(
"{} attachment(s) found for message {id}!\n",
attachments.len()
))?;
for attachment in attachments {
let filename: PathBuf = attachment
.filename
.unwrap_or_else(|| Uuid::new_v4().to_string())
.into();
let filepath = account_config
.get_download_file_path(self.downloads_dir.as_deref(), &filename)?;
printer.log(format!("Downloading {:?}\n", filepath))?;
fs::write(&filepath, &attachment.body)
.with_context(|| format!("cannot save attachment at {filepath:?}"))?;
attachments_count += 1;
}
}
match attachments_count {
0 => printer.out("No attachment found!\n"),
1 => printer.out("Downloaded 1 attachment!\n"),
n => printer.out(format!(
"Downloaded {} attachment(s) from {} messages(s)!\n",
n, emails_count,
)),
}
}
}
@@ -1,28 +0,0 @@
mod download;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::config::TomlConfig;
use self::download::AttachmentDownloadCommand;
/// 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, alias = "dl")]
Download(AttachmentDownloadCommand),
}
impl AttachmentSubcommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self {
Self::Download(cmd) => cmd.execute(printer, config).await,
}
}
}
-1
View File
@@ -1 +0,0 @@
pub mod command;
-69
View File
@@ -1,69 +0,0 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
};
/// Copy the message associated to the given envelope id(s) to the
/// given target folder.
#[derive(Debug, Parser)]
pub struct MessageCopyCommand {
#[command(flatten)]
pub source_folder: SourceFolderNameOptionalFlag,
#[command(flatten)]
pub target_folder: TargetFolderNameArg,
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageCopyCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing copy message(s) command");
let source = &self.source_folder.name;
let target = &self.target_folder.name;
let ids = &self.envelopes.ids;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_copy_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.copy_messages(source, target, ids).await?;
printer.out(format!(
"Message(s) successfully copied from {source} to {target}!\n"
))
}
}
-65
View File
@@ -1,65 +0,0 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::FolderNameOptionalFlag,
};
/// 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
/// its envelope, otherwise it moves it to the trash folder. Only the
/// expunge folder command truly deletes messages.
#[derive(Debug, Parser)]
pub struct MessageDeleteCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageDeleteCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing delete message(s) command");
let folder = &self.folder.name;
let ids = &self.envelopes.ids;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_delete_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.delete_messages(folder, ids).await?;
printer.out(format!("Message(s) successfully removed from {folder}!\n"))
}
}
-103
View File
@@ -1,103 +0,0 @@
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
@@ -1,155 +0,0 @@
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(())
}
}
-84
View File
@@ -1,84 +0,0 @@
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,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
};
/// 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
/// edition process finishes, you can choose between saving or sending
/// the final message.
#[derive(Debug, Parser)]
pub struct MessageForwardCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelope: EnvelopeIdArg,
#[command(flatten)]
pub headers: HeaderRawArgs,
#[command(flatten)]
pub body: MessageRawBodyArg,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageForwardCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing forward 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)
},
)
.build()
.await?;
let id = self.envelope.id;
let tpl = backend
.get_messages(folder, &[id])
.await?
.first()
.ok_or(eyre!("cannot find message"))?
.to_forward_tpl_builder(account_config.clone())
.with_headers(self.headers.raw)
.with_body(self.body.raw())
.build()
.await?;
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await
}
}
-98
View File
@@ -1,98 +0,0 @@
use std::sync::Arc;
use clap::Parser;
use color_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 url::Url;
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
/// 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
/// $EDITOR. When the edition process finishes, you can choose between
/// saving or sending the final message.
#[derive(Debug, Parser)]
pub struct MessageMailtoCommand {
/// The mailto url.
#[arg()]
pub url: Url,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageMailtoCommand {
pub fn new(url: &str) -> Result<Self> {
Ok(Self {
url: Url::parse(url)?,
account: Default::default(),
})
}
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing mailto message command");
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)
},
)
.without_sending_backend()
.build()
.await?;
let mut msg = Vec::<u8>::new();
let mut body = Vec::<u8>::new();
msg.extend(b"Content-Type: text/plain; charset=utf-8\r\n");
for (key, val) in self.url.query_pairs() {
if key.eq_ignore_ascii_case("body") {
body.extend(val.as_bytes());
} else {
msg.extend(key.as_bytes());
msg.extend(b": ");
msg.extend(val.as_bytes());
msg.extend(b"\r\n");
}
}
msg.extend(b"\r\n");
msg.extend(body);
if let Some(sig) = account_config.find_full_signature() {
msg.extend(b"\r\n");
msg.extend(sig.as_bytes());
}
let tpl = account_config
.generate_tpl_interpreter()
.with_show_only_headers(account_config.get_message_write_headers())
.build()
.from_bytes(msg)
.await?
.into();
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await
}
}
-94
View File
@@ -1,94 +0,0 @@
pub mod copy;
pub mod delete;
pub mod edit;
pub mod export;
pub mod forward;
pub mod mailto;
pub mod r#move;
pub mod read;
pub mod reply;
pub mod save;
pub mod send;
pub mod thread;
pub mod write;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::config::TomlConfig;
use self::{
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,
};
/// 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
/// bottom of the message). Both are separated by two new lines. This
/// subcommand allows you to manage them.
#[derive(Debug, Subcommand)]
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),
Reply(MessageReplyCommand),
#[command(aliases = ["fwd", "fd"])]
Forward(MessageForwardCommand),
Edit(MessageEditCommand),
Mailto(MessageMailtoCommand),
Save(MessageSaveCommand),
Send(MessageSendCommand),
#[command(arg_required_else_help = true)]
#[command(aliases = ["cpy", "cp"])]
Copy(MessageCopyCommand),
#[command(arg_required_else_help = true)]
#[command(alias = "mv")]
Move(MessageMoveCommand),
#[command(arg_required_else_help = true)]
#[command(aliases = ["remove", "rm"])]
Delete(MessageDeleteCommand),
}
impl MessageSubcommand {
#[allow(unused)]
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,
Self::Copy(cmd) => cmd.execute(printer, config).await,
Self::Move(cmd) => cmd.execute(printer, config).await,
Self::Delete(cmd) => cmd.execute(printer, config).await,
}
}
}
-70
View File
@@ -1,70 +0,0 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
};
/// Move the message associated to the given envelope id(s) to the
/// given target folder.
#[derive(Debug, Parser)]
pub struct MessageMoveCommand {
#[command(flatten)]
pub source_folder: SourceFolderNameOptionalFlag,
#[command(flatten)]
pub target_folder: TargetFolderNameArg,
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageMoveCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing move message(s) command");
let source = &self.source_folder.name;
let target = &self.target_folder.name;
let ids = &self.envelopes.ids;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_move_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.move_messages(source, target, ids).await?;
printer.out(format!(
"Message(s) successfully moved from {source} to {target}!\n"
))
}
}
-117
View File
@@ -1,117 +0,0 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::FolderNameOptionalFlag,
};
/// 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.
#[derive(Debug, Parser)]
pub struct MessageReadCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
/// Read the message without applying the "seen" flag to its
/// corresponding envelope.
#[arg(long, short)]
pub preview: bool,
/// Read only the body of the message.
///
/// All headers will be removed from the message.
#[arg(long)]
#[arg(conflicts_with = "headers")]
pub no_headers: bool,
/// List of headers that should be visible at the top of the
/// message.
///
/// If a given header is not found in the message, it will not be
/// visible. If no header is given, defaults to the one set up in
/// your TOML configuration file.
#[arg(long = "header", short = 'H', value_name = "NAME")]
#[arg(conflicts_with = "no_headers")]
pub headers: Vec<String>,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageReadCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing read message(s) command");
let folder = &self.folder.name;
let ids = &self.envelopes.ids;
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)
.with_peek_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let emails = if self.preview {
backend.peek_messages(folder, ids).await
} else {
backend.get_messages(folder, ids).await
}?;
let mut glue = "";
let mut bodies = String::default();
for email in emails.to_vec() {
bodies.push_str(glue);
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);
}
tpl
})
.await?;
bodies.push_str(&tpl);
glue = "\n\n";
}
printer.out(bodies)
}
}
-92
View File
@@ -1,92 +0,0 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::{eyre::eyre, Result};
use email::{backend::feature::BackendFeatureSource, config::Config, flag::Flag};
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,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg},
};
/// 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
/// edition process finishes, you can choose between saving or sending
/// the final message.
#[derive(Debug, Parser)]
pub struct MessageReplyCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelope: EnvelopeIdArg,
#[command(flatten)]
pub reply: MessageReplyAllArg,
#[command(flatten)]
pub headers: HeaderRawArgs,
#[command(flatten)]
pub body: MessageRawBodyArg,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageReplyCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing reply 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)
},
)
.build()
.await?;
let id = self.envelope.id;
let tpl = backend
.get_messages(folder, &[id])
.await?
.first()
.ok_or(eyre!("cannot find message {id}"))?
.to_reply_tpl_builder(account_config.clone())
.with_headers(self.headers.raw)
.with_body(self.body.raw())
.with_reply_all(self.reply.all)
.build()
.await?;
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await?;
backend.add_flag(folder, &[id], Flag::Answered).await?;
Ok(())
}
}
-101
View File
@@ -1,101 +0,0 @@
use std::{
fmt,
io::{self, BufRead, IsTerminal},
sync::Arc,
};
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config, envelope::SingleId};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use serde::{ser::SerializeStruct, Serialize, Serializer};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg,
};
/// 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)]
pub struct MessageSaveCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub message: MessageRawArg,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageSaveCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing save 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 backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let is_tty = io::stdin().is_terminal();
let is_json = printer.is_json();
let msg = if !self.message.raw.is_empty() || is_tty || is_json {
self.message.raw()
} else {
io::stdin()
.lock()
.lines()
.map_while(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
let id = backend.add_message(folder, msg.as_bytes()).await?;
printer.out(MessageAdded { folder, id })
}
}
struct MessageAdded<'a> {
folder: &'a String,
id: SingleId,
}
impl fmt::Display for MessageAdded<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let id = self.id.as_str();
let folder = self.folder;
writeln!(f, "Message {id} successfully saved to {folder}")
}
}
impl Serialize for MessageAdded<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = serializer.serialize_struct("MessageAdded", 2)?;
state.serialize_field("folder", self.folder)?;
state.serialize_field("id", self.id.as_str())?;
state.end()
}
}
-67
View File
@@ -1,67 +0,0 @@
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use std::{
io::{self, BufRead, IsTerminal},
sync::Arc,
};
use tracing::info;
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig, message::arg::MessageRawArg};
/// Send the given raw message.
///
/// This command allows you to send a raw message and to save a copy
/// to your send folder.
#[derive(Debug, Parser)]
pub struct MessageSendCommand {
#[command(flatten)]
pub message: MessageRawArg,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageSendCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing send message command");
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
},
)
.build()
.await?;
let msg = if io::stdin().is_terminal() {
self.message.raw()
} else {
io::stdin()
.lock()
.lines()
.map_while(Result::ok)
.collect::<Vec<_>>()
.join("\r\n")
};
backend.send_message_then_save_copy(msg.as_bytes()).await?;
printer.out("Message successfully sent!")
}
}
-133
View File
@@ -1,133 +0,0 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::envelope::arg::ids::EnvelopeIdArg;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::FolderNameOptionalFlag,
};
/// 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
/// envelope. To prevent this behaviour, use the --preview flag.
#[derive(Debug, Parser)]
pub struct MessageThreadCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelope: EnvelopeIdArg,
/// Thread the message without applying the "seen" flag to its
/// corresponding envelope.
#[arg(long, short)]
pub preview: bool,
/// Thread only the body of the message.
///
/// All headers will be removed from the message.
#[arg(long)]
#[arg(conflicts_with = "headers")]
pub no_headers: bool,
/// List of headers that should be visible at the top of the
/// message.
///
/// If a given header is not found in the message, it will not be
/// visible. If no header is given, defaults to the one set up in
/// your TOML configuration file.
#[arg(long = "header", short = 'H', value_name = "NAME")]
#[arg(conflicts_with = "no_headers")]
pub headers: Vec<String>,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageThreadCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing thread message(s) 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)
.with_peek_messages(BackendFeatureSource::Context)
.with_thread_envelopes(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let envelopes = backend
.thread_envelope(folder, *id, Default::default())
.await?;
let ids: Vec<_> = envelopes
.graph()
.nodes()
.filter_map(|e| {
let id = e.id.parse::<usize>().unwrap_or(0);
if id > 0 { Some(id) } else { None }
})
.collect();
let emails = if self.preview {
backend.peek_messages(folder, &ids).await
} else {
backend.get_messages(folder, &ids).await
}?;
let mut glue = "";
let mut bodies = String::default();
for (i, email) in emails.to_vec().iter().enumerate() {
bodies.push_str(glue);
bodies.push_str(&format!("-------- Message {} --------\n\n", ids[i]));
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);
}
tpl
})
.await?;
bodies.push_str(&tpl);
glue = "\n\n";
}
printer.out(bodies)
}
}
-72
View File
@@ -1,72 +0,0 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{
config::Config,
{backend::feature::BackendFeatureSource, message::Message},
};
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,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
};
/// 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
/// process finishes, you can choose between saving or sending the
/// final message.
#[derive(Debug, Parser)]
pub struct MessageWriteCommand {
#[command(flatten)]
pub headers: HeaderRawArgs,
#[command(flatten)]
pub body: MessageRawBodyArg,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageWriteCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing write message command");
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)
},
)
.build()
.await?;
let tpl = Message::new_tpl_builder(account_config.clone())
.with_headers(self.headers.raw)
.with_body(self.body.raw())
.build()
.await?;
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await
}
}
-4
View File
@@ -1,4 +0,0 @@
pub mod arg;
pub mod attachment;
pub mod command;
pub mod template;
-25
View File
@@ -1,25 +0,0 @@
use clap::Parser;
use std::ops::Deref;
/// The raw template body argument parser.
#[derive(Debug, Parser)]
pub struct TemplateRawBodyArg {
/// Prefill the template with a custom MML body.
#[arg(trailing_var_arg = true)]
#[arg(name = "body_raw", value_name = "BODY")]
pub raw: Vec<String>,
}
impl TemplateRawBodyArg {
pub fn raw(self) -> String {
self.raw.join(" ").replace('\r', "")
}
}
impl Deref for TemplateRawBodyArg {
type Target = Vec<String>;
fn deref(&self) -> &Self::Target {
&self.raw
}
}
-18
View File
@@ -1,18 +0,0 @@
pub mod body;
use clap::Parser;
/// The raw template argument parser.
#[derive(Debug, Parser)]
pub struct TemplateRawArg {
/// The raw template, including headers and MML body.
#[arg(trailing_var_arg = true)]
#[arg(name = "template_raw", value_name = "TEMPLATE")]
pub raw: Vec<String>,
}
impl TemplateRawArg {
pub fn raw(self) -> String {
self.raw.join(" ").replace('\r', "")
}
}
@@ -1,84 +0,0 @@
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,
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,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
};
/// Generate a template for forwarding a message.
///
/// The generated template is prefilled with your email in a From
/// header as well as your signature. The forwarded message is also
/// prefilled in the body of the template, prefixed by a separator.
#[derive(Debug, Parser)]
pub struct TemplateForwardCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelope: EnvelopeIdArg,
#[command(flatten)]
pub headers: HeaderRawArgs,
#[command(flatten)]
pub body: MessageRawBodyArg,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl TemplateForwardCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing forward template 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_get_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let id = self.envelope.id;
let tpl = backend
.get_messages(folder, &[id])
.await?
.first()
.ok_or(eyre!("cannot find message {id}"))?
.to_forward_tpl_builder(account_config)
.with_headers(self.headers.raw)
.with_body(self.body.raw())
.build()
.await?;
printer.out(tpl)
}
}
-55
View File
@@ -1,55 +0,0 @@
mod forward;
mod reply;
mod save;
mod send;
mod write;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::config::TomlConfig;
use self::{
forward::TemplateForwardCommand, reply::TemplateReplyCommand, save::TemplateSaveCommand,
send::TemplateSendCommand, write::TemplateWriteCommand,
};
/// 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.
///
/// Learn more about MML at: <https://crates.io/crates/mml-lib>.
#[derive(Debug, Subcommand)]
pub enum TemplateSubcommand {
#[command(aliases = ["add", "create", "new", "compose"])]
Write(TemplateWriteCommand),
#[command(arg_required_else_help = true)]
Reply(TemplateReplyCommand),
#[command(arg_required_else_help = true)]
#[command(alias = "fwd")]
Forward(TemplateForwardCommand),
#[command()]
Save(TemplateSaveCommand),
#[command()]
Send(TemplateSendCommand),
}
impl TemplateSubcommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self {
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::Save(cmd) => cmd.execute(printer, config).await,
Self::Send(cmd) => cmd.execute(printer, config).await,
}
}
}
@@ -1,89 +0,0 @@
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,
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,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg},
};
/// Generate a template for replying to a message.
///
/// The generated template is prefilled with your email in a From
/// header as well as your signature. The replied message is also
/// prefilled in the body of the template, with all lines prefixed by
/// the symbol greater than ">".
#[derive(Debug, Parser)]
pub struct TemplateReplyCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelope: EnvelopeIdArg,
#[command(flatten)]
pub reply: MessageReplyAllArg,
#[command(flatten)]
pub headers: HeaderRawArgs,
#[command(flatten)]
pub body: MessageRawBodyArg,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl TemplateReplyCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing reply template 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 tpl = backend
.get_messages(folder, &[id])
.await?
.first()
.ok_or(eyre!("cannot find message {id}"))?
.to_reply_tpl_builder(account_config)
.with_headers(self.headers.raw)
.with_body(self.body.raw())
.with_reply_all(self.reply.all)
.build()
.await?;
printer.out(tpl)
}
}
-115
View File
@@ -1,115 +0,0 @@
use std::{
fmt,
io::{self, BufRead, IsTerminal},
sync::Arc,
};
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config, envelope::SingleId};
use mml::MmlCompilerBuilder;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use serde::{ser::SerializeStruct, Serialize, Serializer};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg,
folder::arg::name::FolderNameOptionalFlag,
};
/// Save a template to a folder.
///
/// This command allows you to save a template to the given
/// folder. The template is compiled into a MIME message before being
/// saved to the folder. If you want to save a raw message, use the
/// message save command instead.
#[derive(Debug, Parser)]
pub struct TemplateSaveCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub template: TemplateRawArg,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl TemplateSaveCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing save template 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)
},
)
.without_sending_backend()
.build()
.await?;
let is_tty = io::stdin().is_terminal();
let is_json = printer.is_json();
let tpl = if !self.template.raw.is_empty() || is_tty || is_json {
self.template.raw()
} else {
io::stdin()
.lock()
.lines()
.map_while(Result::ok)
.collect::<Vec<String>>()
.join("\n")
};
#[allow(unused_mut)]
let mut compiler = MmlCompilerBuilder::new();
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
compiler.set_some_pgp(account_config.pgp.clone());
let msg = compiler.build(tpl.as_str())?.compile().await?.into_vec()?;
let id = backend.add_message(folder, &msg).await?;
printer.out(TemplateAdded { folder, id })
}
}
struct TemplateAdded<'a> {
folder: &'a String,
id: SingleId,
}
impl fmt::Display for TemplateAdded<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let id = self.id.as_str();
let folder = self.folder;
writeln!(f, "Template {id} successfully saved to {folder}")
}
}
impl Serialize for TemplateAdded<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = serializer.serialize_struct("TemplateAdded", 2)?;
state.serialize_field("folder", self.folder)?;
state.serialize_field("id", self.id.as_str())?;
state.end()
}
}
@@ -1,83 +0,0 @@
use std::{
io::{self, BufRead, IsTerminal},
sync::Arc,
};
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use mml::MmlCompilerBuilder;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg,
};
/// Send a template.
///
/// This command allows you to send a template and save a copy to the
/// sent folder. The template is compiled into a MIME message before
/// being sent. If you want to send a raw message, use the message
/// send command instead.
#[derive(Debug, Parser)]
pub struct TemplateSendCommand {
#[command(flatten)]
pub template: TemplateRawArg,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl TemplateSendCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing send template command");
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)
},
)
.build()
.await?;
let tpl = if io::stdin().is_terminal() {
self.template.raw()
} else {
io::stdin()
.lock()
.lines()
.map_while(Result::ok)
.collect::<Vec<_>>()
.join("\n")
};
#[allow(unused_mut)]
let mut compiler = MmlCompilerBuilder::new();
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
compiler.set_some_pgp(account_config.pgp.clone());
let msg = compiler.build(tpl.as_str())?.compile().await?.into_vec()?;
backend.send_message_then_save_copy(&msg).await?;
printer.out("Message successfully sent!")
}
}
@@ -1,48 +0,0 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{config::Config, message::Message};
use pimalaya_tui::terminal::{cli::printer::Printer, config::TomlConfig as _};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig,
email::template::arg::body::TemplateRawBodyArg, message::arg::header::HeaderRawArgs,
};
/// Generate a template for writing a new message from scratch.
///
/// The generated template is prefilled with your email in a From
/// header as well as your signature.
#[derive(Debug, Parser)]
pub struct TemplateWriteCommand {
#[command(flatten)]
pub headers: HeaderRawArgs,
#[command(flatten)]
pub body: TemplateRawBodyArg,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl TemplateWriteCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing write template command");
let (_, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let tpl = Message::new_tpl_builder(Arc::new(account_config))
.with_headers(self.headers.raw)
.with_body(self.body.raw())
.build()
.await?;
printer.out(tpl)
}
}
-2
View File
@@ -1,2 +0,0 @@
pub mod arg;
pub mod command;
-8
View File
@@ -1,8 +0,0 @@
pub mod envelope;
pub mod message;
#[doc(inline)]
pub use self::{
envelope::flag,
message::{attachment, template},
};
+6 -7
View File
@@ -19,7 +19,6 @@ use crate::{
#[command(rename_all = "lowercase")]
pub enum ImapCommand {
#[command(subcommand)]
#[command(aliases = ["envelope", "env"])]
Envelopes(EnvelopeCommand),
#[command(subcommand)]
Flags(FlagCommand),
@@ -27,17 +26,17 @@ pub enum ImapCommand {
#[command(aliases = ["mboxes", "mbox"])]
Mailboxes(MailboxCommand),
#[command(subcommand)]
#[command(aliases = ["message", "msg"])]
#[command(aliases = ["msgs", "msg"])]
Messages(MessageCommand),
}
impl ImapCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
match self {
Self::Envelopes(cmd) => cmd.execute(printer, config),
Self::Flags(cmd) => cmd.execute(printer, config),
Self::Mailboxes(cmd) => cmd.execute(printer, config),
Self::Messages(cmd) => cmd.execute(printer, config),
Self::Envelopes(cmd) => cmd.exec(printer, config),
Self::Flags(cmd) => cmd.exec(printer, config),
Self::Mailboxes(cmd) => cmd.exec(printer, config),
Self::Messages(cmd) => cmd.exec(printer, config),
}
}
}
@@ -1,16 +1,10 @@
pub mod get;
pub mod list;
pub mod search;
pub mod sort;
pub mod thread;
use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::{
config::ImapConfig,
imap::envelope::command::{
imap::envelope::{
get::GetEnvelopeCommand, list::ListEnvelopesCommand, search::SearchEnvelopesCommand,
sort::SortEnvelopesCommand, thread::ThreadEnvelopesCommand,
},
@@ -31,13 +25,13 @@ pub enum EnvelopeCommand {
}
impl EnvelopeCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
match self {
Self::List(cmd) => cmd.execute(printer, config),
Self::Get(cmd) => cmd.execute(printer, config),
Self::Search(cmd) => cmd.execute(printer, config),
Self::Sort(cmd) => cmd.execute(printer, config),
Self::Thread(cmd) => cmd.execute(printer, config),
Self::List(cmd) => cmd.exec(printer, config),
Self::Get(cmd) => cmd.exec(printer, config),
Self::Search(cmd) => cmd.exec(printer, config),
Self::Sort(cmd) => cmd.exec(printer, config),
Self::Thread(cmd) => cmd.exec(printer, config),
}
}
}
@@ -17,8 +17,8 @@ use serde::{Serialize, Serializer};
use crate::{
config::ImapConfig,
imap::{
envelope::command::list::{decode_mime, format_addresses},
mailbox::arg::name::MailboxNameOptionalFlag,
envelope::list::{decode_mime, format_addresses},
mailbox::arg::MailboxNameOptionalFlag,
stream,
},
};
@@ -43,7 +43,7 @@ pub struct GetEnvelopeCommand {
}
impl GetEnvelopeCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -63,9 +63,8 @@ impl GetEnvelopeCommand {
// FETCH envelope
let id = NonZeroU32::new(self.id).ok_or_else(|| anyhow::anyhow!("ID must be non-zero"))?;
let item_names = MacroOrMessageDataItemNames::MessageDataItemNames(vec![
MessageDataItemName::Envelope,
]);
let item_names =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::Envelope]);
let mut arg = None;
let mut coroutine = ImapFetchFirst::new(context, id, item_names, !self.seq);
@@ -18,7 +18,7 @@ use pimalaya_toolbox::terminal::printer::Printer;
use rfc2047_decoder::{Decoder, RecoverStrategy};
use serde::{Serialize, Serializer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalArg, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameOptionalArg, imap::stream};
/// Decode RFC 2047 MIME-encoded string, falling back to original on error.
pub fn decode_mime(s: &str) -> String {
@@ -52,7 +52,7 @@ pub struct ListEnvelopesCommand {
}
impl ListEnvelopesCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
+5
View File
@@ -1 +1,6 @@
pub mod command;
pub mod get;
pub mod list;
pub mod search;
pub mod sort;
pub mod thread;
@@ -15,7 +15,7 @@ use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::Printer;
use serde::{Serialize, Serializer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalArg, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameOptionalArg, imap::stream};
/// Search messages by criteria.
///
@@ -57,7 +57,7 @@ pub struct SearchEnvelopesCommand {
}
impl SearchEnvelopesCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -16,9 +16,7 @@ use serde::{Serialize, Serializer};
use crate::{
config::ImapConfig,
imap::{
envelope::command::search::parse_query, mailbox::arg::name::MailboxNameOptionalArg, stream,
},
imap::{envelope::search::parse_query, mailbox::arg::MailboxNameOptionalArg, stream},
};
/// Sort messages by criteria.
@@ -58,7 +56,7 @@ pub struct SortEnvelopesCommand {
}
impl SortEnvelopesCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -17,8 +17,8 @@ use serde::{Serialize, Serializer};
use crate::{
config::ImapConfig,
imap::{
envelope::command::{list::decode_mime, search::parse_query},
mailbox::arg::name::MailboxNameOptionalArg,
envelope::{list::decode_mime, search::parse_query},
mailbox::arg::MailboxNameOptionalArg,
stream,
},
};
@@ -50,7 +50,7 @@ pub struct ThreadEnvelopesCommand {
}
impl ThreadEnvelopesCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -80,7 +80,9 @@ impl ThreadEnvelopesCommand {
let (context, threads) = loop {
match coroutine.resume(arg.take()) {
ImapThreadResult::Io { io } => arg = Some(handle(&mut stream, io)?),
ImapThreadResult::Ok { context, threads, .. } => break (context, threads),
ImapThreadResult::Ok {
context, threads, ..
} => break (context, threads),
ImapThreadResult::Err { err, .. } => bail!(err),
}
};
@@ -106,9 +108,7 @@ fn parse_algorithm(s: &str) -> Result<ThreadingAlgorithm<'static>> {
match s.to_lowercase().as_str() {
"references" => Ok(ThreadingAlgorithm::References),
"orderedsubject" => Ok(ThreadingAlgorithm::OrderedSubject),
_ => bail!(
"Unknown threading algorithm: {s}. Valid options: references, orderedsubject"
),
_ => bail!("Unknown threading algorithm: {s}. Valid options: references, orderedsubject"),
}
}
@@ -10,7 +10,7 @@ use io_imap::{
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameOptionalFlag, imap::stream};
/// Add flags to messages.
///
@@ -35,7 +35,7 @@ pub struct AddFlagsCommand {
}
impl AddFlagsCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -1,15 +1,10 @@
pub mod add;
pub mod list;
pub mod remove;
pub mod set;
use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::{
config::ImapConfig,
imap::flag::command::{
imap::flag::{
add::AddFlagsCommand, list::ListFlagsCommand, remove::RemoveFlagsCommand,
set::SetFlagsCommand,
},
@@ -29,12 +24,12 @@ pub enum FlagCommand {
}
impl FlagCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
match self {
Self::List(cmd) => cmd.execute(printer, config),
Self::Add(cmd) => cmd.execute(printer, config),
Self::Set(cmd) => cmd.execute(printer, config),
Self::Remove(cmd) => cmd.execute(printer, config),
Self::List(cmd) => cmd.exec(printer, config),
Self::Add(cmd) => cmd.exec(printer, config),
Self::Set(cmd) => cmd.exec(printer, config),
Self::Remove(cmd) => cmd.exec(printer, config),
}
}
}
@@ -11,7 +11,7 @@ use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::Printer;
use serde::{Serialize, Serializer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalArg, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameOptionalArg, imap::stream};
/// List available flags for a mailbox.
///
@@ -25,7 +25,7 @@ pub struct ListFlagsCommand {
}
impl ListFlagsCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -46,7 +46,10 @@ impl ListFlagsCommand {
}
};
let table = FlagsTable { flags, permanent_flags };
let table = FlagsTable {
flags,
permanent_flags,
};
printer.out(table)?;
Ok(())
+4
View File
@@ -1 +1,5 @@
pub mod add;
pub mod command;
pub mod list;
pub mod remove;
pub mod set;
@@ -10,7 +10,7 @@ use io_imap::{
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameOptionalFlag, imap::stream};
/// Remove flags from messages.
///
@@ -35,7 +35,7 @@ pub struct RemoveFlagsCommand {
}
impl RemoveFlagsCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -10,7 +10,7 @@ use io_imap::{
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameOptionalFlag, imap::stream};
/// Set flags on messages (replacing existing flags).
///
@@ -35,7 +35,7 @@ pub struct SetFlagsCommand {
}
impl SetFlagsCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
-1
View File
@@ -1 +0,0 @@
pub mod name;
@@ -19,7 +19,7 @@ use crate::{config::ImapConfig, imap::stream};
pub struct CloseMailboxCommand;
impl CloseMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mut arg = None;
@@ -1,23 +1,10 @@
pub mod close;
pub mod create;
pub mod delete;
pub mod expunge;
pub mod list;
pub mod purge;
pub mod rename;
pub mod select;
pub mod status;
pub mod subscribe;
pub mod unselect;
pub mod unsubscribe;
use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::{
config::ImapConfig,
imap::mailbox::command::{
imap::mailbox::{
close::CloseMailboxCommand, create::CreateMailboxCommand, delete::DeleteMailboxCommand,
expunge::ExpungeMailboxCommand, list::ListMailboxesCommand, purge::PurgeMailboxCommand,
rename::RenameMailboxCommand, select::SelectMailboxCommand, status::StatusMailboxCommand,
@@ -50,20 +37,20 @@ pub enum MailboxCommand {
}
impl MailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
match self {
Self::Close(cmd) => cmd.execute(printer, config),
Self::Create(cmd) => cmd.execute(printer, config),
Self::Delete(cmd) => cmd.execute(printer, config),
Self::Expunge(cmd) => cmd.execute(printer, config),
Self::List(cmd) => cmd.execute(printer, config),
Self::Purge(cmd) => cmd.execute(printer, config),
Self::Rename(cmd) => cmd.execute(printer, config),
Self::Select(cmd) => cmd.execute(printer, config),
Self::Status(cmd) => cmd.execute(printer, config),
Self::Subscribe(cmd) => cmd.execute(printer, config),
Self::Unselect(cmd) => cmd.execute(printer, config),
Self::Unsubscribe(cmd) => cmd.execute(printer, config),
Self::Close(cmd) => cmd.exec(printer, config),
Self::Create(cmd) => cmd.exec(printer, config),
Self::Delete(cmd) => cmd.exec(printer, config),
Self::Expunge(cmd) => cmd.exec(printer, config),
Self::List(cmd) => cmd.exec(printer, config),
Self::Purge(cmd) => cmd.exec(printer, config),
Self::Rename(cmd) => cmd.exec(printer, config),
Self::Select(cmd) => cmd.exec(printer, config),
Self::Status(cmd) => cmd.exec(printer, config),
Self::Subscribe(cmd) => cmd.exec(printer, config),
Self::Unselect(cmd) => cmd.exec(printer, config),
Self::Unsubscribe(cmd) => cmd.exec(printer, config),
}
}
}
@@ -4,7 +4,7 @@ use io_imap::coroutines::create::*;
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameArg, imap::stream};
/// Create the given mailbox.
///
@@ -17,7 +17,7 @@ pub struct CreateMailboxCommand {
}
impl CreateMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -4,7 +4,7 @@ use io_imap::coroutines::delete::*;
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameArg, imap::stream};
/// Delete the given mailbox.
///
@@ -17,7 +17,7 @@ pub struct DeleteMailboxCommand {
}
impl DeleteMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -4,7 +4,7 @@ use io_imap::coroutines::{expunge::*, select::*};
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameArg, imap::stream};
/// Expunge the given mailbox.
///
@@ -26,7 +26,7 @@ pub struct ExpungeMailboxCommand {
}
impl ExpungeMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (mut context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -32,7 +32,7 @@ pub struct ListMailboxesCommand {
}
impl ListMailboxesCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let reference = self.reference.try_into()?;
+12
View File
@@ -1,2 +1,14 @@
pub mod arg;
pub mod close;
pub mod command;
pub mod create;
pub mod delete;
pub mod expunge;
pub mod list;
pub mod purge;
pub mod rename;
pub mod select;
pub mod status;
pub mod subscribe;
pub mod unselect;
pub mod unsubscribe;
@@ -7,7 +7,7 @@ use io_imap::{
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameArg, imap::stream};
/// Shortcut for marking as deleted all envelopes then expunging the
/// given mailbox.
@@ -30,7 +30,7 @@ pub struct PurgeMailboxCommand {
}
impl PurgeMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (mut context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -7,7 +7,7 @@ use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{
config::ImapConfig,
imap::{
mailbox::arg::name::{MailboxNameArg, TargetMailboxNameArg},
mailbox::arg::{MailboxNameArg, TargetMailboxNameArg},
stream,
},
};
@@ -25,7 +25,7 @@ pub struct RenameMailboxCommand {
}
impl RenameMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let from = self.from.name.try_into()?;
@@ -6,7 +6,7 @@ use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{
config::ImapConfig,
imap::{mailbox::arg::name::MailboxNameArg, stream},
imap::{mailbox::arg::MailboxNameArg, stream},
};
/// Select the given mailbox.
@@ -24,7 +24,7 @@ pub struct SelectMailboxCommand {
}
impl SelectMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -9,7 +9,7 @@ use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::Printer;
use serde::{Serialize, Serializer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameArg, imap::stream};
/// Get the status of the given mailbox.
///
@@ -22,7 +22,7 @@ pub struct StatusMailboxCommand {
}
impl StatusMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -4,7 +4,7 @@ use io_imap::coroutines::subscribe::*;
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameArg, imap::stream};
/// Subscribe to the given mailbox.
///
@@ -17,7 +17,7 @@ pub struct SubscribeMailboxCommand {
}
impl SubscribeMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -18,7 +18,7 @@ use crate::{config::ImapConfig, imap::stream};
pub struct UnselectMailboxCommand;
impl UnselectMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mut arg = None;
@@ -4,7 +4,7 @@ use io_imap::coroutines::unsubscribe::*;
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameArg, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameArg, imap::stream};
/// Unsubscribe from the given mailbox.
///
@@ -17,7 +17,7 @@ pub struct UnsubscribeMailboxCommand {
}
impl UnsubscribeMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -1,18 +1,10 @@
pub mod copy;
pub mod delete;
pub mod export;
pub mod get;
pub mod r#move;
pub mod read;
pub mod save;
use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::{
config::ImapConfig,
imap::message::command::{
imap::message::{
copy::CopyMessageCommand, delete::DeleteMessageCommand, export::ExportMessageCommand,
get::GetMessageCommand, r#move::MoveMessageCommand, read::ReadMessageCommand,
save::SaveMessageCommand,
@@ -36,15 +28,15 @@ pub enum MessageCommand {
}
impl MessageCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
match self {
Self::Save(cmd) => cmd.execute(printer, config),
Self::Get(cmd) => cmd.execute(printer, config),
Self::Read(cmd) => cmd.execute(printer, config),
Self::Export(cmd) => cmd.execute(printer, config),
Self::Copy(cmd) => cmd.execute(printer, config),
Self::Move(cmd) => cmd.execute(printer, config),
Self::Delete(cmd) => cmd.execute(printer, config),
Self::Save(cmd) => cmd.exec(printer, config),
Self::Get(cmd) => cmd.exec(printer, config),
Self::Read(cmd) => cmd.exec(printer, config),
Self::Export(cmd) => cmd.exec(printer, config),
Self::Copy(cmd) => cmd.exec(printer, config),
Self::Move(cmd) => cmd.exec(printer, config),
Self::Delete(cmd) => cmd.exec(printer, config),
}
}
}
@@ -7,7 +7,7 @@ use io_imap::{
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameOptionalFlag, imap::stream};
/// Copy messages to another mailbox.
///
@@ -32,7 +32,7 @@ pub struct CopyMessageCommand {
}
impl CopyMessageCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -7,7 +7,7 @@ use io_imap::{
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameOptionalFlag, imap::stream};
/// Delete messages from a mailbox.
///
@@ -28,7 +28,7 @@ pub struct DeleteMessageCommand {
}
impl DeleteMessageCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -15,7 +15,7 @@ use io_stream::runtimes::std::handle;
use mail_parser::{MessageParser, MimeHeaders};
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameOptionalFlag, imap::stream};
/// Export type for message export.
#[derive(Debug, Clone, clap::ValueEnum)]
@@ -61,7 +61,7 @@ pub struct ExportMessageCommand {
}
impl ExportMessageCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -81,13 +81,12 @@ impl ExportMessageCommand {
// FETCH with BODY.PEEK[] to avoid marking as read
let id = NonZeroU32::new(self.id).ok_or_else(|| anyhow::anyhow!("ID must be non-zero"))?;
let item_names = MacroOrMessageDataItemNames::MessageDataItemNames(vec![
MessageDataItemName::BodyExt {
let item_names =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::BodyExt {
section: None,
partial: None,
peek: true,
},
]);
}]);
let mut arg = None;
let mut coroutine = ImapFetchFirst::new(context, id, item_names, !self.seq);
@@ -163,11 +162,9 @@ impl ExportMessageCommand {
for (i, part) in message.parts.iter().enumerate() {
// Get content type
let content_type = part.content_type().map(|ct| {
match &ct.c_subtype {
let content_type = part.content_type().map(|ct| match &ct.c_subtype {
Some(sub) => format!("{}/{}", ct.c_type, sub),
None => ct.c_type.to_string(),
}
});
// Skip multipart container parts
@@ -8,11 +8,11 @@ use io_imap::{
types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
};
use io_stream::runtimes::std::handle;
use mail_parser::{Address, Addr, ContentType, MessageParser, MimeHeaders};
use mail_parser::{Addr, Address, ContentType, MessageParser, MimeHeaders};
use pimalaya_toolbox::terminal::printer::Printer;
use serde::Serialize;
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameOptionalFlag, imap::stream};
/// Get a message and display its structure.
///
@@ -33,7 +33,7 @@ pub struct GetMessageCommand {
}
impl GetMessageCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -53,13 +53,12 @@ impl GetMessageCommand {
// FETCH with BODY.PEEK[] to avoid marking as read
let id = NonZeroU32::new(self.id).ok_or_else(|| anyhow::anyhow!("ID must be non-zero"))?;
let item_names = MacroOrMessageDataItemNames::MessageDataItemNames(vec![
MessageDataItemName::BodyExt {
let item_names =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::BodyExt {
section: None,
partial: None,
peek: true,
},
]);
}]);
let mut arg = None;
let mut coroutine = ImapFetchFirst::new(context, id, item_names, !self.seq);
@@ -290,7 +289,10 @@ impl fmt::Display for MessageStructure {
]));
}
if let Some(in_reply_to) = &self.headers.in_reply_to {
table.add_row(Row::from([Cell::new("In-Reply-To"), Cell::new(in_reply_to)]));
table.add_row(Row::from([
Cell::new("In-Reply-To"),
Cell::new(in_reply_to),
]));
}
writeln!(f)?;
+7
View File
@@ -1 +1,8 @@
pub mod command;
pub mod copy;
pub mod delete;
pub mod export;
pub mod get;
pub mod r#move;
pub mod read;
pub mod save;
@@ -7,7 +7,7 @@ use io_imap::{
use io_stream::runtimes::std::handle;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameOptionalFlag, imap::stream};
/// Move messages to another mailbox.
///
@@ -33,7 +33,7 @@ pub struct MoveMessageCommand {
}
impl MoveMessageCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -11,7 +11,7 @@ use mail_parser::MessageParser;
use pimalaya_toolbox::terminal::printer::Printer;
use serde::Serialize;
use crate::{config::ImapConfig, imap::mailbox::arg::name::MailboxNameOptionalFlag, imap::stream};
use crate::{config::ImapConfig, imap::mailbox::arg::MailboxNameOptionalFlag, imap::stream};
/// Read message content.
///
@@ -40,7 +40,7 @@ pub struct ReadMessageCommand {
}
impl ReadMessageCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let mailbox = self.mailbox.name.try_into()?;
@@ -60,13 +60,12 @@ impl ReadMessageCommand {
// FETCH with BODY.PEEK[] to avoid marking as read
let id = NonZeroU32::new(self.id).ok_or_else(|| anyhow::anyhow!("ID must be non-zero"))?;
let item_names = MacroOrMessageDataItemNames::MessageDataItemNames(vec![
MessageDataItemName::BodyExt {
let item_names =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::BodyExt {
section: None,
partial: None,
peek: true,
},
]);
}]);
let mut arg = None;
let mut coroutine = ImapFetchFirst::new(context, id, item_names, !self.seq);
@@ -35,7 +35,7 @@ pub struct SaveMessageCommand {
}
impl SaveMessageCommand {
pub fn execute(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: ImapConfig) -> Result<()> {
let (mut context, mut stream) = stream::connect(config)?;
// Read message from stdin
+1 -3
View File
@@ -19,9 +19,7 @@ fn main() {
let config_paths = cli.config_paths.as_ref();
let account_name = cli.account.name.as_deref();
let result = cli
.command
.execute(&mut printer, config_paths, account_name);
let result = cli.command.exec(&mut printer, config_paths, account_name);
ErrorReport::eval(&mut printer, result)
}
+3 -3
View File
@@ -13,14 +13,14 @@ use crate::{config::SmtpConfig, smtp::message::command::MessageCommand};
#[command(rename_all = "lowercase")]
pub enum SmtpCommand {
#[command(subcommand)]
#[command(aliases = ["message", "msg"])]
#[command(aliases = ["msgs", "msg"])]
Messages(MessageCommand),
}
impl SmtpCommand {
pub fn execute(self, printer: &mut impl Printer, config: SmtpConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: SmtpConfig) -> Result<()> {
match self {
Self::Messages(cmd) => cmd.execute(printer, config),
Self::Messages(cmd) => cmd.exec(printer, config),
}
}
}
@@ -2,7 +2,7 @@ use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::{config::SmtpConfig, smtp::message::command::send::SendMessageCommand};
use crate::{config::SmtpConfig, smtp::message::send::SendMessageCommand};
/// Manage messages.
///
@@ -15,9 +15,9 @@ pub enum MessageCommand {
}
impl MessageCommand {
pub fn execute(self, printer: &mut impl Printer, config: SmtpConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: SmtpConfig) -> Result<()> {
match self {
Self::Send(cmd) => cmd.execute(printer, config),
Self::Send(cmd) => cmd.exec(printer, config),
}
}
}
-4
View File
@@ -1,4 +0,0 @@
mod command;
pub mod send;
pub use self::command::MessageCommand;
+1
View File
@@ -1 +1,2 @@
pub mod command;
pub mod send;
@@ -28,7 +28,7 @@ pub struct SendMessageCommand {
}
impl SendMessageCommand {
pub fn execute(self, printer: &mut impl Printer, config: SmtpConfig) -> Result<()> {
pub fn exec(self, printer: &mut impl Printer, config: SmtpConfig) -> Result<()> {
let (context, mut stream) = stream::connect(config)?;
let message = if stdin().is_terminal() || printer.is_json() {
+10
View File
@@ -0,0 +1,10 @@
MIME-Version: 1.0
From: <clement.douin@posteo.net>
To: <pimalaya.org@gmail.com>
Subject: Test
Message-ID: <189af1cfd6621170.2d8f0b477933eb9f.5c085132da6091e3@soywod>
Date: Sun, 8 Mar 2026 18:41:18 +0000
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
Hello!
+5
View File
@@ -0,0 +1,5 @@
From: clement.douin@posteo.net
To: pimalaya.org@gmail.com
Subject: Test
Hello!