clean part 1

This commit is contained in:
Clément DOUIN
2026-05-06 23:02:11 +02:00
parent 8416a41f99
commit cd27969e14
179 changed files with 5244 additions and 3357 deletions
Generated
+208 -18
View File
@@ -26,6 +26,15 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "1.0.0"
@@ -229,7 +238,10 @@ version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"num-traits",
"serde",
"windows-link",
]
[[package]]
@@ -643,6 +655,30 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "fuzzy-matcher"
version = "0.3.7"
@@ -782,12 +818,12 @@ dependencies = [
"convert_case 0.11.0",
"dirs",
"gethostname 1.1.0",
"humansize",
"io-discovery",
"io-email",
"io-imap",
"io-jmap",
"io-maildir",
"io-process",
"io-smtp",
"log",
"mail-builder",
@@ -814,6 +850,39 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "humansize"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
dependencies = [
"libm",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.2.0"
@@ -1000,6 +1069,7 @@ dependencies = [
name = "io-email"
version = "0.0.1"
dependencies = [
"chrono",
"io-imap",
"io-jmap",
"io-maildir",
@@ -1015,12 +1085,13 @@ dependencies = [
[[package]]
name = "io-http"
version = "0.0.3"
source = "git+https://github.com/pimalaya/io-http#c299cf8bc7512507a0e108561e10eaf4194de1a5"
dependencies = [
"anyhow",
"base64",
"httparse",
"log",
"memchr",
"pimalaya-stream",
"secrecy",
"thiserror 2.0.18",
"url",
@@ -1060,19 +1131,6 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "io-process"
version = "0.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54362e497536be1b10ad35a790838e89ce30d5095a66f12f5315154cf5041d03"
dependencies = [
"dirs",
"log",
"serde",
"shellexpand",
"thiserror 2.0.18",
]
[[package]]
name = "io-smtp"
version = "0.0.1"
@@ -1200,6 +1258,18 @@ dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
@@ -1224,6 +1294,12 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libredox"
version = "0.1.16"
@@ -1527,6 +1603,7 @@ dependencies = [
"clap_complete",
"clap_mangen",
"comfy-table",
"crossterm",
"env_logger",
"git2",
"inquire",
@@ -1545,7 +1622,6 @@ version = "0.0.1"
dependencies = [
"anyhow",
"dirs",
"io-process",
"log",
"secrecy",
"serde",
@@ -1560,8 +1636,6 @@ name = "pimalaya-stream"
version = "0.0.1"
dependencies = [
"anyhow",
"gethostname 1.1.0",
"io-smtp",
"log",
"native-tls",
"rustls",
@@ -1572,6 +1646,12 @@ dependencies = [
"url",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkg-config"
version = "0.3.33"
@@ -1922,6 +2002,12 @@ 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"
@@ -2131,6 +2217,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
@@ -2452,6 +2544,51 @@ dependencies = [
"wit-bindgen 0.51.0",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
@@ -2526,12 +2663,65 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
+6 -6
View File
@@ -20,7 +20,7 @@ default = ["imap", "smtp", "maildir", "jmap", "rustls-ring"]
imap = ["dep:io-imap", "dep:mail-parser", "dep:rfc2047-decoder", "io-email/imap", "io-imap/client"]
jmap = ["dep:base64", "dep:io-jmap", "dep:mail-parser", "dep:serde_json", "io-email/jmap", "io-jmap/client"]
smtp = ["dep:io-smtp", "dep:mail-parser", "pimalaya-stream/smtp", "io-email/smtp"]
smtp = ["dep:io-smtp", "dep:mail-parser", "io-email/smtp"]
maildir = ["dep:convert_case", "dep:io-maildir", "dep:mail-parser", "dep:mime_guess", "io-email/maildir", "io-maildir/client"]
native-tls = ["pimalaya-stream/native-tls", "io-discovery/native-tls"]
@@ -35,27 +35,27 @@ pimalaya-cli = { version = "0.0.1", default-features = false, features = ["build
[dependencies]
anyhow = "1"
base64 = { version = "0.22", optional = true }
chrono = { version = "0.4", default-features = false }
chrono = { version = "0.4", default-features = false, features = ["clock"] }
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
comfy-table = "7"
convert_case = { version = "0.11", optional = true }
dirs = "6"
gethostname = "1"
humansize = "2"
io-discovery = { version = "0.0.1", default-features = false, features = ["pacc", "autoconfig", "client"] }
io-email = { version = "0.0.1", default-features = false, features = ["serde", "client"] }
io-imap = { version = "0.0.1", default-features = false, optional = true }
io-jmap = { version = "0.0.1", default-features = false, optional = true }
io-maildir = { version = "0.0.1", default-features = false, features = ["serde"], optional = true }
io-process = { version = "0.0.2", default-features = false }
io-smtp = { version = "0.0.1", default-features = false, optional = true }
log = "0.4"
mail-builder = "0.3"
mail-parser = { version = "0.11", features = ["serde"], optional = true }
mime_guess = { version = "2", optional = true }
open = "5"
pimalaya-cli = { version = "0.0.1", default-features = false, features = ["terminal", "table", "prompt", "wizard", "imap", "smtp"] }
pimalaya-cli = { version = "0.0.1", default-features = false, features = ["terminal", "table", "prompt", "wizard", "imap", "smtp", "jmap", "spinner"] }
pimalaya-config = { version = "0.0.1", default-features = false, features = ["toml", "secret"] }
pimalaya-stream = { version = "0.0.1", default-features = false, features = ["std", "http"] }
pimalaya-stream = { version = "0.0.1", default-features = false, features = ["std"] }
rfc2047-decoder = { version = "1", optional = true }
secrecy = "0.10"
serde = { version = "1", features = ["derive"] }
@@ -78,7 +78,7 @@ tempfile = "3"
domain = { git = "https://github.com/soywod/domain", branch = "new-srv" }
io-discovery.path = "../io-discovery"
io-email.path = "../io-email"
io-http.git = "https://github.com/pimalaya/io-http"
io-http.path = "../io-http"
io-imap.path = "../io-imap"
io-jmap.path = "../io-jmap"
io-maildir.path = "../io-maildir"
-48
View File
@@ -1,48 +0,0 @@
use std::{env::temp_dir, path::PathBuf};
use crate::config::{AccountConfig, Config};
use anyhow::Result;
use comfy_table::{presets, ContentArrangement};
use dirs::download_dir;
#[derive(Clone, Debug)]
pub struct Account<B: Clone> {
pub backend: B,
pub downloads_dir: PathBuf,
pub table_preset: String,
pub table_arrangement: ContentArrangement,
}
impl<B: Clone> Account<B> {
pub fn new(config: Config, account_config: AccountConfig, backend: B) -> Result<Self> {
Ok(Self {
backend,
downloads_dir: account_config
.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string()))
.or(config
.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string())))
.or(download_dir())
.unwrap_or_else(temp_dir),
table_preset: config
.table_preset
.or(account_config.table_preset)
.unwrap_or(presets::UTF8_FULL_CONDENSED.to_string()),
table_arrangement: config
.table_arrangement
.or(account_config.table_arrangement)
.unwrap_or_default()
.into(),
})
}
}
+215
View File
@@ -0,0 +1,215 @@
use std::{fmt, path::PathBuf};
use anyhow::{bail, Result};
use clap::Parser;
use pimalaya_cli::printer::Printer;
use pimalaya_config::toml::TomlConfig;
use serde::Serialize;
use crate::{
cli::BackendFlag,
config::{AccountConfig, Config},
};
/// Validate the account configuration.
///
/// Loads the TOML configuration, picks the active account (via the
/// global `--account` flag or the default), and checks each backend
/// allowed by `--backend`. The check tries to instantiate a client per
/// backend, which exercises the same handshake / authentication paths
/// the other commands would take.
#[derive(Debug, Parser)]
pub struct AccountCheckCommand;
impl AccountCheckCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config_paths: &[PathBuf],
account_name: Option<&str>,
backend: BackendFlag,
) -> Result<()> {
let mut config = match Config::from_paths_or_default(config_paths)? {
Some(config) => config,
None => bail!(
"No configuration found. Run `himalaya` once to launch the wizard, \
or `himalaya account configure <name>` to create one."
),
};
let (name, account_config) = config
.take_account(account_name)?
.ok_or_else(|| anyhow::anyhow!("Cannot find account"))?;
let mut report = CheckReport {
account: name,
backends: Vec::new(),
};
#[cfg(feature = "imap")]
if backend.allows_imap() {
if let Some(imap_config) = account_config.imap.clone() {
report
.backends
.push(check_imap(&config, &account_config, imap_config));
}
}
#[cfg(feature = "jmap")]
if backend.allows_jmap() {
if let Some(jmap_config) = account_config.jmap.clone() {
report
.backends
.push(check_jmap(&config, &account_config, jmap_config));
}
}
#[cfg(feature = "maildir")]
if backend.allows_maildir() {
if let Some(maildir_config) = account_config.maildir.clone() {
report
.backends
.push(check_maildir(&config, &account_config, maildir_config));
}
}
#[cfg(feature = "smtp")]
if backend.allows_smtp() {
if let Some(smtp_config) = account_config.smtp.clone() {
report
.backends
.push(check_smtp(&config, &account_config, smtp_config));
}
}
if report.backends.is_empty() {
bail!("No backend matching `{backend}` is configured for this account");
}
printer.out(report)
}
}
#[cfg(feature = "imap")]
fn check_imap(
_config: &Config,
_account_config: &AccountConfig,
imap_config: crate::config::ImapConfig,
) -> BackendCheck {
use crate::imap::session::ImapSession;
let result = (|| -> Result<()> {
let _session = ImapSession::new(
imap_config.url.clone(),
imap_config.tls.clone().try_into()?,
imap_config.starttls,
imap_config.sasl.clone().try_into()?,
)?;
Ok(())
})();
BackendCheck::from("imap", result)
}
#[cfg(feature = "jmap")]
fn check_jmap(
_config: &Config,
_account_config: &AccountConfig,
jmap_config: crate::config::JmapConfig,
) -> BackendCheck {
use crate::jmap::session::JmapSession;
let result = (|| -> Result<()> {
let _session = JmapSession::new(
jmap_config.server.clone(),
jmap_config.tls.clone().try_into()?,
jmap_config.auth.clone().try_into()?,
)?;
Ok(())
})();
BackendCheck::from("jmap", result)
}
#[cfg(feature = "maildir")]
fn check_maildir(
_config: &Config,
_account_config: &AccountConfig,
maildir_config: crate::config::MaildirConfig,
) -> BackendCheck {
let result = (|| -> Result<()> {
if !maildir_config.root.is_dir() {
bail!(
"Maildir root `{}` does not exist or is not a directory",
maildir_config.root.display()
);
}
Ok(())
})();
BackendCheck::from("maildir", result)
}
#[cfg(feature = "smtp")]
fn check_smtp(
_config: &Config,
_account_config: &AccountConfig,
smtp_config: crate::config::SmtpConfig,
) -> BackendCheck {
use crate::smtp::session::SmtpSession;
let result = (|| -> Result<()> {
let _session = SmtpSession::new(
smtp_config.url.clone(),
smtp_config.tls.clone().try_into()?,
smtp_config.starttls,
smtp_config.sasl.clone().try_into()?,
)?;
Ok(())
})();
BackendCheck::from("smtp", result)
}
#[derive(Clone, Debug, Serialize)]
pub struct CheckReport {
pub account: String,
pub backends: Vec<BackendCheck>,
}
#[derive(Clone, Debug, Serialize)]
pub struct BackendCheck {
pub backend: &'static str,
pub ok: bool,
pub error: Option<String>,
}
impl BackendCheck {
fn from(backend: &'static str, result: Result<()>) -> Self {
match result {
Ok(()) => Self {
backend,
ok: true,
error: None,
},
Err(err) => Self {
backend,
ok: false,
error: Some(format!("{err:#}")),
},
}
}
}
impl fmt::Display for CheckReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Account: {}", self.account)?;
for check in &self.backends {
match &check.error {
None => writeln!(f, " {}: OK", check.backend)?,
Some(err) => writeln!(f, " {}: FAIL ({err})", check.backend)?,
}
}
Ok(())
}
}
+42
View File
@@ -0,0 +1,42 @@
use std::path::PathBuf;
use anyhow::Result;
use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::{
account::{
check::AccountCheckCommand, configure::AccountConfigureCommand, list::AccountListCommand,
},
cli::BackendFlag,
};
/// Manage accounts defined in the TOML configuration file.
///
/// An account is a named group of backend settings (imap, jmap,
/// maildir, smtp). Use these subcommands to inspect them, validate
/// them, or edit them through the interactive wizard.
#[derive(Debug, Subcommand)]
pub enum AccountCommand {
#[command(visible_alias = "ls")]
List(AccountListCommand),
Check(AccountCheckCommand),
#[command(visible_alias = "edit")]
Configure(AccountConfigureCommand),
}
impl AccountCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config_paths: &[PathBuf],
account_name: Option<&str>,
backend: BackendFlag,
) -> Result<()> {
match self {
Self::List(cmd) => cmd.execute(printer, config_paths),
Self::Check(cmd) => cmd.execute(printer, config_paths, account_name, backend),
Self::Configure(cmd) => cmd.execute(printer, config_paths),
}
}
}
+34
View File
@@ -0,0 +1,34 @@
use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use pimalaya_cli::printer::Printer;
use pimalaya_config::toml::TomlConfig;
use crate::{config::Config, wizard};
/// Edit (or create) the given account through the wizard.
///
/// Loads the configuration if any, then runs the IMAP and SMTP
/// wizards with the account's current values as defaults. Provider
/// discovery is skipped: the wizard prompts you for each field with
/// what you previously had. Creates a new account if `name` is not
/// known.
#[derive(Debug, Parser)]
pub struct AccountConfigureCommand {
/// Name of the account to edit. A new entry is created if no
/// account with this name exists in the configuration.
#[arg(value_name = "NAME")]
pub name: String,
}
impl AccountConfigureCommand {
pub fn execute(self, _printer: &mut impl Printer, config_paths: &[PathBuf]) -> Result<()> {
let target = Config::target_path(config_paths)?;
let config = Config::from_paths_or_default(config_paths)?.unwrap_or_default();
wizard::edit_account(&target, config, &self.name)?;
Ok(())
}
}
+137
View File
@@ -0,0 +1,137 @@
//! Merged runtime account — the DTO every command consumes.
//!
//! Built by the dispatch layer (`crate::cli`) in this order:
//!
//! 1. [`Account::default`] (all fields `None` / empty).
//! 2. Fold the global [`Config`] via `Account::from(config)`.
//! 3. Fold the selected `[accounts.<name>]` via [`Account::merge`]
//! with `Account::from(account_config)`.
//!
//! Defaults are applied at consumption time by the `*` accessor
//! methods, not baked in during merge — keeping `Option<T>` fields
//! lets layers compose cleanly.
use std::{collections::HashMap, env::temp_dir, path::PathBuf};
use comfy_table::{presets, ContentArrangement};
use dirs::download_dir;
use crate::config::{AccountConfig, ComposerConfig, Config, ReaderConfig, TableArrangementConfig};
const DEFAULT_DATETIME_FMT: &str = "%F %R%:z";
#[derive(Clone, Debug, Default)]
pub struct Account {
pub downloads_dir: Option<PathBuf>,
pub table_preset: Option<String>,
pub table_arrangement: Option<TableArrangementConfig>,
pub datetime_fmt: Option<String>,
pub datetime_local_tz: Option<bool>,
/// User-defined composers. Only sourced from the global
/// [`Config`]; account-level configs do not override these.
pub composer: HashMap<String, ComposerConfig>,
/// User-defined readers. See [`Account::composer`].
pub reader: HashMap<String, ReaderConfig>,
}
impl Account {
/// Folds `other`'s set fields on top of `self`. Each `Option`
/// field is taken from `other` when `Some`, otherwise from
/// `self`. The composer/reader maps are extended (entries from
/// `other` overwrite same-named entries from `self`).
pub fn merge(self, other: Self) -> Self {
let mut composer = self.composer;
composer.extend(other.composer);
let mut reader = self.reader;
reader.extend(other.reader);
Self {
downloads_dir: other.downloads_dir.or(self.downloads_dir),
table_preset: other.table_preset.or(self.table_preset),
table_arrangement: other.table_arrangement.or(self.table_arrangement),
datetime_fmt: other.datetime_fmt.or(self.datetime_fmt),
datetime_local_tz: other.datetime_local_tz.or(self.datetime_local_tz),
composer,
reader,
}
}
/// Effective downloads directory. Tries the merged
/// `downloads_dir` (shell-expanded), then the system default
/// downloads dir, then the temp dir.
pub fn downloads_dir(&self) -> PathBuf {
self.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string()))
.or_else(download_dir)
.unwrap_or_else(temp_dir)
}
/// Effective `comfy_table` preset string. Defaults to
/// `UTF8_FULL_CONDENSED`.
pub fn table_preset(&self) -> &str {
self.table_preset
.as_deref()
.unwrap_or(presets::UTF8_FULL_CONDENSED)
}
/// Effective `comfy_table` content arrangement. Defaults to
/// `Dynamic`.
pub fn table_arrangement(&self) -> ContentArrangement {
self.table_arrangement
.clone()
.unwrap_or(TableArrangementConfig::Dynamic)
.into()
}
/// Effective `chrono` `strftime` format for envelope DATE
/// columns. Defaults to `%F %R%:z`.
pub fn datetime_fmt(&self) -> &str {
self.datetime_fmt.as_deref().unwrap_or(DEFAULT_DATETIME_FMT)
}
/// Whether to convert envelope `Date:` headers to the system
/// local timezone. Defaults to `false`.
pub fn datetime_local_tz(&self) -> bool {
self.datetime_local_tz.unwrap_or(false)
}
}
impl From<Config> for Account {
fn from(config: Config) -> Self {
Self {
downloads_dir: config.downloads_dir,
table_preset: config.table_preset,
table_arrangement: config.table_arrangement,
datetime_fmt: config.envelope.list.datetime_fmt,
datetime_local_tz: config.envelope.list.datetime_local_tz,
composer: config.message.composer,
reader: config.message.reader,
}
}
}
impl From<AccountConfig> for Account {
fn from(config: AccountConfig) -> Self {
Self {
downloads_dir: config.downloads_dir,
table_preset: config.table_preset,
table_arrangement: config.table_arrangement,
datetime_fmt: config.envelope.list.datetime_fmt,
datetime_local_tz: config.envelope.list.datetime_local_tz,
composer: HashMap::new(),
reader: HashMap::new(),
}
}
}
+124
View File
@@ -0,0 +1,124 @@
use std::{fmt, path::PathBuf};
use anyhow::Result;
use clap::Parser;
use comfy_table::{Cell, ContentArrangement, Row, Table};
use pimalaya_cli::printer::Printer;
use pimalaya_config::toml::TomlConfig;
use serde::Serialize;
use crate::config::{AccountConfig, Config, TableArrangementConfig};
/// List all accounts declared in the configuration.
///
/// Each row shows the account name, the backends with a config block,
/// and whether it is the default account.
#[derive(Debug, Parser)]
pub struct AccountListCommand;
impl AccountListCommand {
pub fn execute(self, printer: &mut impl Printer, config_paths: &[PathBuf]) -> Result<()> {
let config = load_config(config_paths)?;
let preset = config
.table_preset
.clone()
.unwrap_or_else(|| comfy_table::presets::UTF8_FULL_CONDENSED.to_string());
let arrangement = config
.table_arrangement
.clone()
.unwrap_or(TableArrangementConfig::Dynamic)
.into();
let mut accounts: Vec<AccountRow> = config
.accounts
.iter()
.map(|(name, account)| AccountRow::from_account(name, account))
.collect();
accounts.sort_by(|a, b| a.name.cmp(&b.name));
let table = AccountsTable {
preset,
arrangement,
accounts,
};
printer.out(table)
}
}
fn load_config(paths: &[PathBuf]) -> Result<Config> {
match Config::from_paths_or_default(paths)? {
Some(config) => Ok(config),
None => anyhow::bail!(
"No configuration found. Run `himalaya` once to launch the wizard, \
or `himalaya account configure <name>` to create one."
),
}
}
#[derive(Clone, Debug, Serialize)]
pub struct AccountRow {
pub name: String,
pub default: bool,
pub backends: Vec<&'static str>,
}
impl AccountRow {
fn from_account(name: &str, account: &AccountConfig) -> Self {
let mut backends = Vec::new();
if account.imap.is_some() {
backends.push("imap");
}
if account.jmap.is_some() {
backends.push("jmap");
}
if account.maildir.is_some() {
backends.push("maildir");
}
if account.smtp.is_some() {
backends.push("smtp");
}
Self {
name: name.to_owned(),
default: account.default,
backends,
}
}
}
#[derive(Clone, Debug, Serialize)]
pub struct AccountsTable {
#[serde(skip)]
pub preset: String,
#[serde(skip)]
pub arrangement: ContentArrangement,
pub accounts: Vec<AccountRow>,
}
impl fmt::Display for AccountsTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new();
table
.load_preset(&self.preset)
.set_content_arrangement(self.arrangement.clone())
.set_header(Row::from(vec![
Cell::new("NAME"),
Cell::new("BACKENDS"),
Cell::new("DEFAULT"),
]))
.add_rows(self.accounts.iter().map(|account| {
let mut row = Row::new();
row.max_height(1);
row.add_cell(Cell::new(&account.name));
row.add_cell(Cell::new(account.backends.join(", ")));
row.add_cell(Cell::new(if account.default { "yes" } else { "" }));
row
}));
writeln!(f)?;
writeln!(f, "{table}")
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod check;
pub mod cli;
pub mod configure;
pub mod context;
pub mod list;
-35
View File
@@ -1,35 +0,0 @@
use anyhow::Result;
use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::{
attachments::{download::AttachmentsDownloadCommand, list::AttachmentsListCommand},
cli::BackendArg,
config::{AccountConfig, Config},
};
/// List or download attachments carried by a single message.
///
/// Available wherever `messages get` is — that is, IMAP, JMAP and
/// Maildir. The active backend is selected by `--backend` (default
/// `auto`).
#[derive(Debug, Subcommand)]
pub enum AttachmentsCommand {
List(AttachmentsListCommand),
Download(AttachmentsDownloadCommand),
}
impl AttachmentsCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config: Config,
account_config: AccountConfig,
backend: BackendArg,
) -> Result<()> {
match self {
Self::List(cmd) => cmd.execute(printer, config, account_config, backend),
Self::Download(cmd) => cmd.execute(printer, config, account_config, backend),
}
}
}
-153
View File
@@ -1,153 +0,0 @@
use std::{
fs,
path::{Path, PathBuf},
};
use anyhow::{bail, Result};
use clap::Parser;
use mail_parser::{MessageParser, MimeHeaders};
use pimalaya_cli::printer::{Message, Printer};
use crate::{
account::Account,
cli::BackendArg,
config::{AccountConfig, Config},
};
/// Download the attachments carried by a single message to disk.
///
/// "Attachment" follows mail_parser's classification: parts with
/// `Content-Disposition: attachment`, or any non-body part with a
/// `filename`/`name` parameter. Inline parts are skipped by default;
/// pass `--include-inline` to download them too.
///
/// The destination directory defaults to the account's
/// `downloads-dir` config (falling back to the global one, then the
/// platform's standard downloads directory). Pass `--dir <PATH>` to
/// override.
#[derive(Debug, Parser)]
pub struct AttachmentsDownloadCommand {
/// Identifier of the message (IMAP UID, JMAP email id, or Maildir
/// filename id).
#[arg(value_name = "ID")]
pub id: String,
/// Mailbox name or path (IMAP/Maildir). Ignored for JMAP.
#[arg(
long = "mailbox",
short = 'm',
value_name = "NAME",
default_value = "Inbox"
)]
pub mailbox: String,
/// Destination directory. Overrides the account/global
/// `downloads-dir` config.
#[arg(long = "dir", short = 'd', value_name = "PATH")]
pub dir: Option<PathBuf>,
/// Include parts with `Content-Disposition: inline`.
#[arg(long = "include-inline")]
pub include_inline: bool,
}
impl AttachmentsDownloadCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config: Config,
account_config: AccountConfig,
backend: BackendArg,
) -> Result<()> {
let raw = crate::messages::fetch::fetch_raw(
&config,
&account_config,
backend,
&self.mailbox,
&self.id,
)?;
let Some(message) = MessageParser::new().parse(&raw) else {
bail!("Failed to parse RFC 5322 message");
};
let account = Account::new(config, account_config, ())?;
let dir = self.dir.clone().unwrap_or(account.downloads_dir);
if !dir.exists() {
fs::create_dir_all(&dir)?;
}
let mut written = Vec::new();
for (index, part) in message.attachments().enumerate() {
let inline = part
.content_disposition()
.map(|cd| cd.c_type.eq_ignore_ascii_case("inline"))
.unwrap_or(false);
if inline && !self.include_inline {
continue;
}
let filename = part
.attachment_name()
.map(str::to_owned)
.unwrap_or_else(|| format!("attachment-{index}"));
let safe = sanitize(&filename);
let path = unique_path(&dir, &safe);
fs::write(&path, part.contents())?;
written.push(path.display().to_string());
}
if written.is_empty() {
return printer.out(Message::new("No attachments to download"));
}
printer.out(Message::new(format!(
"Downloaded {} attachment(s):\n {}",
written.len(),
written.join("\n ")
)))
}
}
/// Strips path separators and parent traversals so a hostile filename
/// header can't escape the download directory.
fn sanitize(name: &str) -> String {
let trimmed = name.trim();
let cleaned: String = trimmed
.chars()
.map(|c| match c {
'/' | '\\' | '\0' => '_',
_ => c,
})
.collect();
let cleaned = cleaned.trim_start_matches('.').trim();
if cleaned.is_empty() {
"attachment".to_string()
} else {
cleaned.to_string()
}
}
/// Returns a path inside `dir` that doesn't already exist by suffixing
/// `(1)`, `(2)`, … to the stem when needed.
fn unique_path(dir: &Path, name: &str) -> PathBuf {
let candidate = dir.join(name);
if !candidate.exists() {
return candidate;
}
let (stem, ext) = match name.rsplit_once('.') {
Some((s, e)) if !s.is_empty() => (s.to_string(), format!(".{e}")),
_ => (name.to_string(), String::new()),
};
for n in 1..1024 {
let candidate = dir.join(format!("{stem} ({n}){ext}"));
if !candidate.exists() {
return candidate;
}
}
dir.join(name)
}
-107
View File
@@ -1,107 +0,0 @@
use anyhow::{bail, Result};
use clap::Parser;
use mail_parser::{MessageParser, MessagePart, MimeHeaders};
use pimalaya_cli::printer::Printer;
use crate::{
account::Account,
attachments::table::{AttachmentEntry, AttachmentsTable},
cli::BackendArg,
config::{AccountConfig, Config},
};
/// List the attachments carried by a single message in the active
/// account.
///
/// "Attachment" follows mail_parser's classification: parts with
/// `Content-Disposition: attachment`, or any non-body part with a
/// `filename`/`name` parameter. Inline parts (e.g. embedded images
/// referenced by HTML bodies) are skipped by default; pass
/// `--include-inline` to surface them too.
#[derive(Debug, Parser)]
pub struct AttachmentsListCommand {
/// Identifier of the message (IMAP UID, JMAP email id, or Maildir
/// filename id).
#[arg(value_name = "ID")]
pub id: String,
/// Mailbox name or path (IMAP/Maildir). Ignored for JMAP.
#[arg(
long = "mailbox",
short = 'm',
value_name = "NAME",
default_value = "Inbox"
)]
pub mailbox: String,
/// Include parts with `Content-Disposition: inline`.
#[arg(long = "include-inline")]
pub include_inline: bool,
}
impl AttachmentsListCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config: Config,
account_config: AccountConfig,
backend: BackendArg,
) -> Result<()> {
let raw = crate::messages::fetch::fetch_raw(
&config,
&account_config,
backend,
&self.mailbox,
&self.id,
)?;
let Some(message) = MessageParser::new().parse(&raw) else {
bail!("Failed to parse RFC 5322 message");
};
let mut attachments = Vec::new();
for (index, part) in message.attachments().enumerate() {
let inline = is_inline(part);
if inline && !self.include_inline {
continue;
}
attachments.push(AttachmentEntry {
index,
filename: part
.attachment_name()
.map(str::to_owned)
.unwrap_or_else(|| format!("attachment-{index}")),
mime: mime_string(part),
size: part.contents().len(),
inline,
});
}
// Reuse the active account's table styling. Constructing
// an `Account<()>` is enough to read the preset/arrangement.
let account = Account::new(config, account_config, ())?;
printer.out(AttachmentsTable {
preset: account.table_preset,
arrangement: account.table_arrangement,
attachments,
})
}
}
fn is_inline(part: &MessagePart<'_>) -> bool {
part.content_disposition()
.map(|cd| cd.c_type.eq_ignore_ascii_case("inline"))
.unwrap_or(false)
}
fn mime_string(part: &MessagePart<'_>) -> String {
let Some(ct) = part.content_type() else {
return "application/octet-stream".to_string();
};
match ct.c_subtype.as_deref() {
Some(sub) => format!("{}/{}", ct.c_type, sub),
None => ct.c_type.to_string(),
}
}
-68
View File
@@ -1,68 +0,0 @@
use std::fmt;
use comfy_table::{Cell, ContentArrangement, Row, Table};
use serde::Serialize;
/// One row of the `attachments list` output.
#[derive(Clone, Debug, Serialize)]
pub struct AttachmentEntry {
pub index: usize,
pub filename: String,
pub mime: String,
pub size: usize,
pub inline: bool,
}
#[derive(Clone, Debug, Serialize)]
pub struct AttachmentsTable {
#[serde(skip)]
pub preset: String,
#[serde(skip)]
pub arrangement: ContentArrangement,
pub attachments: Vec<AttachmentEntry>,
}
impl fmt::Display for AttachmentsTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new();
table
.load_preset(&self.preset)
.set_content_arrangement(self.arrangement.clone())
.set_header(Row::from([
Cell::new("INDEX"),
Cell::new("FILENAME"),
Cell::new("MIME"),
Cell::new("SIZE"),
Cell::new("INLINE"),
]))
.add_rows(self.attachments.iter().map(|a| {
let mut row = Row::new();
row.max_height(1);
row.add_cell(Cell::new(a.index));
row.add_cell(Cell::new(&a.filename));
row.add_cell(Cell::new(&a.mime));
row.add_cell(Cell::new(human_size(a.size)));
row.add_cell(Cell::new(if a.inline { "yes" } else { "no" }));
row
}));
writeln!(f)?;
writeln!(f, "{table}")
}
}
fn human_size(bytes: usize) -> String {
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
let mut size = bytes as f64;
let mut unit = 0;
while size >= 1024.0 && unit < UNITS.len() - 1 {
size /= 1024.0;
unit += 1;
}
if unit == 0 {
format!("{bytes} B")
} else {
format!("{size:.1} {}", UNITS[unit])
}
}
+122 -142
View File
@@ -14,16 +14,22 @@ use pimalaya_cli::{
use pimalaya_config::toml::TomlConfig;
#[cfg(feature = "imap")]
use crate::imap::cli::ImapCommand;
use crate::imap::{cli::ImapCommand, client::build_imap_client};
#[cfg(feature = "jmap")]
use crate::jmap::cli::JmapCommand;
use crate::jmap::{cli::JmapCommand, client::build_jmap_client};
#[cfg(feature = "maildir")]
use crate::maildir::cli::MaildirCommand;
use crate::maildir::{cli::MaildirCommand, client::build_maildir_client};
#[cfg(feature = "smtp")]
use crate::smtp::cli::SmtpCommand;
use crate::smtp::{cli::SmtpCommand, client::build_smtp_client};
use crate::{
account::Account, config::Config, envelopes::cli::EnvelopesCommand, flags::cli::FlagsCommand,
mailboxes::cli::MailboxesCommand, messages::cli::MessagesCommand,
account::cli::AccountCommand,
config::Config,
shared::{
attachments::cli::AttachmentCommand, client::build_email_client,
envelopes::cli::EnvelopeCommand, flags::cli::FlagCommand, mailboxes::cli::MailboxCommand,
messages::cli::MessageCommand,
},
wizard,
};
#[derive(Parser, Debug)]
@@ -33,7 +39,7 @@ use crate::{
#[command(propagate_version = true, infer_subcommands = true)]
pub struct HimalayaCli {
#[command(subcommand)]
pub command: BackendCommand,
pub command: HimalayaCommand,
/// Override the default configuration file path.
///
@@ -50,7 +56,6 @@ pub struct HimalayaCli {
pub config_paths: Vec<PathBuf>,
#[command(flatten)]
pub account: AccountFlag,
/// Force a specific backend for cross-protocol commands.
///
/// Only consumed by the shared commands (`mailboxes`, `envelopes`,
@@ -65,14 +70,115 @@ pub struct HimalayaCli {
/// config block, or if the operation has no implementation for it
/// — e.g. `--backend smtp mailboxes list`).
#[arg(short, long, global = true, default_value_t)]
pub backend: BackendArg,
pub backend: BackendFlag,
#[command(flatten)]
pub json: JsonFlag,
#[command(flatten)]
pub log: LogFlags,
}
#[derive(Debug, Subcommand)]
pub enum HimalayaCommand {
// --- Shared API
//
#[command(subcommand, aliases = ["mboxes", "mbox"])]
Mailboxes(MailboxCommand),
#[command(subcommand)]
Envelopes(EnvelopeCommand),
#[command(subcommand)]
Flags(FlagCommand),
#[command(subcommand)]
Messages(MessageCommand),
#[command(subcommand)]
Attachments(AttachmentCommand),
// --- Protocol-specific APIs
//
#[cfg(feature = "imap")]
#[command(subcommand)]
Imap(ImapCommand),
#[cfg(feature = "jmap")]
#[command(subcommand)]
Jmap(JmapCommand),
#[cfg(feature = "maildir")]
#[command(subcommand)]
Maildir(MaildirCommand),
#[cfg(feature = "smtp")]
#[command(subcommand)]
Smtp(SmtpCommand),
// --- Meta
//
#[command(subcommand)]
Account(AccountCommand),
Completions(CompletionCommand),
Manuals(ManualCommand),
}
impl HimalayaCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config_paths: &[PathBuf],
account_name: Option<&str>,
backend: BackendFlag,
) -> Result<()> {
match self {
// --- Shared API
//
Self::Mailboxes(cmd) => {
let client = build_email_client(config_paths, account_name, backend)?;
cmd.execute(printer, client)
}
Self::Envelopes(cmd) => {
let client = build_email_client(config_paths, account_name, backend)?;
cmd.execute(printer, client)
}
Self::Flags(cmd) => {
let client = build_email_client(config_paths, account_name, backend)?;
cmd.execute(printer, client)
}
Self::Messages(cmd) => {
let client = build_email_client(config_paths, account_name, backend)?;
cmd.execute(printer, client)
}
Self::Attachments(cmd) => {
let client = build_email_client(config_paths, account_name, backend)?;
cmd.execute(printer, client)
}
// --- Protocol-specific APIs
//
#[cfg(feature = "imap")]
Self::Imap(cmd) => {
let client = build_imap_client(config_paths, account_name)?;
cmd.execute(printer, client)
}
#[cfg(feature = "jmap")]
Self::Jmap(cmd) => {
let client = build_jmap_client(config_paths, account_name)?;
cmd.execute(printer, client)
}
#[cfg(feature = "maildir")]
Self::Maildir(cmd) => {
let client = build_maildir_client(config_paths, account_name)?;
cmd.execute(printer, client)
}
#[cfg(feature = "smtp")]
Self::Smtp(cmd) => {
let client = build_smtp_client(config_paths, account_name)?;
cmd.execute(printer, client)
}
// --- Meta
//
Self::Account(cmd) => cmd.execute(printer, config_paths, account_name, backend),
Self::Completions(cmd) => cmd.execute(printer, HimalayaCli::command()),
Self::Manuals(cmd) => cmd.execute(printer, HimalayaCli::command()),
}
}
}
/// Selects which backend a cross-protocol command should target.
///
/// `Auto` lets the command pick the first configured-and-supported
@@ -83,7 +189,7 @@ pub struct HimalayaCli {
/// The protocol-specific subcommands (`imap`, `jmap`, `maildir`,
/// `smtp`) ignore this arg entirely.
#[derive(Clone, Copy, Debug, Default, Parser, PartialEq, Eq)]
pub enum BackendArg {
pub enum BackendFlag {
#[default]
Auto,
Imap,
@@ -92,7 +198,7 @@ pub enum BackendArg {
Smtp,
}
impl BackendArg {
impl BackendFlag {
/// Whether the IMAP arm of a shared command is allowed to run.
pub fn allows_imap(self) -> bool {
matches!(self, Self::Auto | Self::Imap)
@@ -114,7 +220,7 @@ impl BackendArg {
}
}
impl FromStr for BackendArg {
impl FromStr for BackendFlag {
type Err = Error;
fn from_str(backend: &str) -> Result<Self, Self::Err> {
@@ -128,7 +234,7 @@ impl FromStr for BackendArg {
}
}
}
impl fmt::Display for BackendArg {
impl fmt::Display for BackendFlag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Auto => write!(f, "auto"),
@@ -140,138 +246,12 @@ impl fmt::Display for BackendArg {
}
}
#[derive(Debug, Subcommand)]
pub enum BackendCommand {
Manuals(ManualCommand),
Completions(CompletionCommand),
#[command(subcommand)]
Mailboxes(MailboxesCommand),
#[command(subcommand)]
Envelopes(EnvelopesCommand),
#[command(subcommand)]
Flags(FlagsCommand),
#[command(subcommand)]
Messages(MessagesCommand),
#[cfg(any(feature = "imap", feature = "jmap", feature = "maildir"))]
#[command(subcommand)]
Attachments(crate::attachments::cli::AttachmentsCommand),
#[cfg(feature = "imap")]
#[command(subcommand)]
Imap(ImapCommand),
#[cfg(feature = "jmap")]
#[command(subcommand)]
Jmap(JmapCommand),
#[cfg(feature = "maildir")]
#[command(subcommand)]
Maildir(MaildirCommand),
#[cfg(feature = "smtp")]
#[command(subcommand)]
Smtp(SmtpCommand),
}
impl BackendCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config_paths: &[PathBuf],
account_name: Option<&str>,
backend: BackendArg,
) -> Result<()> {
match self {
Self::Manuals(cmd) => cmd.execute(printer, HimalayaCli::command()),
Self::Completions(cmd) => cmd.execute(printer, HimalayaCli::command()),
Self::Mailboxes(cmd) => {
let config = load_or_wizard(config_paths)?;
let (_, account_config) = config.get_account(account_name)?;
cmd.execute(printer, config, account_config, backend)
}
Self::Envelopes(cmd) => {
let config = load_or_wizard(config_paths)?;
let (_, account_config) = config.get_account(account_name)?;
cmd.execute(printer, config, account_config, backend)
}
Self::Flags(cmd) => {
let config = load_or_wizard(config_paths)?;
let (_, account_config) = config.get_account(account_name)?;
cmd.execute(printer, config, account_config, backend)
}
Self::Messages(cmd) => {
let config = load_or_wizard(config_paths)?;
let (_, account_config) = config.get_account(account_name)?;
cmd.execute(printer, config, account_config, backend)
}
#[cfg(any(feature = "imap", feature = "jmap", feature = "maildir"))]
Self::Attachments(cmd) => {
let config = load_or_wizard(config_paths)?;
let (_, account_config) = config.get_account(account_name)?;
cmd.execute(printer, config, account_config, backend)
}
#[cfg(feature = "imap")]
Self::Imap(cmd) => {
let config = load_or_wizard(config_paths)?;
let (account_name, mut account_config) = config.get_account(account_name)?;
let Some(imap_config) = account_config.imap.take() else {
bail!("IMAP config is missing for account `{account_name}`")
};
let account = Account::new(config, account_config, imap_config)?;
cmd.execute(printer, account)
}
#[cfg(feature = "jmap")]
Self::Jmap(cmd) => {
let config = load_or_wizard(config_paths)?;
let (account_name, mut account_config) = config.get_account(account_name)?;
let Some(jmap_config) = account_config.jmap.take() else {
bail!("JMAP config is missing for account `{account_name}`")
};
let account = Account::new(config, account_config, jmap_config)?;
cmd.execute(printer, account)
}
#[cfg(feature = "maildir")]
Self::Maildir(cmd) => {
let config = load_or_wizard(config_paths)?;
let (account_name, mut account_config) = config.get_account(account_name)?;
let Some(maildir_config) = account_config.maildir.take() else {
bail!("Maildir config is missing for account `{account_name}`")
};
let account = Account::new(config, account_config, maildir_config)?;
cmd.execute(printer, account)
}
#[cfg(feature = "smtp")]
Self::Smtp(cmd) => {
let config = load_or_wizard(config_paths)?;
let (account_name, mut account_config) = config.get_account(account_name)?;
let Some(smtp_config) = account_config.smtp.take() else {
bail!("SMTP config is missing for account `{account_name}`")
};
let account = Account::new(config, account_config, smtp_config)?;
cmd.execute(printer, account)
}
}
}
}
/// Loads `Config` from `paths`, or runs the wizard if no config file
/// is found. Centralises the `Result<Option<Config>>` → `Config`
/// adaptation so call sites stay readable.
fn load_or_wizard(paths: &[PathBuf]) -> Result<Config> {
pub(crate) fn load_or_wizard(paths: &[PathBuf]) -> Result<Config> {
match Config::from_paths_or_default(paths)? {
Some(config) => Ok(config),
None => crate::wizard::run_or_exit(&Config::target_path(paths)?),
None => wizard::run_or_exit(&Config::target_path(paths)?),
}
}
+91 -10
View File
@@ -8,7 +8,7 @@ use pimalaya_config::{
};
use pimalaya_stream::{
sasl::{Sasl, SaslAnonymous, SaslLogin, SaslMechanism, SaslPlain},
tls::{Rustls, RustlsCrypto, Tls, TlsProvider},
std::tls::{Rustls, RustlsCrypto, Tls, TlsProvider},
};
use serde::{Deserialize, Serialize};
use url::Url;
@@ -22,6 +22,10 @@ pub struct Config {
pub downloads_dir: Option<PathBuf>,
pub table_preset: Option<String>,
pub table_arrangement: Option<TableArrangementConfig>,
#[serde(default)]
pub envelope: EnvelopeConfig,
#[serde(default)]
pub message: MessageConfig,
pub accounts: HashMap<String, AccountConfig>,
}
@@ -32,17 +36,17 @@ impl TomlConfig for Config {
env!("CARGO_PKG_NAME")
}
fn find_default_account(&self) -> Option<(String, Self::Account)> {
self.accounts
.iter()
.find(|(_, account)| account.default)
.map(|(name, account)| (name.to_owned(), account.clone()))
fn take_named_account(&mut self, name: &str) -> Option<(String, Self::Account)> {
self.accounts.remove_entry(name)
}
fn find_account(&self, name: &str) -> Option<(String, Self::Account)> {
self.accounts
.get(name)
.map(|account| (name.to_owned(), account.clone()))
fn take_default_account(&mut self) -> Option<(String, Self::Account)> {
let name = self
.accounts
.iter()
.find_map(|(name, account)| account.default.then(|| name.clone()))?;
self.take_named_account(&name)
}
}
@@ -77,6 +81,9 @@ pub struct AccountConfig {
pub table_preset: Option<String>,
pub table_arrangement: Option<TableArrangementConfig>,
#[serde(default)]
pub envelope: EnvelopeConfig,
#[allow(unused)]
pub imap: Option<ImapConfig>,
#[allow(unused)]
@@ -87,6 +94,80 @@ pub struct AccountConfig {
pub smtp: Option<SmtpConfig>,
}
/// Envelope-level rendering options.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct EnvelopeConfig {
#[serde(default)]
pub list: EnvelopeListConfig,
}
/// `envelopes list` rendering options. Mirrors the pre-v2
/// `envelope.list.*` keys.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct EnvelopeListConfig {
/// chrono `strftime` format used to render the DATE column.
/// Defaults to `"%F %R%:z"` (e.g. `2026-05-06 14:30+02:00`) when
/// neither the global nor the account config sets it.
pub datetime_fmt: Option<String>,
/// When `true`, the `Date:` header timezone offset is converted
/// to the system's local timezone before formatting. Defaults to
/// `false`, which preserves the wire offset.
pub datetime_local_tz: Option<bool>,
}
/// Message-level configuration: user-defined composers and readers.
///
/// Composers produce a MIME draft on stdout (called by `compose-with`,
/// `reply-with`, `forward-with`). Readers consume a MIME message from
/// stdin and emit human-readable bytes on stdout (called by
/// `read-with`). Both are looked up by name; the entry flagged
/// `default = true` is used when no name is passed.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct MessageConfig {
#[serde(default)]
pub composer: HashMap<String, ComposerConfig>,
#[serde(default)]
pub reader: HashMap<String, ReaderConfig>,
}
/// Single composer entry under `[message.composer.<name>]`.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ComposerConfig {
/// Shell command line invoked via `sh -c`. Stdin carries the
/// source MIME bytes (empty for new messages); stdout is
/// captured as the MIME draft; stderr is inherited so the
/// composer can prompt the user.
pub command: String,
/// Marks this entry as the fallback when `compose-with` /
/// `reply-with` / `forward-with` are invoked without a name.
/// Exactly one composer should set this; if several do, the
/// first one returned by the config lookup wins.
#[serde(default)]
pub default: bool,
}
/// Single reader entry under `[message.reader.<name>]`.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ReaderConfig {
/// Shell command line invoked via `sh -c`. Stdin carries the
/// source MIME bytes; stdout is forwarded to the terminal (zero
/// bytes is fine — the reader may have spawned its own UI);
/// stderr is inherited.
pub command: String,
/// Marks this entry as the fallback when `read-with` is
/// invoked without a name.
#[serde(default)]
pub default: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub enum TableArrangementConfig {
-119
View File
@@ -1,119 +0,0 @@
//! Builder for the unified [`io_email::client::EmailClient`] used by
//! cross-protocol shared subcommands (`mailboxes`, `envelopes`,
//! `flags`, `messages`).
//!
//! The legacy per-backend dispatch — three nearly-identical
//! `if backend.allows_X() { … resume loop … }` blocks per command —
//! is replaced by a single call to [`build`] that returns a fully
//! authenticated [`EmailContext`]. The shared command then calls one
//! [`EmailClient`] method and renders the result.
//!
//! Construction is still backend-asymmetric (IMAP needs TLS + SASL,
//! JMAP needs an HTTP credential, Maildir just needs a root path),
//! and that asymmetry is collapsed here. We delegate to the existing
//! transitional [`ImapSession`] / [`JmapSession`] helpers for the
//! handshake/auth flow, then bridge the resulting `(stream, context)`
//! pairs into [`io_imap::client::ImapClient`] / [`io_jmap::client::JmapClient`]
//! via their `from_parts` constructors.
//!
//! [`ImapSession`]: crate::imap::session::ImapSession
//! [`JmapSession`]: crate::jmap::session::JmapSession
use std::path::PathBuf;
use anyhow::{bail, Result};
use comfy_table::ContentArrangement;
use io_email::client::EmailClient;
use crate::{
account::Account,
cli::BackendArg,
config::{AccountConfig, Config},
};
/// Bundle handed to shared commands: a fully-built [`EmailClient`]
/// plus the account-level rendering settings the per-backend
/// dispatchers used to extract independently.
pub struct EmailContext {
pub client: EmailClient,
#[allow(dead_code)]
pub downloads_dir: PathBuf,
pub table_preset: String,
pub table_arrangement: ContentArrangement,
}
/// Builds an [`EmailContext`] from `(config, account_config, backend)`.
///
/// Tries each backend in `imap → jmap → maildir` order, picking the
/// first one whose config block is present and whose [`BackendArg`]
/// filter allows it. Bails when nothing matches. SMTP is omitted on
/// purpose: none of the shared read-side operations have an SMTP
/// implementation.
pub fn build(
config: Config,
mut account_config: AccountConfig,
backend: BackendArg,
) -> Result<EmailContext> {
#[cfg(feature = "imap")]
if backend.allows_imap() {
if let Some(imap_config) = account_config.imap.take() {
use crate::imap::session::ImapSession;
use io_imap::client::ImapClient;
let account = Account::new(config, account_config, imap_config)?;
let session = ImapSession::new(
account.backend.url.clone(),
account.backend.tls.clone().try_into()?,
account.backend.starttls,
account.backend.sasl.clone().try_into()?,
)?;
let client = ImapClient::from_parts(session.stream, session.context);
return Ok(EmailContext {
client: client.into(),
downloads_dir: account.downloads_dir,
table_preset: account.table_preset,
table_arrangement: account.table_arrangement,
});
}
}
#[cfg(feature = "jmap")]
if backend.allows_jmap() {
if let Some(jmap_config) = account_config.jmap.take() {
use crate::jmap::session::JmapSession;
use io_jmap::client::JmapClient;
let account = Account::new(config, account_config, jmap_config)?;
let session = JmapSession::new(
account.backend.server.clone(),
account.backend.tls.clone().try_into()?,
account.backend.auth.clone().try_into()?,
)?;
let client = JmapClient::from_parts(session.stream, session.http_auth, session.session);
return Ok(EmailContext {
client: client.into(),
downloads_dir: account.downloads_dir,
table_preset: account.table_preset,
table_arrangement: account.table_arrangement,
});
}
}
#[cfg(feature = "maildir")]
if backend.allows_maildir() {
if let Some(maildir_config) = account_config.maildir.take() {
use io_maildir::client::MaildirClient;
let account = Account::new(config, account_config, maildir_config)?;
let client = MaildirClient::new(account.backend.root.clone());
return Ok(EmailContext {
client: client.into(),
downloads_dir: account.downloads_dir,
table_preset: account.table_preset,
table_arrangement: account.table_arrangement,
});
}
}
bail!("no backend matching `{backend}` is configured for this account")
}
-34
View File
@@ -1,34 +0,0 @@
use anyhow::Result;
use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::{
cli::BackendArg,
config::{AccountConfig, Config},
envelopes::list::EnvelopesListCommand,
};
/// List envelopes through whichever backend the active account has
/// configured.
///
/// The active backend is selected by `--backend` (defaults to `auto`,
/// which picks the first configured backend in priority order).
#[derive(Debug, Subcommand)]
pub enum EnvelopesCommand {
#[command(visible_alias = "ls")]
List(EnvelopesListCommand),
}
impl EnvelopesCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config: Config,
account_config: AccountConfig,
backend: BackendArg,
) -> Result<()> {
match self {
Self::List(cmd) => cmd.execute(printer, config, account_config, backend),
}
}
}
-59
View File
@@ -1,59 +0,0 @@
use anyhow::Result;
use clap::Parser;
use pimalaya_cli::printer::Printer;
use crate::{
cli::BackendArg,
config::{AccountConfig, Config},
email_client::build,
envelopes::table::EnvelopesTable,
};
/// List envelopes for the active account, regardless of the underlying
/// backend (IMAP, JMAP or Maildir).
#[derive(Debug, Parser)]
pub struct EnvelopesListCommand {
/// Path or name of the IMAP/Maildir mailbox.
#[arg(
long = "mailbox",
short = 'm',
value_name = "PATH",
default_value = "Inbox"
)]
pub mailbox: String,
/// Page number, starting from 1. The most recent envelopes are on
/// page 1.
#[arg(long, short = 'p', value_name = "N", default_value = "1")]
pub page: u32,
/// Maximum number of envelopes per page.
#[arg(
long = "page-size",
short = 's',
value_name = "N",
default_value = "25"
)]
pub page_size: u32,
}
impl EnvelopesListCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config: Config,
account_config: AccountConfig,
backend: BackendArg,
) -> Result<()> {
let mut ctx = build(config, account_config, backend)?;
let envelopes =
ctx.client
.list_envelopes(&self.mailbox, Some(self.page), Some(self.page_size))?;
printer.out(EnvelopesTable {
preset: ctx.table_preset,
arrangement: ctx.table_arrangement,
envelopes,
})
}
}
-59
View File
@@ -1,59 +0,0 @@
use std::fmt;
use comfy_table::{Cell, ContentArrangement, Row, Table};
use io_email::envelope::Envelope;
use serde::Serialize;
#[derive(Clone, Debug, Serialize)]
pub struct EnvelopesTable {
#[serde(skip)]
pub preset: String,
#[serde(skip)]
pub arrangement: ContentArrangement,
pub envelopes: Vec<Envelope>,
}
impl fmt::Display for EnvelopesTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new();
table
.load_preset(&self.preset)
.set_content_arrangement(self.arrangement.clone())
.set_header(Row::from([
Cell::new("ID"),
Cell::new("FLAGS"),
Cell::new("SUBJECT"),
Cell::new("FROM"),
Cell::new("DATE"),
]))
.add_rows(self.envelopes.iter().map(|e| {
let mut row = Row::new();
row.max_height(1);
row.add_cell(Cell::new(&e.id));
row.add_cell(Cell::new(
e.flags
.iter()
.map(|f| format!("{f:?}"))
.collect::<Vec<_>>()
.join(", "),
));
row.add_cell(Cell::new(&e.subject));
row.add_cell(Cell::new(
e.from
.iter()
.map(|a| match &a.name {
Some(name) if !name.is_empty() => name.clone(),
_ => a.email.clone(),
})
.collect::<Vec<_>>()
.join(", "),
));
row.add_cell(Cell::new(&e.date));
row
}));
writeln!(f)?;
writeln!(f, "{table}")
}
}
-40
View File
@@ -1,40 +0,0 @@
use anyhow::Result;
use clap::Parser;
use pimalaya_cli::printer::{Message, Printer};
use crate::{
cli::BackendArg,
config::{AccountConfig, Config},
email_client::build,
flags::arg::{FlagsArg, MailboxFlag, MessageIdsArg},
};
/// Add flag(s) to message(s) for the active account.
#[derive(Debug, Parser)]
pub struct FlagsAddCommand {
#[command(flatten)]
pub ids: MessageIdsArg,
#[command(flatten)]
pub flags: FlagsArg,
#[command(flatten)]
pub mailbox: MailboxFlag,
}
impl FlagsAddCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config: Config,
account_config: AccountConfig,
backend: BackendArg,
) -> Result<()> {
let mut ctx = build(config, account_config, backend)?;
let ids: Vec<&str> = self.ids.inner.iter().map(String::as_str).collect();
let flags: Vec<io_email::flag::Flag> = self.flags.inner.iter().map(Into::into).collect();
ctx.client.add_flags(&self.mailbox.inner, &ids, &flags)?;
printer.out(Message::new("Flag(s) successfully added"))
}
}
-38
View File
@@ -1,38 +0,0 @@
use anyhow::Result;
use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::{
cli::BackendArg,
config::{AccountConfig, Config},
flags::{add::FlagsAddCommand, delete::FlagsDeleteCommand, set::FlagsSetCommand},
};
/// Manage flags through whichever backend the active account has
/// configured.
///
/// The active backend is selected by `--backend` (defaults to `auto`,
/// which picks the first configured backend in priority order).
#[derive(Debug, Subcommand)]
pub enum FlagsCommand {
Add(FlagsAddCommand),
Set(FlagsSetCommand),
#[command(visible_alias = "remove", visible_alias = "rm")]
Delete(FlagsDeleteCommand),
}
impl FlagsCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config: Config,
account_config: AccountConfig,
backend: BackendArg,
) -> Result<()> {
match self {
Self::Add(cmd) => cmd.execute(printer, config, account_config, backend),
Self::Set(cmd) => cmd.execute(printer, config, account_config, backend),
Self::Delete(cmd) => cmd.execute(printer, config, account_config, backend),
}
}
}
-40
View File
@@ -1,40 +0,0 @@
use anyhow::Result;
use clap::Parser;
use pimalaya_cli::printer::{Message, Printer};
use crate::{
cli::BackendArg,
config::{AccountConfig, Config},
email_client::build,
flags::arg::{FlagsArg, MailboxFlag, MessageIdsArg},
};
/// Remove flag(s) from message(s) for the active account.
#[derive(Debug, Parser)]
pub struct FlagsDeleteCommand {
#[command(flatten)]
pub ids: MessageIdsArg,
#[command(flatten)]
pub flags: FlagsArg,
#[command(flatten)]
pub mailbox: MailboxFlag,
}
impl FlagsDeleteCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config: Config,
account_config: AccountConfig,
backend: BackendArg,
) -> Result<()> {
let mut ctx = build(config, account_config, backend)?;
let ids: Vec<&str> = self.ids.inner.iter().map(String::as_str).collect();
let flags: Vec<io_email::flag::Flag> = self.flags.inner.iter().map(Into::into).collect();
ctx.client.delete_flags(&self.mailbox.inner, &ids, &flags)?;
printer.out(Message::new("Flag(s) successfully removed"))
}
}
-40
View File
@@ -1,40 +0,0 @@
use anyhow::Result;
use clap::Parser;
use pimalaya_cli::printer::{Message, Printer};
use crate::{
cli::BackendArg,
config::{AccountConfig, Config},
email_client::build,
flags::arg::{FlagsArg, MailboxFlag, MessageIdsArg},
};
/// Replace the flags of message(s) with the given set.
#[derive(Debug, Parser)]
pub struct FlagsSetCommand {
#[command(flatten)]
pub ids: MessageIdsArg,
#[command(flatten)]
pub flags: FlagsArg,
#[command(flatten)]
pub mailbox: MailboxFlag,
}
impl FlagsSetCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config: Config,
account_config: AccountConfig,
backend: BackendArg,
) -> Result<()> {
let mut ctx = build(config, account_config, backend)?;
let ids: Vec<&str> = self.ids.inner.iter().map(String::as_str).collect();
let flags: Vec<io_email::flag::Flag> = self.flags.inner.iter().map(Into::into).collect();
ctx.client.set_flags(&self.mailbox.inner, &ids, &flags)?;
printer.out(Message::new("Flag(s) successfully set"))
}
}
-21
View File
@@ -1,21 +0,0 @@
use anyhow::Result;
use io_imap::client::ImapClient;
use crate::{account::Account, config::ImapConfig, imap::session::ImapSession};
pub type ImapAccount = Account<ImapConfig>;
impl ImapAccount {
/// Opens the IMAP connection (TCP/TLS/STARTTLS, greeting, SASL),
/// then hands the established stream and context off to a fresh
/// [`ImapClient`].
pub fn new_imap_client(&self) -> Result<ImapClient> {
let session = ImapSession::new(
self.backend.url.clone(),
self.backend.tls.clone().try_into()?,
self.backend.starttls,
self.backend.sasl.clone().try_into()?,
)?;
Ok(ImapClient::from_parts(session.stream, session.context))
}
}
+8 -8
View File
@@ -3,11 +3,11 @@ use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::imap::{
account::ImapAccount, envelope::cli::ImapEnvelopeCommand, flag::cli::ImapFlagCommand,
client::ImapClient, envelope::cli::ImapEnvelopeCommand, flag::cli::ImapFlagCommand,
id::ImapIdCommand, mailbox::cli::ImapMailboxCommand, message::cli::ImapMessageCommand,
};
/// IMAP CLI (requires the `imap` cargo feature).
/// IMAP CLI.
///
/// This command gives you access to the IMAP CLI API, and allows you
/// to manage IMAP mailboxes, envelopes, flags, messages etc.
@@ -29,14 +29,14 @@ pub enum ImapCommand {
}
impl ImapCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
pub fn execute(self, printer: &mut impl Printer, client: ImapClient) -> Result<()> {
match self {
Self::Id(cmd) => cmd.execute(printer, account),
Self::Id(cmd) => cmd.execute(printer, client),
Self::Envelopes(cmd) => cmd.execute(printer, account),
Self::Flags(cmd) => cmd.execute(printer, account),
Self::Mailboxes(cmd) => cmd.execute(printer, account),
Self::Messages(cmd) => cmd.execute(printer, account),
Self::Envelopes(cmd) => cmd.execute(printer, client),
Self::Flags(cmd) => cmd.execute(printer, client),
Self::Mailboxes(cmd) => cmd.execute(printer, client),
Self::Messages(cmd) => cmd.execute(printer, client),
}
}
}
+73
View File
@@ -0,0 +1,73 @@
//! Himalaya wrapper around [`io_imap::client::ImapClient`] that
//! bundles the merged [`Account`] alongside the live IMAP client.
//!
//! This is what every IMAP-specific subcommand receives: the dispatch
//! layer (`crate::cli`) opens the session up front via
//! [`build_imap_client`] and hands the ready-to-use wrapper down.
use std::{
ops::{Deref, DerefMut},
path::PathBuf,
};
use anyhow::{anyhow, Result};
use io_imap::client::ImapClient as Inner;
use pimalaya_config::toml::TomlConfig;
use crate::{
account::context::Account, cli::load_or_wizard, config::ImapConfig, imap::session::ImapSession,
};
pub struct ImapClient {
inner: Inner,
pub account: Account,
}
impl ImapClient {
/// Opens the IMAP connection (TCP/TLS/STARTTLS, greeting, SASL)
/// then wraps the resulting stream + context in an
/// [`io_imap::client::ImapClient`] alongside `account`.
pub fn new(config: ImapConfig, account: Account) -> Result<Self> {
let session = ImapSession::new(
config.url,
config.tls.try_into()?,
config.starttls,
config.sasl.try_into()?,
)?;
let inner = Inner::from_parts(session.stream, session.context);
Ok(Self { inner, account })
}
}
impl Deref for ImapClient {
type Target = Inner;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for ImapClient {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
/// Loads the configuration, picks the active account, builds the
/// merged [`Account`] then opens the IMAP session. Bails when the
/// account has no `[imap]` block.
pub fn build_imap_client(
config_paths: &[PathBuf],
account_name: Option<&str>,
) -> Result<ImapClient> {
let mut config = load_or_wizard(config_paths)?;
let (name, mut ac) = config
.take_account(account_name)?
.ok_or_else(|| anyhow!("Cannot find account"))?;
let imap_config = ac
.imap
.take()
.ok_or_else(|| anyhow!("IMAP config is missing for account `{name}`"))?;
let account = Account::from(config).merge(Account::from(ac));
ImapClient::new(imap_config, account)
}
+7 -7
View File
@@ -3,7 +3,7 @@ use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::imap::{
account::ImapAccount,
client::ImapClient,
envelope::{
get::ImapEnvelopeGetCommand, list::ImapEnvelopeListCommand,
search::ImapEnvelopeSearchCommand, sort::ImapEnvelopeSortCommand,
@@ -26,13 +26,13 @@ pub enum ImapEnvelopeCommand {
}
impl ImapEnvelopeCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
pub fn execute(self, printer: &mut impl Printer, client: ImapClient) -> Result<()> {
match self {
Self::Get(cmd) => cmd.execute(printer, account),
Self::List(cmd) => cmd.execute(printer, account),
Self::Search(cmd) => cmd.execute(printer, account),
Self::Sort(cmd) => cmd.execute(printer, account),
Self::Thread(cmd) => cmd.execute(printer, account),
Self::Get(cmd) => cmd.execute(printer, client),
Self::List(cmd) => cmd.execute(printer, client),
Self::Search(cmd) => cmd.execute(printer, client),
Self::Sort(cmd) => cmd.execute(printer, client),
Self::Thread(cmd) => cmd.execute(printer, client),
}
}
}
+3 -4
View File
@@ -11,7 +11,7 @@ use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::imap::{
account::ImapAccount,
client::ImapClient,
envelope::list::{decode_mime, format_address},
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
@@ -37,8 +37,7 @@ pub struct ImapEnvelopeGetCommand {
}
impl ImapEnvelopeGetCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
if !self.mailbox_no_select.inner {
@@ -60,7 +59,7 @@ impl ImapEnvelopeGetCommand {
};
let table = EnvelopeTable {
preset: account.table_preset,
preset: client.account.table_preset().to_string(),
envelope: items.into(),
};
+4 -5
View File
@@ -16,7 +16,7 @@ use rfc2047_decoder::{Decoder, RecoverStrategy};
use serde::Serialize;
use crate::imap::{
account::ImapAccount,
client::ImapClient,
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
@@ -50,8 +50,7 @@ pub struct ImapEnvelopeListCommand {
}
impl ImapEnvelopeListCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
let exists = if self.mailbox_no_select.inner {
@@ -84,8 +83,8 @@ impl ImapEnvelopeListCommand {
let data = client.fetch(sequence_set, item_names, !self.sequence && has_sequence)?;
let table = EnvelopesTable {
preset: account.table_preset,
arrangement: account.table_arrangement,
preset: client.account.table_preset().to_string(),
arrangement: client.account.table_arrangement(),
envelopes: map_envelopes_table_entries(data),
};
+4 -5
View File
@@ -12,7 +12,7 @@ use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::imap::{
account::ImapAccount,
client::ImapClient,
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
@@ -58,8 +58,7 @@ pub struct ImapEnvelopeSearchCommand {
}
impl ImapEnvelopeSearchCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
if !self.mailbox_no_select.inner {
@@ -70,8 +69,8 @@ impl ImapEnvelopeSearchCommand {
let ids = client.search(criteria, !self.seq)?;
let table = SearchTable {
preset: account.table_preset,
arrangement: account.table_arrangement,
preset: client.account.table_preset().to_string(),
arrangement: client.account.table_arrangement(),
ids: ids
.into_iter()
.map(|id| SearchResult { id: id.get() })
+2 -3
View File
@@ -11,7 +11,7 @@ use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::imap::{
account::ImapAccount, envelope::search::parse_query, mailbox::arg::MailboxNameOptionalArg,
client::ImapClient, envelope::search::parse_query, mailbox::arg::MailboxNameOptionalArg,
};
/// Sort messages by criteria.
@@ -51,8 +51,7 @@ pub struct ImapEnvelopeSortCommand {
}
impl ImapEnvelopeSortCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
client.select(mailbox)?;
+3 -7
View File
@@ -2,19 +2,16 @@ use std::{collections::HashMap, fmt, num::NonZeroU32};
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::{
client::ImapClient,
types::{
use io_imap::types::{
extensions::thread::{Thread, ThreadingAlgorithm},
fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
sequence::SequenceSet,
},
};
use pimalaya_cli::printer::Printer;
use serde::{ser::SerializeStruct, Serialize, Serializer};
use crate::imap::{
account::ImapAccount,
client::ImapClient,
envelope::{list::decode_mime, search::parse_query},
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
@@ -48,8 +45,7 @@ pub struct ImapEnvelopeThreadCommand {
}
impl ImapEnvelopeThreadCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
if !self.mailbox_no_select.inner {
+2 -3
View File
@@ -7,7 +7,7 @@ use io_imap::types::{
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
client::ImapClient,
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
@@ -35,8 +35,7 @@ pub struct ImapFlagAddCommand {
}
impl ImapFlagAddCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
if !self.mailbox_no_select.inner {
+6 -6
View File
@@ -3,7 +3,7 @@ use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::imap::{
account::ImapAccount,
client::ImapClient,
flag::{
add::ImapFlagAddCommand, list::ImapFlagListCommand, remove::ImapFlagRemoveCommand,
set::ImapFlagSetCommand,
@@ -23,12 +23,12 @@ pub enum ImapFlagCommand {
}
impl ImapFlagCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
pub fn execute(self, printer: &mut impl Printer, client: ImapClient) -> Result<()> {
match self {
Self::List(cmd) => cmd.execute(printer, account),
Self::Add(cmd) => cmd.execute(printer, account),
Self::Set(cmd) => cmd.execute(printer, account),
Self::Remove(cmd) => cmd.execute(printer, account),
Self::List(cmd) => cmd.execute(printer, client),
Self::Add(cmd) => cmd.execute(printer, client),
Self::Set(cmd) => cmd.execute(printer, client),
Self::Remove(cmd) => cmd.execute(printer, client),
}
}
}
+4 -5
View File
@@ -7,7 +7,7 @@ use io_imap::types::flag::{Flag, FlagPerm};
use pimalaya_cli::printer::Printer;
use serde::{Serialize, Serializer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
use crate::imap::{client::ImapClient, mailbox::arg::MailboxNameArg};
/// List available IMAP flags for the given mailbox.
///
@@ -21,8 +21,7 @@ pub struct ImapFlagListCommand {
}
impl ImapFlagListCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
let data = client.select(mailbox)?;
@@ -30,8 +29,8 @@ impl ImapFlagListCommand {
let permanent_flags = data.permanent_flags.unwrap_or_default();
let table = FlagsTable {
preset: account.table_preset,
arrangement: account.table_arrangement,
preset: client.account.table_preset().to_string(),
arrangement: client.account.table_arrangement(),
flags,
permanent_flags,
};
+2 -3
View File
@@ -7,7 +7,7 @@ use io_imap::types::{
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
client::ImapClient,
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
@@ -35,8 +35,7 @@ pub struct ImapFlagRemoveCommand {
}
impl ImapFlagRemoveCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
if !self.mailbox_no_select.inner {
+2 -3
View File
@@ -7,7 +7,7 @@ use io_imap::types::{
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
client::ImapClient,
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
@@ -35,8 +35,7 @@ pub struct ImapFlagSetCommand {
}
impl ImapFlagSetCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
if !self.mailbox_no_select.inner {
+3 -4
View File
@@ -10,7 +10,7 @@ use io_imap::types::{
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::imap::account::ImapAccount;
use crate::imap::client::ImapClient;
/// Get information about the IMAP server.
///
@@ -27,8 +27,7 @@ pub struct ImapIdCommand {
}
impl ImapIdCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mut params = HashMap::new();
params.extend([
@@ -57,7 +56,7 @@ impl ImapIdCommand {
let params = client.id(Some(params.into_iter().collect()))?;
let table = ServerIdTable {
preset: account.table_preset,
preset: client.account.table_preset().to_string(),
server_id: params
.unwrap_or_default()
.into_iter()
+14 -14
View File
@@ -3,7 +3,7 @@ use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::imap::{
account::ImapAccount,
client::ImapClient,
mailbox::{
close::ImapMailboxCloseCommand, create::ImapMailboxCreateCommand,
delete::ImapMailboxDeleteCommand, expunge::ImapMailboxExpungeCommand,
@@ -38,20 +38,20 @@ pub enum ImapMailboxCommand {
}
impl ImapMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
pub fn execute(self, printer: &mut impl Printer, client: ImapClient) -> Result<()> {
match self {
Self::Close(cmd) => cmd.execute(printer, account),
Self::Create(cmd) => cmd.execute(printer, account),
Self::Delete(cmd) => cmd.execute(printer, account),
Self::Expunge(cmd) => cmd.execute(printer, account),
Self::List(cmd) => cmd.execute(printer, account),
Self::Purge(cmd) => cmd.execute(printer, account),
Self::Rename(cmd) => cmd.execute(printer, account),
Self::Select(cmd) => cmd.execute(printer, account),
Self::Status(cmd) => cmd.execute(printer, account),
Self::Subscribe(cmd) => cmd.execute(printer, account),
Self::Unselect(cmd) => cmd.execute(printer, account),
Self::Unsubscribe(cmd) => cmd.execute(printer, account),
Self::Close(cmd) => cmd.execute(printer, client),
Self::Create(cmd) => cmd.execute(printer, client),
Self::Delete(cmd) => cmd.execute(printer, client),
Self::Expunge(cmd) => cmd.execute(printer, client),
Self::List(cmd) => cmd.execute(printer, client),
Self::Purge(cmd) => cmd.execute(printer, client),
Self::Rename(cmd) => cmd.execute(printer, client),
Self::Select(cmd) => cmd.execute(printer, client),
Self::Status(cmd) => cmd.execute(printer, client),
Self::Subscribe(cmd) => cmd.execute(printer, client),
Self::Unselect(cmd) => cmd.execute(printer, client),
Self::Unsubscribe(cmd) => cmd.execute(printer, client),
}
}
}
+2 -3
View File
@@ -2,7 +2,7 @@ use anyhow::Result;
use clap::Parser;
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::account::ImapAccount;
use crate::imap::client::ImapClient;
/// Close the current, selected mailbox.
///
@@ -17,8 +17,7 @@ use crate::imap::account::ImapAccount;
pub struct ImapMailboxCloseCommand;
impl ImapMailboxCloseCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
client.close()?;
printer.out(Message::new("Mailbox successfully closed"))
}
+2 -3
View File
@@ -2,7 +2,7 @@ use anyhow::Result;
use clap::Parser;
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
use crate::imap::{client::ImapClient, mailbox::arg::MailboxNameArg};
/// Create the given mailbox.
///
@@ -15,8 +15,7 @@ pub struct ImapMailboxCreateCommand {
}
impl ImapMailboxCreateCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
client.create(mailbox)?;
printer.out(Message::new("Mailbox successfully created"))
+2 -3
View File
@@ -2,7 +2,7 @@ use anyhow::Result;
use clap::Parser;
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
use crate::imap::{client::ImapClient, mailbox::arg::MailboxNameArg};
/// Delete the given mailbox.
///
@@ -15,8 +15,7 @@ pub struct ImapMailboxDeleteCommand {
}
impl ImapMailboxDeleteCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
client.delete(mailbox)?;
printer.out(Message::new("Mailbox successfully deleted"))
+2 -3
View File
@@ -3,7 +3,7 @@ use clap::Parser;
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
client::ImapClient,
mailbox::arg::{MailboxNameArg, MailboxNoSelectFlag},
};
@@ -20,8 +20,7 @@ pub struct ImapMailboxExpungeCommand {
}
impl ImapMailboxExpungeCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
if !self.mailbox_no_select.inner {
+15 -4
View File
@@ -3,11 +3,12 @@ use std::fmt;
use anyhow::Result;
use clap::Parser;
use comfy_table::{Cell, Row, Table};
use io_email::mailbox::MailboxRole;
use io_imap::types::{core::QuotedChar, flag::FlagNameAttribute, mailbox::Mailbox};
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::imap::account::ImapAccount;
use crate::imap::client::ImapClient;
/// List, search and filter mailboxes.
///
@@ -30,8 +31,7 @@ pub struct ImapMailboxListCommand {
}
impl ImapMailboxListCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let reference = self.reference.try_into()?;
let pattern = self.pattern.try_into()?;
@@ -42,7 +42,7 @@ impl ImapMailboxListCommand {
};
let table = MailboxesTable {
preset: account.table_preset,
preset: client.account.table_preset().to_string(),
mailboxes: mailboxes.into_iter().map(From::from).collect(),
};
@@ -66,14 +66,25 @@ impl fmt::Display for MailboxesTable {
.set_header(Row::from([
Cell::new("NAME"),
Cell::new("DELIMITER"),
Cell::new("ROLE"),
Cell::new("ATTRIBUTES"),
]))
.add_rows(self.mailboxes.iter().map(|mbox| {
let mut row = Row::new();
let role = mbox
.attributes
.iter()
.find_map(|raw| match MailboxRole::parse(raw) {
MailboxRole::Other(_) => None,
role => Some(format!("{role:?}")),
})
.unwrap_or_default();
row.max_height(1)
.add_cell(Cell::new(&mbox.name))
.add_cell(Cell::new(&mbox.delimiter))
.add_cell(Cell::new(role))
.add_cell(Cell::new(mbox.attributes.join(", ")));
row
+2 -3
View File
@@ -4,7 +4,7 @@ use io_imap::types::flag::{Flag, StoreType};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
client::ImapClient,
mailbox::arg::{MailboxNameArg, MailboxNoSelectFlag},
};
@@ -22,8 +22,7 @@ pub struct ImapMailboxPurgeCommand {
}
impl ImapMailboxPurgeCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
if !self.mailbox_no_select.inner {
+2 -3
View File
@@ -3,7 +3,7 @@ use clap::Parser;
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
client::ImapClient,
mailbox::arg::{MailboxNameArg, TargetMailboxNameArg},
};
@@ -19,8 +19,7 @@ pub struct ImapMailboxRenameCommand {
}
impl ImapMailboxRenameCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let from = self.mailbox_source_name.inner.try_into()?;
let to = self.mailbox_dest_name.inner.try_into()?;
client.rename(from, to)?;
+2 -3
View File
@@ -2,7 +2,7 @@ use anyhow::Result;
use clap::Parser;
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
use crate::imap::{client::ImapClient, mailbox::arg::MailboxNameArg};
/// Select the given mailbox.
///
@@ -19,8 +19,7 @@ pub struct ImapMailboxSelectCommand {
}
impl ImapMailboxSelectCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
client.select(mailbox)?;
printer.out(Message::new("Mailbox successfully selected"))
+3 -4
View File
@@ -7,7 +7,7 @@ use io_imap::types::status::{StatusDataItem, StatusDataItemName};
use pimalaya_cli::printer::Printer;
use serde::{Serialize, Serializer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
use crate::imap::{client::ImapClient, mailbox::arg::MailboxNameArg};
/// Get the status of the given mailbox.
///
@@ -20,8 +20,7 @@ pub struct ImapMailboxStatusCommand {
}
impl ImapMailboxStatusCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
let item_names = vec![
StatusDataItemName::Messages,
@@ -34,7 +33,7 @@ impl ImapMailboxStatusCommand {
let items = client.status(mailbox, item_names)?;
let table = MailboxStatusTable {
preset: account.table_preset,
preset: client.account.table_preset().to_string(),
status: items.into(),
};
+2 -3
View File
@@ -2,7 +2,7 @@ use anyhow::Result;
use clap::Parser;
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
use crate::imap::{client::ImapClient, mailbox::arg::MailboxNameArg};
/// Subscribe to the given mailbox.
///
@@ -15,8 +15,7 @@ pub struct ImapMailboxSubscribeCommand {
}
impl ImapMailboxSubscribeCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
client.subscribe(mailbox)?;
printer.out(Message::new("Mailbox successfully subscribed"))
+2 -3
View File
@@ -2,7 +2,7 @@ use anyhow::Result;
use clap::Parser;
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::account::ImapAccount;
use crate::imap::client::ImapClient;
/// Unselect a current, selected mailbox.
///
@@ -16,8 +16,7 @@ use crate::imap::account::ImapAccount;
pub struct ImapMailboxUnselectCommand;
impl ImapMailboxUnselectCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
client.unselect()?;
printer.out(Message::new("Mailbox successfully unselected"))
}
+2 -3
View File
@@ -2,7 +2,7 @@ use anyhow::Result;
use clap::Parser;
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
use crate::imap::{client::ImapClient, mailbox::arg::MailboxNameArg};
/// Unsubscribe from the given mailbox.
///
@@ -15,8 +15,7 @@ pub struct ImapMailboxUnsubscribeCommand {
}
impl ImapMailboxUnsubscribeCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
client.unsubscribe(mailbox)?;
printer.out(Message::new("Mailbox successfully unsubscribed"))
+8 -8
View File
@@ -3,7 +3,7 @@ use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::imap::{
account::ImapAccount,
client::ImapClient,
message::{
copy::ImapMessageCopyCommand, export::ImapMessageExportCommand, get::ImapMessageGetCommand,
r#move::ImapMessageMoveCommand, read::ImapMessageReadCommand, save::ImapMessageSaveCommand,
@@ -26,14 +26,14 @@ pub enum ImapMessageCommand {
}
impl ImapMessageCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
pub fn execute(self, printer: &mut impl Printer, client: ImapClient) -> Result<()> {
match self {
Self::Save(cmd) => cmd.execute(printer, account),
Self::Get(cmd) => cmd.execute(printer, account),
Self::Read(cmd) => cmd.execute(printer, account),
Self::Export(cmd) => cmd.execute(printer, account),
Self::Copy(cmd) => cmd.execute(printer, account),
Self::Move(cmd) => cmd.execute(printer, account),
Self::Save(cmd) => cmd.execute(printer, client),
Self::Get(cmd) => cmd.execute(printer, client),
Self::Read(cmd) => cmd.execute(printer, client),
Self::Export(cmd) => cmd.execute(printer, client),
Self::Copy(cmd) => cmd.execute(printer, client),
Self::Move(cmd) => cmd.execute(printer, client),
}
}
}
+2 -3
View File
@@ -4,7 +4,7 @@ use io_imap::types::mailbox::Mailbox;
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
client::ImapClient,
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag, TargetMailboxNameArg},
};
@@ -31,8 +31,7 @@ pub struct ImapMessageCopyCommand {
}
impl ImapMessageCopyCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
if !self.mailbox_no_select.inner {
+5 -4
View File
@@ -10,7 +10,7 @@ use io_imap::types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, Messag
use mail_parser::{MessageParser, MimeHeaders};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag};
use crate::imap::{client::ImapClient, mailbox::arg::MailboxNameOptionalFlag};
/// Export type for message export.
#[derive(Debug, Clone, clap::ValueEnum)]
@@ -56,8 +56,7 @@ pub struct ImapMessageExportCommand {
}
impl ImapMessageExportCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
client.select(mailbox)?;
@@ -117,7 +116,9 @@ impl ImapMessageExportCommand {
// Generate filename from subject or message-id
let filename = generate_eml_filename(&message, self.id);
let dir = self.directory.unwrap_or(account.downloads_dir);
let dir = self
.directory
.unwrap_or_else(|| client.account.downloads_dir());
if !dir.exists() {
fs::create_dir_all(&dir)?;
+2 -3
View File
@@ -9,7 +9,7 @@ use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::imap::{
account::ImapAccount,
client::ImapClient,
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
@@ -32,8 +32,7 @@ pub struct ImapMessageGetCommand {
}
impl ImapMessageGetCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
if self.id == 0 {
bail!("ID must be non-zero");
+2 -3
View File
@@ -4,7 +4,7 @@ use io_imap::types::mailbox::Mailbox;
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
client::ImapClient,
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag, TargetMailboxNameArg},
};
@@ -32,8 +32,7 @@ pub struct ImapMessageMoveCommand {
}
impl ImapMessageMoveCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
if !self.mailbox_no_select.inner {
+2 -3
View File
@@ -8,7 +8,7 @@ use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::imap::{
account::ImapAccount,
client::ImapClient,
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
@@ -37,8 +37,7 @@ pub struct ImapMessageReadCommand {
}
impl ImapMessageReadCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox = self.mailbox_name.inner.try_into()?;
if !self.mailbox_no_select.inner {
+2 -3
View File
@@ -7,7 +7,7 @@ use io_imap::types::{
};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
use crate::imap::{client::ImapClient, mailbox::arg::MailboxNameArg};
/// Save a message to a mailbox.
///
@@ -29,8 +29,7 @@ pub struct ImapMessageSaveCommand {
}
impl ImapMessageSaveCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut client = account.new_imap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: ImapClient) -> Result<()> {
let mailbox: Mailbox<'static> = self.mailbox.inner.try_into()?;
let message = if !self.message.is_empty() || stdin().is_terminal() || printer.is_json() {
self.message
+1 -1
View File
@@ -1,5 +1,5 @@
pub mod account;
pub mod cli;
pub mod client;
pub mod envelope;
pub mod flag;
pub mod id;
+3 -1
View File
@@ -30,8 +30,10 @@ use io_imap::{
use log::info;
use pimalaya_stream::{
sasl::{Sasl, SaslMechanism},
std::stream::Stream,
std::{
stream::Stream,
tls::{upgrade_tls, Tls},
},
};
#[cfg(windows)]
use uds_windows::UnixStream;
-24
View File
@@ -1,24 +0,0 @@
use anyhow::Result;
use io_jmap::client::JmapClient;
use crate::{account::Account, config::JmapConfig, jmap::session::JmapSession};
pub type JmapAccount = Account<JmapConfig>;
impl JmapAccount {
/// Establishes the JMAP session (TLS, `/.well-known/jmap` discovery)
/// then hands the resulting stream, bearer token and discovered
/// session off to a fresh [`JmapClient`].
pub fn new_jmap_client(&self) -> Result<JmapClient> {
let session = JmapSession::new(
self.backend.server.clone(),
self.backend.tls.clone().try_into()?,
self.backend.auth.clone().try_into()?,
)?;
Ok(JmapClient::from_parts(
session.stream,
session.http_auth,
session.session,
))
}
}
+10 -10
View File
@@ -3,13 +3,13 @@ use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::jmap::{
account::JmapAccount, email::cli::JmapEmailCommand, identity::cli::JmapIdentityCommand,
client::JmapClient, email::cli::JmapEmailCommand, identity::cli::JmapIdentityCommand,
mailbox::cli::JmapMailboxCommand, query::JmapQueryCommand,
submission::cli::JmapSubmissionCommand, thread::cli::JmapThreadCommand,
vacation::cli::JmapVacationCommand,
};
/// JMAP CLI (requires the `jmap` cargo feature).
/// JMAP CLI.
///
/// This command gives you access to the JMAP CLI API, and allows you
/// to manage JMAP mailboxes, threads, emails, identities, submissions
@@ -40,16 +40,16 @@ pub enum JmapCommand {
}
impl JmapCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
pub fn execute(self, printer: &mut impl Printer, client: JmapClient) -> Result<()> {
match self {
Self::Mailboxes(cmd) => cmd.execute(printer, account),
Self::Emails(cmd) => cmd.execute(printer, account),
Self::Mailboxes(cmd) => cmd.execute(printer, client),
Self::Emails(cmd) => cmd.execute(printer, client),
Self::Threads(cmd) => cmd.execute(printer, account),
Self::Identity(cmd) => cmd.execute(printer, account),
Self::Submission(cmd) => cmd.execute(printer, account),
Self::Vacation(cmd) => cmd.execute(printer, account),
Self::Query(cmd) => cmd.execute(printer, account),
Self::Threads(cmd) => cmd.execute(printer, client),
Self::Identity(cmd) => cmd.execute(printer, client),
Self::Submission(cmd) => cmd.execute(printer, client),
Self::Vacation(cmd) => cmd.execute(printer, client),
Self::Query(cmd) => cmd.execute(printer, client),
}
}
}
+82
View File
@@ -0,0 +1,82 @@
//! Himalaya wrapper around [`io_jmap::client::JmapClient`] that
//! bundles the merged [`Account`] alongside the live JMAP client.
//!
//! Built up front by the dispatch layer (`crate::cli`) via
//! [`build_jmap_client`] and handed down to every JMAP-specific
//! subcommand.
use std::{
ops::{Deref, DerefMut},
path::PathBuf,
};
use anyhow::{anyhow, Result};
use io_jmap::client::JmapClient as Inner;
use pimalaya_config::toml::TomlConfig;
use crate::{
account::context::Account, cli::load_or_wizard, config::JmapConfig, jmap::session::JmapSession,
};
pub struct JmapClient {
inner: Inner,
pub account: Account,
/// The original JMAP config block, kept around so commands like
/// `email import` / `email export` can spin up their own
/// auxiliary sessions (e.g. against the upload/download URL when
/// it lives on a different authority than the API URL).
pub config: JmapConfig,
}
impl JmapClient {
/// Establishes the JMAP session (TLS, `/.well-known/jmap`
/// discovery) then wraps the resulting client alongside
/// `account`.
pub fn new(config: JmapConfig, account: Account) -> Result<Self> {
let session = JmapSession::new(
config.server.clone(),
config.tls.clone().try_into()?,
config.auth.clone().try_into()?,
)?;
let inner = Inner::from_parts(session.stream, session.http_auth, session.session);
Ok(Self {
inner,
account,
config,
})
}
}
impl Deref for JmapClient {
type Target = Inner;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for JmapClient {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
/// Loads the configuration, picks the active account, builds the
/// merged [`Account`] then opens the JMAP session. Bails when the
/// account has no `[jmap]` block.
pub fn build_jmap_client(
config_paths: &[PathBuf],
account_name: Option<&str>,
) -> Result<JmapClient> {
let mut config = load_or_wizard(config_paths)?;
let (name, mut ac) = config
.take_account(account_name)?
.ok_or_else(|| anyhow!("Cannot find account"))?;
let jmap_config = ac
.jmap
.take()
.ok_or_else(|| anyhow!("JMAP config is missing for account `{name}`"))?;
let account = Account::from(config).merge(Account::from(ac));
JmapClient::new(jmap_config, account)
}
+11 -11
View File
@@ -3,7 +3,7 @@ use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::jmap::{
account::JmapAccount,
client::JmapClient,
email::{
copy::JmapEmailCopyCommand, delete::JmapEmailDestroyCommand,
export::JmapEmailExportCommand, get::JmapEmailGetCommand, import::JmapEmailImportCommand,
@@ -30,17 +30,17 @@ pub enum JmapEmailCommand {
}
impl JmapEmailCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
pub fn execute(self, printer: &mut impl Printer, client: JmapClient) -> Result<()> {
match self {
Self::Get(cmd) => cmd.execute(printer, account),
Self::Query(cmd) => cmd.execute(printer, account),
Self::Read(cmd) => cmd.execute(printer, account),
Self::Update(cmd) => cmd.execute(printer, account),
Self::Delete(cmd) => cmd.execute(printer, account),
Self::Copy(cmd) => cmd.execute(printer, account),
Self::Export(cmd) => cmd.execute(printer, account),
Self::Import(cmd) => cmd.execute(printer, account),
Self::Parse(cmd) => cmd.execute(printer, account),
Self::Get(cmd) => cmd.execute(printer, client),
Self::Query(cmd) => cmd.execute(printer, client),
Self::Read(cmd) => cmd.execute(printer, client),
Self::Update(cmd) => cmd.execute(printer, client),
Self::Delete(cmd) => cmd.execute(printer, client),
Self::Copy(cmd) => cmd.execute(printer, client),
Self::Export(cmd) => cmd.execute(printer, client),
Self::Import(cmd) => cmd.execute(printer, client),
Self::Parse(cmd) => cmd.execute(printer, client),
}
}
}
+2 -4
View File
@@ -5,7 +5,7 @@ use clap::Parser;
use io_jmap::rfc8621::email::EmailCopy;
use pimalaya_cli::printer::{Message, Printer};
use crate::jmap::{account::JmapAccount, error::format_set_error};
use crate::jmap::{client::JmapClient, error::format_set_error};
/// Copy JMAP emails from another account (Email/copy).
#[derive(Debug, Parser)]
@@ -24,9 +24,7 @@ pub struct JmapEmailCopyCommand {
}
impl JmapEmailCopyCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let mailbox_ids: BTreeMap<String, bool> =
self.mailbox_id.into_iter().map(|m| (m, true)).collect();
+2 -3
View File
@@ -3,7 +3,7 @@ use clap::Parser;
use io_jmap::rfc8621::email_set::JmapEmailSetArgs;
use pimalaya_cli::printer::{Message, Printer};
use crate::jmap::{account::JmapAccount, error::format_set_error};
use crate::jmap::{client::JmapClient, error::format_set_error};
/// Delete JMAP emails (Email/set destroy).
#[derive(Debug, Parser)]
@@ -14,8 +14,7 @@ pub struct JmapEmailDestroyCommand {
}
impl JmapEmailDestroyCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let mut args = JmapEmailSetArgs::default();
for id in self.ids {
+7 -9
View File
@@ -2,13 +2,13 @@ use std::net::TcpStream;
use anyhow::{anyhow, Result};
use clap::Parser;
use io_jmap::{client::JmapClient, rfc8621::capabilities::MAIL};
use io_jmap::{client::JmapClient as InnerJmapClient, rfc8621::capabilities::MAIL};
use pimalaya_cli::printer::{Message, Printer};
use pimalaya_stream::tls::upgrade_tls;
use pimalaya_stream::std::tls::upgrade_tls;
use secrecy::SecretString;
use url::Url;
use crate::jmap::{account::JmapAccount, session::JmapAuth};
use crate::jmap::{client::JmapClient, session::JmapAuth};
/// Export a raw RFC 5322 message to stdout (Email/get + blob download).
///
@@ -21,13 +21,11 @@ pub struct JmapEmailExportCommand {
}
impl JmapEmailExportCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let tls = account.backend.tls.clone().try_into()?;
let auth: JmapAuth = account.backend.auth.clone().try_into()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let tls = client.config.tls.clone().try_into()?;
let auth: JmapAuth = client.config.auth.clone().try_into()?;
let http_auth: SecretString = auth.into();
let mut client = account.new_jmap_client()?;
let properties = Some(vec!["id".to_owned(), "blobId".to_owned()]);
let output = client.email_get(vec![self.id.clone()], properties, false, false, 0)?;
@@ -61,7 +59,7 @@ impl JmapEmailExportCommand {
let port = download_url.port_or_known_default().unwrap_or(443);
let tcp = TcpStream::connect((host, port))?;
let stream = upgrade_tls(host, tcp, &tls, &[b"http/1.1"])?;
let mut download_client = JmapClient::new(stream, http_auth);
let mut download_client = InnerJmapClient::new(stream, http_auth);
download_client.blob_download(&download_url)?
};
+4 -5
View File
@@ -3,7 +3,7 @@ use clap::Parser;
use log::warn;
use pimalaya_cli::printer::Printer;
use crate::jmap::{account::JmapAccount, email::query::EmailsTable};
use crate::jmap::{client::JmapClient, email::query::EmailsTable};
/// Get JMAP emails by ID (Email/get).
///
@@ -16,8 +16,7 @@ pub struct JmapEmailGetCommand {
}
impl JmapEmailGetCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let output = client.email_get(self.ids.clone(), None, false, false, 0)?;
for id in output.not_found {
@@ -25,8 +24,8 @@ impl JmapEmailGetCommand {
}
let table = EmailsTable {
preset: account.table_preset,
arrangement: account.table_arrangement,
preset: client.account.table_preset().to_string(),
arrangement: client.account.table_arrangement(),
emails: output.emails,
};
+7 -9
View File
@@ -7,15 +7,15 @@ use std::{
use anyhow::{bail, Result};
use clap::Parser;
use io_jmap::{
client::JmapClient,
client::JmapClient as InnerJmapClient,
rfc8621::{capabilities::MAIL, email::EmailImport},
};
use pimalaya_cli::printer::{Message, Printer};
use pimalaya_stream::tls::upgrade_tls;
use pimalaya_stream::std::tls::upgrade_tls;
use secrecy::SecretString;
use url::Url;
use crate::jmap::{account::JmapAccount, error::format_set_error, session::JmapAuth};
use crate::jmap::{client::JmapClient, error::format_set_error, session::JmapAuth};
/// Import an RFC 5322 message into a mailbox (upload + Email/import).
///
@@ -46,13 +46,11 @@ pub struct JmapEmailImportCommand {
}
impl JmapEmailImportCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let tls = account.backend.tls.clone().try_into()?;
let auth: JmapAuth = account.backend.auth.clone().try_into()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let tls = client.config.tls.clone().try_into()?;
let auth: JmapAuth = client.config.auth.clone().try_into()?;
let http_auth: SecretString = auth.into();
let mut client = account.new_jmap_client()?;
let data: Vec<u8> = if stdin().is_terminal() || printer.is_json() {
self.message
.join(" ")
@@ -85,7 +83,7 @@ impl JmapEmailImportCommand {
let port = upload_url.port_or_known_default().unwrap_or(443);
let tcp = TcpStream::connect((host, port))?;
let stream = upgrade_tls(host, tcp, &tls, &[b"http/1.1"])?;
let mut upload_client = JmapClient::new(stream, http_auth);
let mut upload_client = InnerJmapClient::new(stream, http_auth);
upload_client
.blob_upload(&upload_url, "message/rfc822", data)?
.blob_id
+2 -3
View File
@@ -4,7 +4,7 @@ use log::warn;
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::jmap::account::JmapAccount;
use crate::jmap::client::JmapClient;
/// Parse RFC 5322 message blobs without storing them (Email/parse).
///
@@ -18,8 +18,7 @@ pub struct JmapEmailParseCommand {
}
impl JmapEmailParseCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let output = client.email_parse(self.blob_ids.clone(), None)?;
for id in output.not_found {
+4 -6
View File
@@ -9,7 +9,7 @@ use io_jmap::rfc8621::email::{
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::jmap::account::JmapAccount;
use crate::jmap::client::JmapClient;
/// Query JMAP emails (Email/query + Email/get).
///
@@ -86,9 +86,7 @@ pub struct JmapEmailQueryCommand {
}
impl JmapEmailQueryCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let filter = {
let f = EmailFilter {
in_mailbox: self.mailbox,
@@ -148,8 +146,8 @@ impl JmapEmailQueryCommand {
)?;
let table = EmailsTable {
preset: account.table_preset,
arrangement: account.table_arrangement,
preset: client.account.table_preset().to_string(),
arrangement: client.account.table_arrangement(),
emails: output.emails,
};
+2 -3
View File
@@ -4,7 +4,7 @@ use io_jmap::rfc8621::email::EmailAddress;
use log::warn;
use pimalaya_cli::printer::{Message, Printer};
use crate::jmap::account::JmapAccount;
use crate::jmap::client::JmapClient;
/// Read the content of a JMAP email (Email/get with body).
///
@@ -21,8 +21,7 @@ pub struct JmapEmailReadCommand {
}
impl JmapEmailReadCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let output = client.email_get(self.ids.clone(), None, !self.html, self.html, 0)?;
for id in output.not_found {
+2 -3
View File
@@ -5,7 +5,7 @@ use clap::Parser;
use io_jmap::rfc8621::email_set::JmapEmailSetArgs;
use pimalaya_cli::printer::{Message, Printer};
use crate::jmap::{account::JmapAccount, error::format_set_error};
use crate::jmap::{client::JmapClient, error::format_set_error};
/// Update JMAP emails via patch operations (Email/set).
#[derive(Debug, Parser)]
@@ -40,8 +40,7 @@ pub struct JmapEmailUpdateCommand {
}
impl JmapEmailUpdateCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let mut args = JmapEmailSetArgs::default();
for id in &self.ids {
+6 -6
View File
@@ -3,7 +3,7 @@ use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::jmap::{
account::JmapAccount,
client::JmapClient,
identity::{
create::JmapIdentityCreateCommand, delete::JmapIdentityDeleteCommand,
get::JmapIdentityGetCommand, update::JmapIdentityUpdateCommand,
@@ -28,12 +28,12 @@ pub enum JmapIdentityCommand {
}
impl JmapIdentityCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
pub fn execute(self, printer: &mut impl Printer, client: JmapClient) -> Result<()> {
match self {
Self::Get(cmd) => cmd.execute(printer, account),
Self::Create(cmd) => cmd.execute(printer, account),
Self::Update(cmd) => cmd.execute(printer, account),
Self::Delete(cmd) => cmd.execute(printer, account),
Self::Get(cmd) => cmd.execute(printer, client),
Self::Create(cmd) => cmd.execute(printer, client),
Self::Update(cmd) => cmd.execute(printer, client),
Self::Delete(cmd) => cmd.execute(printer, client),
}
}
}
+2 -4
View File
@@ -3,7 +3,7 @@ use clap::Parser;
use io_jmap::rfc8621::{identity::IdentityCreate, identity_set::JmapIdentitySetArgs};
use pimalaya_cli::printer::{Message, Printer};
use crate::jmap::{account::JmapAccount, error::format_set_error};
use crate::jmap::{client::JmapClient, error::format_set_error};
/// Create a JMAP sender identity (Identity/set).
#[derive(Debug, Parser)]
@@ -24,9 +24,7 @@ pub struct JmapIdentityCreateCommand {
}
impl JmapIdentityCreateCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let identity = IdentityCreate {
name: self.name.clone(),
email: self.email.clone(),
+2 -3
View File
@@ -3,7 +3,7 @@ use clap::Parser;
use io_jmap::rfc8621::identity_set::JmapIdentitySetArgs;
use pimalaya_cli::printer::{Message, Printer};
use crate::jmap::{account::JmapAccount, error::format_set_error};
use crate::jmap::{client::JmapClient, error::format_set_error};
/// Delete a JMAP sender identity (Identity/set).
#[derive(Debug, Parser)]
@@ -14,8 +14,7 @@ pub struct JmapIdentityDeleteCommand {
}
impl JmapIdentityDeleteCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let mut args = JmapIdentitySetArgs::default();
for id in self.ids {
+3 -4
View File
@@ -8,7 +8,7 @@ use log::warn;
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::jmap::account::JmapAccount;
use crate::jmap::client::JmapClient;
/// Get JMAP identities (Identity/get).
///
@@ -22,8 +22,7 @@ pub struct JmapIdentityGetCommand {
}
impl JmapIdentityGetCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let output = client.identity_get(self.ids)?;
for id in output.not_found {
@@ -31,7 +30,7 @@ impl JmapIdentityGetCommand {
}
let table = IdentitiesTable {
preset: account.table_preset,
preset: client.account.table_preset().to_string(),
identities: output.identities,
};
+2 -4
View File
@@ -3,7 +3,7 @@ use clap::Parser;
use io_jmap::rfc8621::{identity::IdentityUpdate, identity_set::JmapIdentitySetArgs};
use pimalaya_cli::printer::{Message, Printer};
use crate::jmap::{account::JmapAccount, error::format_set_error};
use crate::jmap::{client::JmapClient, error::format_set_error};
/// Update a JMAP sender identity (Identity/set).
#[derive(Debug, Parser)]
@@ -25,9 +25,7 @@ pub struct JmapIdentityUpdateCommand {
}
impl JmapIdentityUpdateCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let patch = IdentityUpdate {
name: self.name,
reply_to: None,
+7 -7
View File
@@ -3,7 +3,7 @@ use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::jmap::{
account::JmapAccount,
client::JmapClient,
mailbox::{
create::JmapMailboxCreateCommand, destroy::JmapMailboxDestroyCommand,
get::JmapMailboxGetCommand, query::JmapMailboxQueryCommand,
@@ -24,13 +24,13 @@ pub enum JmapMailboxCommand {
}
impl JmapMailboxCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
pub fn execute(self, printer: &mut impl Printer, client: JmapClient) -> Result<()> {
match self {
Self::Get(cmd) => cmd.execute(printer, account),
Self::Query(cmd) => cmd.execute(printer, account),
Self::Create(cmd) => cmd.execute(printer, account),
Self::Update(cmd) => cmd.execute(printer, account),
Self::Destroy(cmd) => cmd.execute(printer, account),
Self::Get(cmd) => cmd.execute(printer, client),
Self::Query(cmd) => cmd.execute(printer, client),
Self::Create(cmd) => cmd.execute(printer, client),
Self::Update(cmd) => cmd.execute(printer, client),
Self::Destroy(cmd) => cmd.execute(printer, client),
}
}
}
+2 -4
View File
@@ -5,7 +5,7 @@ use clap::Parser;
use io_jmap::rfc8621::{mailbox::MailboxCreate, mailbox_set::JmapMailboxSetArgs};
use pimalaya_cli::printer::{Message, Printer};
use crate::jmap::{account::JmapAccount, error::format_set_error};
use crate::jmap::{client::JmapClient, error::format_set_error};
/// Create a JMAP mailbox.
#[derive(Debug, Parser)]
@@ -25,9 +25,7 @@ pub struct JmapMailboxCreateCommand {
}
impl JmapMailboxCreateCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let new_mailbox = MailboxCreate {
name: Some(self.name.clone()),
parent_id: self.parent_id,
+2 -4
View File
@@ -3,7 +3,7 @@ use clap::Parser;
use io_jmap::rfc8621::mailbox_set::JmapMailboxSetArgs;
use pimalaya_cli::printer::{Message, Printer};
use crate::jmap::{account::JmapAccount, error::format_set_error};
use crate::jmap::{client::JmapClient, error::format_set_error};
/// Delete a JMAP mailbox.
#[derive(Debug, Parser)]
@@ -18,9 +18,7 @@ pub struct JmapMailboxDestroyCommand {
}
impl JmapMailboxDestroyCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let mut args = JmapMailboxSetArgs::default();
args.destroy = Some(self.ids.clone());
args.on_destroy_remove_emails = if self.purge { Some(true) } else { None };
+3 -4
View File
@@ -3,7 +3,7 @@ use clap::Parser;
use log::warn;
use pimalaya_cli::printer::Printer;
use crate::jmap::{account::JmapAccount, mailbox::query::MailboxesTable};
use crate::jmap::{client::JmapClient, mailbox::query::MailboxesTable};
/// Get JMAP mailboxes by ID (Mailbox/get).
#[derive(Debug, Parser)]
@@ -14,8 +14,7 @@ pub struct JmapMailboxGetCommand {
}
impl JmapMailboxGetCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let output = client.mailbox_get(Some(self.ids.clone()), None)?;
for id in output.not_found {
@@ -23,7 +22,7 @@ impl JmapMailboxGetCommand {
}
let table = MailboxesTable {
preset: account.table_preset,
preset: client.account.table_preset().to_string(),
mailboxes: output.mailboxes,
};
+3 -5
View File
@@ -9,7 +9,7 @@ use io_jmap::rfc8621::mailbox::{
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::jmap::account::JmapAccount;
use crate::jmap::client::JmapClient;
/// Query JMAP mailboxes (Mailbox/query + Mailbox/get).
///
@@ -55,9 +55,7 @@ pub struct JmapMailboxQueryCommand {
}
impl JmapMailboxQueryCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let filter = {
let f = MailboxFilter {
parent_id: self.parent_id,
@@ -94,7 +92,7 @@ impl JmapMailboxQueryCommand {
)?;
let table = MailboxesTable {
preset: account.table_preset,
preset: client.account.table_preset().to_string(),
mailboxes: output.mailboxes,
};
+2 -4
View File
@@ -5,7 +5,7 @@ use clap::Parser;
use io_jmap::rfc8621::{mailbox::MailboxUpdate, mailbox_set::JmapMailboxSetArgs};
use pimalaya_cli::printer::{Message, Printer};
use crate::jmap::{account::JmapAccount, error::format_set_error, mailbox::query::RoleArg};
use crate::jmap::{client::JmapClient, error::format_set_error, mailbox::query::RoleArg};
/// Update a JMAP mailbox.
#[derive(Debug, Parser)]
@@ -40,9 +40,7 @@ pub struct JmapMailboxUpdateCommand {
}
impl JmapMailboxUpdateCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let is_subscribed = if self.subscribe {
Some(true)
} else if self.unsubscribe {
+1 -1
View File
@@ -1,5 +1,5 @@
pub mod account;
pub mod cli;
pub mod client;
pub mod email;
pub mod error;
pub mod identity;
+2 -4
View File
@@ -13,7 +13,7 @@ use pimalaya_cli::printer::Printer;
use serde::Serialize;
use serde_json::Value;
use crate::jmap::account::JmapAccount;
use crate::jmap::client::JmapClient;
/// Send a raw JMAP method-calls array and print the response.
///
@@ -37,9 +37,7 @@ pub struct JmapQueryCommand {
}
impl JmapQueryCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let raw = if self.method_calls.is_empty()
|| self.method_calls.first().map(|s| s.as_str()) == Some("-")
{
+2 -2
View File
@@ -15,8 +15,8 @@ use io_jmap::rfc8620::{
session_get::{JmapSessionGet, JmapSessionGetResult},
};
use log::info;
use pimalaya_stream::{
std::stream::Stream,
use pimalaya_stream::std::{
stream::Stream,
tls::{upgrade_tls, Tls},
};
use secrecy::{ExposeSecret, SecretString};
+2 -3
View File
@@ -2,7 +2,7 @@ use anyhow::{bail, Result};
use clap::Parser;
use pimalaya_cli::printer::{Message, Printer};
use crate::jmap::{account::JmapAccount, error::format_set_error};
use crate::jmap::{client::JmapClient, error::format_set_error};
/// Cancel (undo) a pending JMAP email submission (EmailSubmission/set).
///
@@ -16,8 +16,7 @@ pub struct JmapSubmissionCancelCommand {
}
impl JmapSubmissionCancelCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let output = client.email_submission_cancel(self.ids.clone())?;
if !output.not_updated.is_empty() {
+6 -6
View File
@@ -3,7 +3,7 @@ use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::jmap::{
account::JmapAccount,
client::JmapClient,
submission::{
cancel::JmapSubmissionCancelCommand, create::JmapSubmissionCreateCommand,
get::JmapSubmissionGetCommand, query::JmapSubmissionQueryCommand,
@@ -26,12 +26,12 @@ pub enum JmapSubmissionCommand {
}
impl JmapSubmissionCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
pub fn execute(self, printer: &mut impl Printer, client: JmapClient) -> Result<()> {
match self {
Self::Get(cmd) => cmd.execute(printer, account),
Self::Query(cmd) => cmd.execute(printer, account),
Self::Create(cmd) => cmd.execute(printer, account),
Self::Cancel(cmd) => cmd.execute(printer, account),
Self::Get(cmd) => cmd.execute(printer, client),
Self::Query(cmd) => cmd.execute(printer, client),
Self::Create(cmd) => cmd.execute(printer, client),
Self::Cancel(cmd) => cmd.execute(printer, client),
}
}
}
+3 -5
View File
@@ -8,7 +8,7 @@ use io_jmap::rfc8621::email_submission::{
use pimalaya_cli::printer::Printer;
use crate::jmap::{
account::JmapAccount, error::format_set_error, submission::query::SubmissionsTable,
client::JmapClient, error::format_set_error, submission::query::SubmissionsTable,
};
/// Submit a JMAP email for sending (EmailSubmission/set).
@@ -35,9 +35,7 @@ pub struct JmapSubmissionCreateCommand {
}
impl JmapSubmissionCreateCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let envelope = if let Some(mail_from_addr) = self.mail_from {
let rcpt_to = self
.rcpt_to
@@ -76,7 +74,7 @@ impl JmapSubmissionCreateCommand {
}
let table = SubmissionsTable {
preset: account.table_preset,
preset: client.account.table_preset().to_string(),
submissions: output.created.into_values().collect(),
};
+3 -4
View File
@@ -3,7 +3,7 @@ use clap::Parser;
use log::warn;
use pimalaya_cli::printer::Printer;
use crate::jmap::{account::JmapAccount, submission::query::SubmissionsTable};
use crate::jmap::{client::JmapClient, submission::query::SubmissionsTable};
/// Get JMAP email submissions by ID (EmailSubmission/get).
#[derive(Debug, Parser)]
@@ -14,8 +14,7 @@ pub struct JmapSubmissionGetCommand {
}
impl JmapSubmissionGetCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let output = client.email_submission_get(Some(self.ids.clone()))?;
for id in output.not_found {
@@ -23,7 +22,7 @@ impl JmapSubmissionGetCommand {
}
let table = SubmissionsTable {
preset: account.table_preset,
preset: client.account.table_preset().to_string(),
submissions: output.submissions,
};
+3 -5
View File
@@ -7,7 +7,7 @@ use io_jmap::rfc8621::email_submission::{EmailSubmission, EmailSubmissionFilter,
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::jmap::account::JmapAccount;
use crate::jmap::client::JmapClient;
/// CLI proxy for [`UndoStatus`].
#[derive(Clone, Debug, ValueEnum)]
@@ -52,9 +52,7 @@ pub struct JmapSubmissionQueryCommand {
}
impl JmapSubmissionQueryCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let filter = {
let f = EmailSubmissionFilter {
undo_status: self.undo_status.map(Into::into),
@@ -80,7 +78,7 @@ impl JmapSubmissionQueryCommand {
)?;
let table = SubmissionsTable {
preset: account.table_preset,
preset: client.account.table_preset().to_string(),
submissions: output.submissions,
};
+3 -3
View File
@@ -2,7 +2,7 @@ use anyhow::Result;
use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::jmap::{account::JmapAccount, thread::get::JmapThreadGetCommand};
use crate::jmap::{client::JmapClient, thread::get::JmapThreadGetCommand};
/// Manage JMAP threads.
#[derive(Debug, Subcommand)]
@@ -12,9 +12,9 @@ pub enum JmapThreadCommand {
}
impl JmapThreadCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
pub fn execute(self, printer: &mut impl Printer, client: JmapClient) -> Result<()> {
match self {
Self::Get(cmd) => cmd.execute(printer, account),
Self::Get(cmd) => cmd.execute(printer, client),
}
}
}
+3 -4
View File
@@ -8,7 +8,7 @@ use log::warn;
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::jmap::account::JmapAccount;
use crate::jmap::client::JmapClient;
/// Get JMAP threads by ID (Thread/get).
///
@@ -21,8 +21,7 @@ pub struct JmapThreadGetCommand {
}
impl JmapThreadGetCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let output = client.thread_get(self.ids.clone())?;
for id in output.not_found {
@@ -30,7 +29,7 @@ impl JmapThreadGetCommand {
}
printer.out(ThreadsTable {
preset: account.table_preset,
preset: client.account.table_preset().to_string(),
threads: output.threads,
})
}
+4 -4
View File
@@ -3,7 +3,7 @@ use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::jmap::{
account::JmapAccount,
client::JmapClient,
vacation::{get::JmapVacationGetCommand, set::JmapVacationSetCommand},
};
@@ -17,10 +17,10 @@ pub enum JmapVacationCommand {
}
impl JmapVacationCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
pub fn execute(self, printer: &mut impl Printer, client: JmapClient) -> Result<()> {
match self {
Self::Get(cmd) => cmd.execute(printer, account),
Self::Set(cmd) => cmd.execute(printer, account),
Self::Get(cmd) => cmd.execute(printer, client),
Self::Set(cmd) => cmd.execute(printer, client),
}
}
}
+3 -5
View File
@@ -7,16 +7,14 @@ use io_jmap::rfc8621::{capabilities::VACATION_RESPONSE, vacation_response::Vacat
use pimalaya_cli::printer::{Message, Printer};
use serde::Serialize;
use crate::jmap::account::JmapAccount;
use crate::jmap::client::JmapClient;
/// Get the JMAP vacation response (VacationResponse/get).
#[derive(Debug, Parser)]
pub struct JmapVacationGetCommand;
impl JmapVacationGetCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let has_vacation = client
.session()
.map(|s| s.capabilities.contains_key(VACATION_RESPONSE))
@@ -31,7 +29,7 @@ impl JmapVacationGetCommand {
};
let table = VacationTable {
preset: account.table_preset,
preset: client.account.table_preset().to_string(),
vacation,
};
+2 -4
View File
@@ -5,7 +5,7 @@ use io_jmap::rfc8621::{
};
use pimalaya_cli::printer::{Message, Printer};
use crate::jmap::account::JmapAccount;
use crate::jmap::client::JmapClient;
/// Update the JMAP vacation response (VacationResponse/set).
#[derive(Debug, Parser)]
@@ -40,9 +40,7 @@ pub struct JmapVacationSetCommand {
}
impl JmapVacationSetCommand {
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
let mut client = account.new_jmap_client()?;
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
let has_vacation = client
.session()
.map(|s| s.capabilities.contains_key(VACATION_RESPONSE))
-34
View File
@@ -1,34 +0,0 @@
use anyhow::Result;
use clap::Subcommand;
use pimalaya_cli::printer::Printer;
use crate::{
cli::BackendArg,
config::{AccountConfig, Config},
mailboxes::list::MailboxesListCommand,
};
/// Manage mailboxes through whichever backend the active account has
/// configured.
///
/// The active backend is selected by `--backend` (defaults to `auto`,
/// which picks the first configured backend in priority order).
#[derive(Debug, Subcommand)]
pub enum MailboxesCommand {
#[command(visible_alias = "ls")]
List(MailboxesListCommand),
}
impl MailboxesCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config: Config,
account_config: AccountConfig,
backend: BackendArg,
) -> Result<()> {
match self {
Self::List(cmd) => cmd.execute(printer, config, account_config, backend),
}
}
}
-34
View File
@@ -1,34 +0,0 @@
use anyhow::Result;
use clap::Parser;
use pimalaya_cli::printer::Printer;
use crate::{
cli::BackendArg,
config::{AccountConfig, Config},
email_client::build,
mailboxes::table::MailboxesTable,
};
/// List mailboxes for the active account, regardless of the underlying
/// backend (IMAP, JMAP or Maildir).
#[derive(Debug, Parser)]
pub struct MailboxesListCommand;
impl MailboxesListCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config: Config,
account_config: AccountConfig,
backend: BackendArg,
) -> Result<()> {
let mut ctx = build(config, account_config, backend)?;
let mailboxes = ctx.client.list_mailboxes()?;
printer.out(MailboxesTable {
preset: ctx.table_preset,
arrangement: ctx.table_arrangement,
mailboxes,
})
}
}
-45
View File
@@ -1,45 +0,0 @@
use std::fmt;
use comfy_table::{Cell, ContentArrangement, Row, Table};
use io_email::mailbox::Mailbox;
use serde::Serialize;
#[derive(Clone, Debug, Serialize)]
pub struct MailboxesTable {
#[serde(skip)]
pub preset: String,
#[serde(skip)]
pub arrangement: ContentArrangement,
pub mailboxes: Vec<Mailbox>,
}
impl fmt::Display for MailboxesTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new();
table
.load_preset(&self.preset)
.set_content_arrangement(self.arrangement.clone())
.set_header(Row::from([
Cell::new("ID"),
Cell::new("NAME"),
Cell::new("ROLE"),
Cell::new("ATTRIBUTES"),
]))
.add_rows(self.mailboxes.iter().map(|m| {
let mut row = Row::new();
row.max_height(1);
row.add_cell(Cell::new(&m.id));
row.add_cell(Cell::new(&m.name));
row.add_cell(match &m.role {
Some(role) => Cell::new(format!("{role:?}")),
None => Cell::new(""),
});
row.add_cell(Cell::new(m.attributes.join(", ")));
row
}));
writeln!(f)?;
writeln!(f, "{table}")
}
}

Some files were not shown because too many files have changed in this diff Show More