mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-15 20:07:57 +08:00
clean part 1
This commit is contained in:
Generated
+208
-18
@@ -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
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod check;
|
||||
pub mod cli;
|
||||
pub mod configure;
|
||||
pub mod context;
|
||||
pub mod list;
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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() })
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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,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,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"))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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"))
|
||||
|
||||
@@ -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,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,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,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"))
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -1,5 +1,5 @@
|
||||
pub mod account;
|
||||
pub mod cli;
|
||||
pub mod client;
|
||||
pub mod envelope;
|
||||
pub mod flag;
|
||||
pub mod id;
|
||||
|
||||
+3
-1
@@ -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;
|
||||
|
||||
@@ -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
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)?
|
||||
};
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -1,5 +1,5 @@
|
||||
pub mod account;
|
||||
pub mod cli;
|
||||
pub mod client;
|
||||
pub mod email;
|
||||
pub mod error;
|
||||
pub mod identity;
|
||||
|
||||
+2
-4
@@ -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
@@ -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,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() {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user