Merge branch 'thread'

Replaced `imap` crate by `imap-{types,codec,flow,client}`, and added
thread support for IMAP and Maildir.
This commit is contained in:
Clément DOUIN 2024-05-29 10:55:54 +02:00
commit f9b92e6e7a
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
50 changed files with 1023 additions and 541 deletions

58
Cargo.lock generated
View file

@ -1006,7 +1006,10 @@ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
"crossterm_winapi", "crossterm_winapi",
"libc", "libc",
"mio",
"parking_lot 0.12.1", "parking_lot 0.12.1",
"signal-hook",
"signal-hook-mio",
"winapi", "winapi",
] ]
@ -1396,7 +1399,7 @@ dependencies = [
[[package]] [[package]]
name = "email-lib" name = "email-lib"
version = "0.24.1" version = "0.24.1"
source = "git+https://git.sr.ht/~soywod/pimalaya#033ba2a2e193769e1272c9493aa1d6c975346eb5" source = "git+https://git.sr.ht/~soywod/pimalaya#a28e746a634c066f4b9b0b15cd6f742fa530164d"
dependencies = [ dependencies = [
"advisory-lock", "advisory-lock",
"async-ctrlc", "async-ctrlc",
@ -1425,6 +1428,7 @@ dependencies = [
"once_cell", "once_cell",
"ouroboros", "ouroboros",
"paste", "paste",
"petgraph",
"pgp-lib", "pgp-lib",
"process-lib", "process-lib",
"rayon", "rayon",
@ -2063,6 +2067,7 @@ dependencies = [
"color-eyre", "color-eyre",
"comfy-table", "comfy-table",
"console", "console",
"crossterm 0.27.0",
"dirs 4.0.0", "dirs 4.0.0",
"email-lib", "email-lib",
"email_address", "email_address",
@ -2074,6 +2079,7 @@ dependencies = [
"mml-lib", "mml-lib",
"oauth-lib", "oauth-lib",
"once_cell", "once_cell",
"petgraph",
"process-lib", "process-lib",
"secret-lib", "secret-lib",
"serde", "serde",
@ -2081,7 +2087,6 @@ dependencies = [
"serde_json", "serde_json",
"shellexpand-utils", "shellexpand-utils",
"sled", "sled",
"termcolor",
"terminal_size 0.1.17", "terminal_size 0.1.17",
"tokio", "tokio",
"toml", "toml",
@ -2183,7 +2188,7 @@ dependencies = [
"httpdate", "httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.6", "socket2 0.4.10",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@ -2217,7 +2222,7 @@ dependencies = [
"iana-time-zone-haiku", "iana-time-zone-haiku",
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
"windows-core 0.52.0", "windows-core",
] ]
[[package]] [[package]]
@ -2267,12 +2272,13 @@ dependencies = [
[[package]] [[package]]
name = "imap-client" name = "imap-client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff" source = "git+https://github.com/soywod/imap-client.git#4533995f3ebe6efdb503128af15a867b60e48645"
dependencies = [ dependencies = [
"imap-flow", "imap-flow",
"imap-types",
"once_cell", "once_cell",
"rustls-native-certs 0.7.0", "rustls-native-certs 0.7.0",
"tasks", "tag-generator",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-rustls 0.26.0", "tokio-rustls 0.26.0",
@ -2282,7 +2288,7 @@ dependencies = [
[[package]] [[package]]
name = "imap-codec" name = "imap-codec"
version = "2.0.0" version = "2.0.0"
source = "git+https://github.com/duesee/imap-codec.git#d6b265fd01123334db2d48100537eb140932589c" source = "git+https://github.com/duesee/imap-codec.git#638924e92d9a8ea82208397d8e739110296daf01"
dependencies = [ dependencies = [
"abnf-core", "abnf-core",
"base64 0.21.7", "base64 0.21.7",
@ -2297,7 +2303,7 @@ dependencies = [
[[package]] [[package]]
name = "imap-flow" name = "imap-flow"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff" source = "git+https://github.com/soywod/imap-flow?branch=into-inner-stream#b705adbc03976367330f2b24e99a9623e5da3733"
dependencies = [ dependencies = [
"bounded-static", "bounded-static",
"bytes", "bytes",
@ -2313,7 +2319,7 @@ dependencies = [
[[package]] [[package]]
name = "imap-types" name = "imap-types"
version = "2.0.0" version = "2.0.0"
source = "git+https://github.com/duesee/imap-codec.git#d6b265fd01123334db2d48100537eb140932589c" source = "git+https://github.com/duesee/imap-codec.git#638924e92d9a8ea82208397d8e739110296daf01"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.21.7",
"bounded-static", "bounded-static",
@ -4577,7 +4583,7 @@ dependencies = [
[[package]] [[package]]
name = "tag-generator" name = "tag-generator"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff" source = "git+https://github.com/duesee/imap-flow#9ffda2b321247896b3f452072ccfd38789bb547a"
dependencies = [ dependencies = [
"imap-types", "imap-types",
"rand", "rand",
@ -4589,18 +4595,6 @@ version = "0.12.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
[[package]]
name = "tasks"
version = "0.1.0"
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff"
dependencies = [
"imap-flow",
"imap-types",
"tag-generator",
"thiserror",
"tracing",
]
[[package]] [[package]]
name = "tauri-winrt-notification" name = "tauri-winrt-notification"
version = "0.1.3" version = "0.1.3"
@ -4623,15 +4617,6 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "terminal_size" name = "terminal_size"
version = "0.1.17" version = "0.1.17"
@ -5244,7 +5229,7 @@ version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9"
dependencies = [ dependencies = [
"windows-core 0.51.1", "windows-core",
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
@ -5257,15 +5242,6 @@ dependencies = [
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"

View file

@ -54,6 +54,7 @@ clap_mangen = "0.2"
color-eyre = "0.6.3" color-eyre = "0.6.3"
comfy-table = "7.1.1" comfy-table = "7.1.1"
console = "0.15.2" console = "0.15.2"
crossterm = "0.27"
dirs = "4" dirs = "4"
email-lib = { version = "=0.24.1", default-features = false, features = ["derive", "tracing"] } email-lib = { version = "=0.24.1", default-features = false, features = ["derive", "tracing"] }
email_address = "0.2.4" email_address = "0.2.4"
@ -65,6 +66,7 @@ md5 = "0.7"
mml-lib = { version = "=1.0.12", default-features = false, features = ["derive"] } mml-lib = { version = "=1.0.12", default-features = false, features = ["derive"] }
oauth-lib = "=0.1.1" oauth-lib = "=0.1.1"
once_cell = "1.16" once_cell = "1.16"
petgraph = "0.6"
process-lib = { version = "=0.4.2", features = ["derive"] } process-lib = { version = "=0.4.2", features = ["derive"] }
secret-lib = { version = "=0.4.4", features = ["derive"] } secret-lib = { version = "=0.4.4", features = ["derive"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
@ -72,7 +74,6 @@ serde-toml-merge = "0.3"
serde_json = "1" serde_json = "1"
shellexpand-utils = "=0.2.1" shellexpand-utils = "=0.2.1"
sled = "=0.34.7" sled = "=0.34.7"
termcolor = "1"
terminal_size = "0.1" terminal_size = "0.1"
tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] } tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] }
toml = "0.8" toml = "0.8"
@ -85,9 +86,8 @@ url = "2.2"
uuid = { version = "0.8", features = ["v4"] } uuid = { version = "0.8", features = ["v4"] }
[patch.crates-io] [patch.crates-io]
# WIP: transition from `imap` to `imap-codec` # WIP: transition from `imap` to `imap-{types,codec,client}`
email-lib = { git = "https://git.sr.ht/~soywod/pimalaya" } email-lib = { git = "https://git.sr.ht/~soywod/pimalaya" }
imap-client = { git = "https://github.com/soywod/imap-flow.git", branch = "session" } imap-client = { git = "https://github.com/soywod/imap-client.git" }
tasks = { git = "https://github.com/soywod/imap-flow.git", branch = "session" }
imap-codec = { git = "https://github.com/duesee/imap-codec.git" } imap-codec = { git = "https://github.com/duesee/imap-codec.git" }
imap-types = { git = "https://github.com/duesee/imap-codec.git" } imap-types = { git = "https://github.com/duesee/imap-codec.git" }

View file

@ -24,7 +24,7 @@ impl AccountCheckUpCommand {
let account = self.account.name.as_ref().map(String::as_str); let account = self.account.name.as_ref().map(String::as_str);
printer.print_log("Checking configuration integrity…")?; printer.log("Checking configuration integrity…")?;
let (toml_account_config, account_config) = config.clone().into_account_configs( let (toml_account_config, account_config) = config.clone().into_account_configs(
account, account,
@ -33,7 +33,7 @@ impl AccountCheckUpCommand {
)?; )?;
let used_backends = toml_account_config.get_used_backends(); let used_backends = toml_account_config.get_used_backends();
printer.print_log("Checking backend context integrity…")?; printer.log("Checking backend context integrity…")?;
let ctx_builder = backend::BackendContextBuilder::new( let ctx_builder = backend::BackendContextBuilder::new(
toml_account_config.clone(), toml_account_config.clone(),
@ -46,7 +46,7 @@ impl AccountCheckUpCommand {
#[cfg(feature = "maildir")] #[cfg(feature = "maildir")]
{ {
printer.print_log("Checking Maildir integrity…")?; printer.log("Checking Maildir integrity…")?;
let maildir = ctx_builder let maildir = ctx_builder
.maildir .maildir
@ -61,7 +61,7 @@ impl AccountCheckUpCommand {
#[cfg(feature = "imap")] #[cfg(feature = "imap")]
{ {
printer.print_log("Checking IMAP integrity…")?; printer.log("Checking IMAP integrity…")?;
let imap = ctx_builder let imap = ctx_builder
.imap .imap
@ -76,7 +76,7 @@ impl AccountCheckUpCommand {
#[cfg(feature = "notmuch")] #[cfg(feature = "notmuch")]
{ {
printer.print_log("Checking Notmuch integrity…")?; printer.print("Checking Notmuch integrity…")?;
let notmuch = ctx_builder let notmuch = ctx_builder
.notmuch .notmuch
@ -91,7 +91,7 @@ impl AccountCheckUpCommand {
#[cfg(feature = "smtp")] #[cfg(feature = "smtp")]
{ {
printer.print_log("Checking SMTP integrity…")?; printer.log("Checking SMTP integrity…")?;
let smtp = ctx_builder let smtp = ctx_builder
.smtp .smtp
@ -106,7 +106,7 @@ impl AccountCheckUpCommand {
#[cfg(feature = "sendmail")] #[cfg(feature = "sendmail")]
{ {
printer.print_log("Checking Sendmail integrity…")?; printer.log("Checking Sendmail integrity…")?;
let sendmail = ctx_builder let sendmail = ctx_builder
.sendmail .sendmail
@ -119,6 +119,6 @@ impl AccountCheckUpCommand {
} }
} }
printer.print("Checkup successfully completed!") printer.out("Checkup successfully completed!")
} }
} }

View file

@ -105,7 +105,7 @@ impl AccountConfigureCommand {
.await?; .await?;
} }
printer.print(format!( printer.out(format!(
"Account {account} successfully {}configured!", "Account {account} successfully {}configured!",
if self.reset { "re" } else { "" } if self.reset { "re" } else { "" }
)) ))

View file

@ -2,7 +2,11 @@ use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use tracing::info; use tracing::info;
use crate::{account::Accounts, config::TomlConfig, printer::Printer}; use crate::{
account::{Accounts, AccountsTable},
config::TomlConfig,
printer::Printer,
};
/// List all accounts. /// List all accounts.
/// ///
@ -23,9 +27,10 @@ impl AccountListCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing list accounts command"); info!("executing list accounts command");
let accounts: Accounts = config.accounts.iter().into(); let accounts = Accounts::from(config.accounts.iter());
let table = AccountsTable::from(accounts).with_some_width(self.table_max_width);
printer.print_table(accounts, self.table_max_width)?; printer.out(table)?;
Ok(()) Ok(())
} }
} }

View file

@ -138,28 +138,28 @@ impl AccountSyncCommand {
let mut hunks_count = report.folder.patch.len(); let mut hunks_count = report.folder.patch.len();
if !report.folder.patch.is_empty() { if !report.folder.patch.is_empty() {
printer.print_log("Folders patch:")?; printer.log("Folders patch:")?;
for (hunk, _) in report.folder.patch { for (hunk, _) in report.folder.patch {
printer.print_log(format!(" - {hunk}"))?; printer.log(format!(" - {hunk}"))?;
} }
printer.print_log("")?; printer.log("")?;
} }
if !report.email.patch.is_empty() { if !report.email.patch.is_empty() {
printer.print_log("Envelopes patch:")?; printer.log("Envelopes patch:")?;
for (hunk, _) in report.email.patch { for (hunk, _) in report.email.patch {
hunks_count += 1; hunks_count += 1;
printer.print_log(format!(" - {hunk}"))?; printer.log(format!(" - {hunk}"))?;
} }
printer.print_log("")?; printer.log("")?;
} }
printer.print(format!( printer.out(format!(
"Estimated patch length for account {account_name} to be synchronized: {hunks_count}" "Estimated patch length for account {account_name} to be synchronized: {hunks_count}"
))?; ))?;
} else if printer.is_json() { } else if printer.is_json() {
sync_builder.sync().await?; sync_builder.sync().await?;
printer.print(format!("Account {account_name} successfully synchronized!"))?; printer.out(format!("Account {account_name} successfully synchronized!"))?;
} else { } else {
let multi = MultiProgress::new(); let multi = MultiProgress::new();
let sub_progresses = Mutex::new(HashMap::new()); let sub_progresses = Mutex::new(HashMap::new());
@ -239,11 +239,11 @@ impl AccountSyncCommand {
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err))) .filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if !folders_patch_err.is_empty() { if !folders_patch_err.is_empty() {
printer.print_log("")?; printer.log("")?;
printer.print_log("Errors occurred while applying the folders patch:")?; printer.log("Errors occurred while applying the folders patch:")?;
folders_patch_err folders_patch_err
.iter() .iter()
.try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?; .try_for_each(|(hunk, err)| printer.log(format!(" - {hunk}: {err}")))?;
} }
let envelopes_patch_err = report let envelopes_patch_err = report
@ -253,14 +253,14 @@ impl AccountSyncCommand {
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err))) .filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if !envelopes_patch_err.is_empty() { if !envelopes_patch_err.is_empty() {
printer.print_log("")?; printer.log("")?;
printer.print_log("Errors occurred while applying the envelopes patch:")?; printer.log("Errors occurred while applying the envelopes patch:")?;
for (hunk, err) in envelopes_patch_err { for (hunk, err) in envelopes_patch_err {
printer.print_log(format!(" - {hunk}: {err}"))?; printer.log(format!(" - {hunk}: {err}"))?;
} }
} }
printer.print(format!("Account {account_name} successfully synchronized!"))?; printer.out(format!("Account {account_name} successfully synchronized!"))?;
} }
Ok(()) Ok(())

View file

@ -142,6 +142,14 @@ impl TomlAccountConfig {
.or(self.backend.as_ref()) .or(self.backend.as_ref())
} }
pub fn thread_envelopes_kind(&self) -> Option<&BackendKind> {
self.envelope
.as_ref()
.and_then(|envelope| envelope.thread.as_ref())
.and_then(|thread| thread.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn watch_envelopes_kind(&self) -> Option<&BackendKind> { pub fn watch_envelopes_kind(&self) -> Option<&BackendKind> {
self.envelope self.envelope
.as_ref() .as_ref()

View file

@ -3,13 +3,10 @@ pub mod command;
pub mod config; pub mod config;
pub(crate) mod wizard; pub(crate) mod wizard;
use color_eyre::Result;
use comfy_table::{presets, Attribute, Cell, Color, ContentArrangement, Row, Table}; use comfy_table::{presets, Attribute, Cell, Color, ContentArrangement, Row, Table};
use serde::Serialize; use serde::{Serialize, Serializer};
use std::{collections::hash_map::Iter, fmt, ops::Deref}; use std::{collections::hash_map::Iter, fmt, ops::Deref};
use crate::printer::{PrintTable, WriteColor};
use self::config::TomlAccountConfig; use self::config::TomlAccountConfig;
/// Represents the printable account. /// Represents the printable account.
@ -31,6 +28,16 @@ impl Account {
default, default,
} }
} }
pub fn to_row(&self) -> Row {
let mut row = Row::new();
row.add_cell(Cell::new(&self.name).fg(Color::Green));
row.add_cell(Cell::new(&self.backend).fg(Color::Blue));
row.add_cell(Cell::new(if self.default { "yes" } else { "" }).fg(Color::White));
row
}
} }
impl fmt::Display for Account { impl fmt::Display for Account {
@ -39,28 +46,27 @@ impl fmt::Display for Account {
} }
} }
impl From<Account> for Row {
fn from(account: Account) -> Self {
let mut r = Row::new();
r.add_cell(Cell::new(account.name).fg(Color::Green));
r.add_cell(Cell::new(account.backend).fg(Color::Blue));
r.add_cell(Cell::new(if account.default { "yes" } else { "" }).fg(Color::White));
r
}
}
impl From<&Account> for Row {
fn from(account: &Account) -> Self {
let mut r = Row::new();
r.add_cell(Cell::new(&account.name).fg(Color::Green));
r.add_cell(Cell::new(&account.backend).fg(Color::Blue));
r.add_cell(Cell::new(if account.default { "yes" } else { "" }).fg(Color::White));
r
}
}
/// Represents the list of printable accounts. /// Represents the list of printable accounts.
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
pub struct Accounts(pub Vec<Account>); pub struct Accounts(Vec<Account>);
impl Accounts {
pub fn to_table(&self) -> Table {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("BACKENDS").add_attribute(Attribute::Reverse),
Cell::new("DEFAULT").add_attribute(Attribute::Reverse),
]))
.add_rows(self.iter().map(Account::to_row));
table
}
}
impl Deref for Accounts { impl Deref for Accounts {
type Target = Vec<Account>; type Target = Vec<Account>;
@ -70,51 +76,6 @@ impl Deref for Accounts {
} }
} }
impl From<Accounts> for Table {
fn from(accounts: Accounts) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("BACKENDS").add_attribute(Attribute::Reverse),
Cell::new("DEFAULT").add_attribute(Attribute::Reverse),
]))
.add_rows(accounts.0.into_iter().map(Row::from));
table
}
}
impl From<&Accounts> for Table {
fn from(accounts: &Accounts) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("BACKENDS").add_attribute(Attribute::Reverse),
Cell::new("DEFAULT").add_attribute(Attribute::Reverse),
]))
.add_rows(accounts.0.iter().map(Row::from));
table
}
}
impl PrintTable for Accounts {
fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option<u16>) -> Result<()> {
let mut table = Table::from(self);
if let Some(width) = table_max_width {
table.set_width(width);
}
writeln!(writer)?;
write!(writer, "{}", table)?;
writeln!(writer)?;
Ok(())
}
}
impl From<Iter<'_, String, TomlAccountConfig>> for Accounts { impl From<Iter<'_, String, TomlAccountConfig>> for Accounts {
fn from(map: Iter<'_, String, TomlAccountConfig>) -> Self { fn from(map: Iter<'_, String, TomlAccountConfig>) -> Self {
let mut accounts: Vec<_> = map let mut accounts: Vec<_> = map
@ -169,3 +130,48 @@ impl From<Iter<'_, String, TomlAccountConfig>> for Accounts {
Self(accounts) Self(accounts)
} }
} }
pub struct AccountsTable {
accounts: Accounts,
width: Option<u16>,
}
impl AccountsTable {
pub fn with_some_width(mut self, width: Option<u16>) -> Self {
self.width = width;
self
}
}
impl From<Accounts> for AccountsTable {
fn from(accounts: Accounts) -> Self {
Self {
accounts,
width: None,
}
}
}
impl fmt::Display for AccountsTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = self.accounts.to_table();
if let Some(width) = self.width {
table.set_width(width);
}
writeln!(f)?;
write!(f, "{table}")?;
writeln!(f)?;
Ok(())
}
}
impl Serialize for AccountsTable {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.accounts.serialize(serializer)
}
}

View file

@ -23,6 +23,7 @@ use email::{
envelope::{ envelope::{
get::GetEnvelope, get::GetEnvelope,
list::{ListEnvelopes, ListEnvelopesOptions}, list::{ListEnvelopes, ListEnvelopesOptions},
thread::ThreadEnvelopes,
watch::WatchEnvelopes, watch::WatchEnvelopes,
Id, SingleId, Id, SingleId,
}, },
@ -45,7 +46,11 @@ use email::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{account::config::TomlAccountConfig, cache::IdMapper, envelope::Envelopes}; use crate::{
account::config::TomlAccountConfig,
cache::IdMapper,
envelope::{Envelopes, ThreadedEnvelopes},
};
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
@ -337,6 +342,23 @@ impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
} }
} }
fn thread_envelopes(&self) -> Option<BackendFeature<Self::Context, dyn ThreadEnvelopes>> {
match self.toml_account_config.thread_envelopes_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.thread_envelopes_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.thread_envelopes()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.thread_envelopes_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.thread_envelopes_with_some(&self.notmuch),
_ => None,
}
}
fn watch_envelopes(&self) -> Option<BackendFeature<Self::Context, dyn WatchEnvelopes>> { fn watch_envelopes(&self) -> Option<BackendFeature<Self::Context, dyn WatchEnvelopes>> {
match self.toml_account_config.watch_envelopes_kind() { match self.toml_account_config.watch_envelopes_kind() {
#[cfg(feature = "imap")] #[cfg(feature = "imap")]
@ -683,7 +705,36 @@ impl Backend {
let id_mapper = self.build_id_mapper(folder, backend_kind)?; let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let envelopes = self.backend.list_envelopes(folder, opts).await?; let envelopes = self.backend.list_envelopes(folder, opts).await?;
let envelopes = let envelopes =
Envelopes::from_backend(&self.backend.account_config, &id_mapper, envelopes)?; Envelopes::try_from_backend(&self.backend.account_config, &id_mapper, envelopes)?;
Ok(envelopes)
}
pub async fn thread_envelopes(
&self,
folder: &str,
opts: ListEnvelopesOptions,
) -> Result<ThreadedEnvelopes> {
let backend_kind = self.toml_account_config.thread_envelopes_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let envelopes = self.backend.thread_envelopes(folder, opts).await?;
let envelopes = ThreadedEnvelopes::try_from_backend(&id_mapper, envelopes)?;
Ok(envelopes)
}
pub async fn thread_envelope(
&self,
folder: &str,
id: usize,
opts: ListEnvelopesOptions,
) -> Result<ThreadedEnvelopes> {
let backend_kind = self.toml_account_config.thread_envelopes_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let id = id_mapper.get_id(id)?;
let envelopes = self
.backend
.thread_envelope(folder, SingleId::from(id), opts)
.await?;
let envelopes = ThreadedEnvelopes::try_from_backend(&id_mapper, envelopes)?;
Ok(envelopes) Ok(envelopes)
} }

View file

@ -14,7 +14,7 @@ use crate::{
attachment::command::AttachmentSubcommand, command::MessageSubcommand, attachment::command::AttachmentSubcommand, command::MessageSubcommand,
template::command::TemplateSubcommand, template::command::TemplateSubcommand,
}, },
output::{ColorFmt, OutputFmt}, output::OutputFmt,
printer::Printer, printer::Printer,
}; };
@ -52,30 +52,6 @@ pub struct Cli {
#[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())] #[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())]
pub output: OutputFmt, pub output: OutputFmt,
/// Control when to use colors
///
/// The default setting is 'auto', which means himalaya will try
/// to guess when to use colors. For example, if himalaya is
/// printing to a terminal, then it will use colors, but if it is
/// redirected to a file or a pipe, then it will suppress color
/// output. himalaya will suppress color output in some other
/// circumstances as well. For example, if the TERM environment
/// variable is not set or set to 'dumb', then himalaya will not
/// use colors.
///
/// The possible values are:
///
/// - never: colors will never be used
///
/// - always: colors will always be used regardless of where output is sent
///
/// - ansi: like 'always', but emits ANSI escapes (even in a Windows console)
///
/// - auto: himalaya tries to be smart
#[arg(long, short = 'C', global = true)]
#[arg(value_name = "MODE", value_enum, default_value_t = Default::default())]
pub color: ColorFmt,
/// Enable logs with spantrace. /// Enable logs with spantrace.
/// ///
/// This is the same as running the command with `RUST_LOG=debug` /// This is the same as running the command with `RUST_LOG=debug`

View file

@ -244,6 +244,7 @@ impl TomlConfig {
}), }),
envelope: config.envelope.map(|c| EnvelopeConfig { envelope: config.envelope.map(|c| EnvelopeConfig {
list: c.list.map(|c| c.remote), list: c.list.map(|c| c.remote),
thread: c.thread.map(|c| c.remote),
watch: c.watch.map(|c| c.remote), watch: c.watch.map(|c| c.remote),
#[cfg(feature = "account-sync")] #[cfg(feature = "account-sync")]
sync: c.sync, sync: c.sync,

View file

@ -12,7 +12,7 @@ use tracing::info;
use crate::cache::arg::disable::CacheDisableFlag; use crate::cache::arg::disable::CacheDisableFlag;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig, account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag, printer::Printer, envelope::EnvelopesTable, folder::arg::name::FolderNameOptionalFlag, printer::Printer,
}; };
/// List all envelopes. /// List all envelopes.
@ -198,9 +198,9 @@ impl ListEnvelopesCommand {
}; };
let envelopes = backend.list_envelopes(folder, opts).await?; let envelopes = backend.list_envelopes(folder, opts).await?;
let table = EnvelopesTable::from(envelopes).with_some_width(self.table_max_width);
printer.print_table(envelopes, self.table_max_width)?; printer.out(table)?;
Ok(()) Ok(())
} }
} }

View file

@ -1,12 +1,15 @@
pub mod list; pub mod list;
pub mod thread;
pub mod watch; pub mod watch;
use color_eyre::Result;
use clap::Subcommand; use clap::Subcommand;
use color_eyre::Result;
use crate::{config::TomlConfig, printer::Printer}; use crate::{config::TomlConfig, printer::Printer};
use self::{list::ListEnvelopesCommand, watch::WatchEnvelopesCommand}; use self::{
list::ListEnvelopesCommand, thread::ThreadEnvelopesCommand, watch::WatchEnvelopesCommand,
};
/// Manage envelopes. /// Manage envelopes.
/// ///
@ -19,6 +22,9 @@ pub enum EnvelopeSubcommand {
#[command(alias = "lst")] #[command(alias = "lst")]
List(ListEnvelopesCommand), List(ListEnvelopesCommand),
#[command()]
Thread(ThreadEnvelopesCommand),
#[command()] #[command()]
Watch(WatchEnvelopesCommand), Watch(WatchEnvelopesCommand),
} }
@ -28,6 +34,7 @@ impl EnvelopeSubcommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self { match self {
Self::List(cmd) => cmd.execute(printer, config).await, Self::List(cmd) => cmd.execute(printer, config).await,
Self::Thread(cmd) => cmd.execute(printer, config).await,
Self::Watch(cmd) => cmd.execute(printer, config).await, Self::Watch(cmd) => cmd.execute(printer, config).await,
} }
} }

View file

@ -0,0 +1,197 @@
use ariadne::{Label, Report, ReportKind, Source};
use clap::Parser;
use color_eyre::Result;
use email::{
backend::feature::BackendFeatureSource, email::search_query,
envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery,
};
use std::process::exit;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
envelope::EnvelopesTree, folder::arg::name::FolderNameOptionalFlag, printer::Printer,
};
/// Thread all envelopes.
///
/// This command allows you to thread all envelopes included in the
/// given folder.
#[derive(Debug, Parser)]
pub struct ThreadEnvelopesCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
/// Show only threads that contain the given envelope identifier.
#[arg(long, short)]
pub id: Option<usize>,
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
pub query: Option<Vec<String>>,
}
impl ThreadEnvelopesCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing thread envelopes command");
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let folder = &self.folder.name;
let thread_envelopes_kind = toml_account_config.thread_envelopes_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config.clone(),
thread_envelopes_kind,
|builder| builder.set_thread_envelopes(BackendFeatureSource::Context),
)
.await?;
let query = self
.query
.map(|query| query.join(" ").parse::<SearchEmailsQuery>());
let query = match query {
None => None,
Some(Ok(query)) => Some(query),
Some(Err(main_err)) => {
let source = "query";
let search_query::error::Error::ParseError(errs, query) = &main_err;
for err in errs {
Report::build(ReportKind::Error, source, err.span().start)
.with_message(main_err.to_string())
.with_label(
Label::new((source, err.span().into_range()))
.with_message(err.reason().to_string())
.with_color(ariadne::Color::Red),
)
.finish()
.eprint((source, Source::from(&query)))
.unwrap();
}
exit(0)
}
};
let opts = ListEnvelopesOptions {
page: 0,
page_size: 0,
query,
};
let envelopes = match self.id {
Some(id) => backend.thread_envelope(folder, id, opts).await,
None => backend.thread_envelopes(folder, opts).await,
}?;
let tree = EnvelopesTree::new(account_config, envelopes);
printer.out(tree)?;
Ok(())
}
}
// #[cfg(test)]
// mod test {
// use email::{account::config::AccountConfig, envelope::ThreadedEnvelope};
// use petgraph::graphmap::DiGraphMap;
// use super::write_tree;
// macro_rules! e {
// ($id:literal) => {
// ThreadedEnvelope {
// id: $id,
// message_id: $id,
// from: "",
// subject: "",
// date: Default::default(),
// }
// };
// }
// #[test]
// fn tree_1() {
// let config = AccountConfig::default();
// let mut buf = Vec::new();
// let mut graph = DiGraphMap::new();
// graph.add_edge(e!("0"), e!("1"), 0);
// graph.add_edge(e!("0"), e!("2"), 0);
// graph.add_edge(e!("0"), e!("3"), 0);
// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap();
// let buf = String::from_utf8_lossy(&buf);
// let expected = "
// 0
// ├─ 1
// ├─ 2
// └─ 3
// ";
// assert_eq!(expected.trim_start(), buf)
// }
// #[test]
// fn tree_2() {
// let config = AccountConfig::default();
// let mut buf = Vec::new();
// let mut graph = DiGraphMap::new();
// graph.add_edge(e!("0"), e!("1"), 0);
// graph.add_edge(e!("1"), e!("2"), 1);
// graph.add_edge(e!("1"), e!("3"), 1);
// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap();
// let buf = String::from_utf8_lossy(&buf);
// let expected = "
// 0
// └─ 1
// ├─ 2
// └─ 3
// ";
// assert_eq!(expected.trim_start(), buf)
// }
// #[test]
// fn tree_3() {
// let config = AccountConfig::default();
// let mut buf = Vec::new();
// let mut graph = DiGraphMap::new();
// graph.add_edge(e!("0"), e!("1"), 0);
// graph.add_edge(e!("1"), e!("2"), 1);
// graph.add_edge(e!("2"), e!("22"), 2);
// graph.add_edge(e!("1"), e!("3"), 1);
// graph.add_edge(e!("0"), e!("4"), 0);
// graph.add_edge(e!("4"), e!("5"), 1);
// graph.add_edge(e!("5"), e!("6"), 2);
// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap();
// let buf = String::from_utf8_lossy(&buf);
// let expected = "
// 0
// ├─ 1
// │ ├─ 2
// │ │ └─ 22
// │ └─ 3
// └─ 4
// └─ 5
// └─ 6
// ";
// assert_eq!(expected.trim_start(), buf)
// }
// }

View file

@ -48,7 +48,7 @@ impl WatchEnvelopesCommand {
) )
.await?; .await?;
printer.print_log(format!( printer.out(format!(
"Start watching folder {folder} for envelopes changes…" "Start watching folder {folder} for envelopes changes…"
))?; ))?;

View file

@ -8,6 +8,7 @@ use crate::backend::BackendKind;
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct EnvelopeConfig { pub struct EnvelopeConfig {
pub list: Option<ListEnvelopesConfig>, pub list: Option<ListEnvelopesConfig>,
pub thread: Option<ThreadEnvelopesConfig>,
pub watch: Option<WatchEnvelopesConfig>, pub watch: Option<WatchEnvelopesConfig>,
pub get: Option<GetEnvelopeConfig>, pub get: Option<GetEnvelopeConfig>,
#[cfg(feature = "account-sync")] #[cfg(feature = "account-sync")]
@ -54,6 +55,26 @@ impl ListEnvelopesConfig {
} }
} }
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct ThreadEnvelopesConfig {
pub backend: Option<BackendKind>,
#[serde(flatten)]
pub remote: email::envelope::thread::config::EnvelopeThreadConfig,
}
impl ThreadEnvelopesConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct WatchEnvelopesConfig { pub struct WatchEnvelopesConfig {
pub backend: Option<BackendKind>, pub backend: Option<BackendKind>,

View file

@ -58,6 +58,6 @@ impl FlagAddCommand {
backend.add_flags(folder, &ids, &flags).await?; backend.add_flags(folder, &ids, &flags).await?;
printer.print(format!("Flag(s) {flags} successfully added!")) printer.out(format!("Flag(s) {flags} successfully added!"))
} }
} }

View file

@ -58,6 +58,6 @@ impl FlagRemoveCommand {
backend.remove_flags(folder, &ids, &flags).await?; backend.remove_flags(folder, &ids, &flags).await?;
printer.print(format!("Flag(s) {flags} successfully removed!")) printer.out(format!("Flag(s) {flags} successfully removed!"))
} }
} }

View file

@ -58,6 +58,6 @@ impl FlagSetCommand {
backend.set_flags(folder, &ids, &flags).await?; backend.set_flags(folder, &ids, &flags).await?;
printer.print(format!("Flag(s) {flags} successfully replaced!")) printer.out(format!("Flag(s) {flags} successfully replaced!"))
} }
} }

View file

@ -4,15 +4,16 @@ pub mod config;
pub mod flag; pub mod flag;
use color_eyre::Result; use color_eyre::Result;
use comfy_table::{presets, Attribute, Cell, Color, ContentArrangement, Row, Table}; use comfy_table::{presets, Attribute, Cell, ContentArrangement, Row, Table};
use email::account::config::AccountConfig; use crossterm::{cursor, style::Stylize, terminal};
use serde::Serialize; use email::{account::config::AccountConfig, envelope::ThreadedEnvelope};
use std::ops; use petgraph::graphmap::DiGraphMap;
use serde::{Serialize, Serializer};
use std::{collections::HashMap, fmt, ops::Deref, sync::Arc};
use crate::{ use crate::{
cache::IdMapper, cache::IdMapper,
flag::{Flag, Flags}, flag::{Flag, Flags},
printer::{PrintTable, WriteColor},
}; };
#[derive(Clone, Debug, Default, Serialize)] #[derive(Clone, Debug, Default, Serialize)]
@ -60,17 +61,17 @@ impl From<Envelope> for Row {
row.add_cell( row.add_cell(
Cell::new(envelope.id) Cell::new(envelope.id)
.add_attributes(all_attributes.clone()) .add_attributes(all_attributes.clone())
.fg(Color::Red), .fg(comfy_table::Color::Red),
) )
.add_cell( .add_cell(
Cell::new(flags) Cell::new(flags)
.add_attributes(all_attributes.clone()) .add_attributes(all_attributes.clone())
.fg(Color::White), .fg(comfy_table::Color::White),
) )
.add_cell( .add_cell(
Cell::new(envelope.subject) Cell::new(envelope.subject)
.add_attributes(all_attributes.clone()) .add_attributes(all_attributes.clone())
.fg(Color::Green), .fg(comfy_table::Color::Green),
) )
.add_cell( .add_cell(
Cell::new(if let Some(name) = envelope.from.name { Cell::new(if let Some(name) = envelope.from.name {
@ -79,12 +80,12 @@ impl From<Envelope> for Row {
envelope.from.addr envelope.from.addr
}) })
.add_attributes(all_attributes.clone()) .add_attributes(all_attributes.clone())
.fg(Color::Blue), .fg(comfy_table::Color::Blue),
) )
.add_cell( .add_cell(
Cell::new(envelope.date) Cell::new(envelope.date)
.add_attributes(all_attributes) .add_attributes(all_attributes)
.fg(Color::Yellow), .fg(comfy_table::Color::Yellow),
); );
row row
@ -121,17 +122,17 @@ impl From<&Envelope> for Row {
row.add_cell( row.add_cell(
Cell::new(&envelope.id) Cell::new(&envelope.id)
.add_attributes(all_attributes.clone()) .add_attributes(all_attributes.clone())
.fg(Color::Red), .fg(comfy_table::Color::Red),
) )
.add_cell( .add_cell(
Cell::new(flags) Cell::new(flags)
.add_attributes(all_attributes.clone()) .add_attributes(all_attributes.clone())
.fg(Color::White), .fg(comfy_table::Color::White),
) )
.add_cell( .add_cell(
Cell::new(&envelope.subject) Cell::new(&envelope.subject)
.add_attributes(all_attributes.clone()) .add_attributes(all_attributes.clone())
.fg(Color::Green), .fg(comfy_table::Color::Green),
) )
.add_cell( .add_cell(
Cell::new(if let Some(name) = &envelope.from.name { Cell::new(if let Some(name) = &envelope.from.name {
@ -140,62 +141,23 @@ impl From<&Envelope> for Row {
&envelope.from.addr &envelope.from.addr
}) })
.add_attributes(all_attributes.clone()) .add_attributes(all_attributes.clone())
.fg(Color::Blue), .fg(comfy_table::Color::Blue),
) )
.add_cell( .add_cell(
Cell::new(&envelope.date) Cell::new(&envelope.date)
.add_attributes(all_attributes) .add_attributes(all_attributes)
.fg(Color::Yellow), .fg(comfy_table::Color::Yellow),
); );
row row
} }
} }
/// Represents the list of envelopes.
#[derive(Clone, Debug, Default, Serialize)] #[derive(Clone, Debug, Default, Serialize)]
pub struct Envelopes(Vec<Envelope>); pub struct Envelopes(Vec<Envelope>);
impl From<Envelopes> for Table {
fn from(envelopes: Envelopes) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("ID").add_attribute(Attribute::Reverse),
Cell::new("FLAGS").add_attribute(Attribute::Reverse),
Cell::new("SUBJECT").add_attribute(Attribute::Reverse),
Cell::new("FROM").add_attribute(Attribute::Reverse),
Cell::new("DATE").add_attribute(Attribute::Reverse),
]))
.add_rows(envelopes.0.into_iter().map(Row::from));
table
}
}
impl From<&Envelopes> for Table {
fn from(envelopes: &Envelopes) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("ID").add_attribute(Attribute::Reverse),
Cell::new("FLAGS").add_attribute(Attribute::Reverse),
Cell::new("SUBJECT").add_attribute(Attribute::Reverse),
Cell::new("FROM").add_attribute(Attribute::Reverse),
Cell::new("DATE").add_attribute(Attribute::Reverse),
]))
.add_rows(envelopes.0.iter().map(Row::from));
table
}
}
impl Envelopes { impl Envelopes {
pub fn from_backend( pub fn try_from_backend(
config: &AccountConfig, config: &AccountConfig,
id_mapper: &IdMapper, id_mapper: &IdMapper,
envelopes: email::envelope::Envelopes, envelopes: email::envelope::Envelopes,
@ -222,9 +184,27 @@ impl Envelopes {
Ok(Envelopes(envelopes)) Ok(Envelopes(envelopes))
} }
pub fn to_table(&self) -> Table {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("ID").add_attribute(Attribute::Reverse),
Cell::new("FLAGS").add_attribute(Attribute::Reverse),
Cell::new("SUBJECT").add_attribute(Attribute::Reverse),
Cell::new("FROM").add_attribute(Attribute::Reverse),
Cell::new("DATE").add_attribute(Attribute::Reverse),
]))
.add_rows(self.iter().map(Row::from));
table
}
} }
impl ops::Deref for Envelopes { impl Deref for Envelopes {
type Target = Vec<Envelope>; type Target = Vec<Envelope>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
@ -232,15 +212,230 @@ impl ops::Deref for Envelopes {
} }
} }
impl PrintTable for Envelopes { pub struct EnvelopesTable {
fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option<u16>) -> Result<()> { envelopes: Envelopes,
let mut table = Table::from(self); width: Option<u16>,
if let Some(width) = table_max_width { }
impl EnvelopesTable {
pub fn with_some_width(mut self, width: Option<u16>) -> Self {
self.width = width;
self
}
}
impl From<Envelopes> for EnvelopesTable {
fn from(envelopes: Envelopes) -> Self {
Self {
envelopes,
width: None,
}
}
}
impl fmt::Display for EnvelopesTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = self.envelopes.to_table();
if let Some(width) = self.width {
table.set_width(width); table.set_width(width);
} }
writeln!(writer)?;
write!(writer, "{}", table)?; writeln!(f)?;
writeln!(writer)?; write!(f, "{table}")?;
writeln!(f)?;
Ok(()) Ok(())
} }
} }
impl Serialize for EnvelopesTable {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.envelopes.serialize(serializer)
}
}
pub struct ThreadedEnvelopes(email::envelope::ThreadedEnvelopes);
impl ThreadedEnvelopes {
pub fn try_from_backend(
id_mapper: &IdMapper,
envelopes: email::envelope::ThreadedEnvelopes,
) -> Result<ThreadedEnvelopes> {
let prev_edges = envelopes
.graph()
.all_edges()
.map(|(a, b, w)| {
let a = id_mapper.get_or_create_alias(&a.id)?;
let b = id_mapper.get_or_create_alias(&b.id)?;
Ok((a, b, *w))
})
.collect::<Result<Vec<_>>>()?;
let envelopes = envelopes
.map()
.iter()
.map(|(_, envelope)| {
let id = id_mapper.get_or_create_alias(&envelope.id)?;
let envelope = email::envelope::Envelope {
id: id.clone(),
message_id: envelope.message_id.clone(),
in_reply_to: envelope.in_reply_to.clone(),
flags: envelope.flags.clone(),
subject: envelope.subject.clone(),
from: envelope.from.clone(),
to: envelope.to.clone(),
date: envelope.date.clone(),
};
Ok((id, envelope))
})
.collect::<Result<HashMap<_, _>>>()?;
let envelopes = email::envelope::ThreadedEnvelopes::build(envelopes, move |envelopes| {
let mut graph = DiGraphMap::<ThreadedEnvelope, u8>::new();
for (a, b, w) in prev_edges.clone() {
let eb = envelopes.get(&b).unwrap();
match envelopes.get(&a) {
Some(ea) => {
graph.add_edge(ea.as_threaded(), eb.as_threaded(), w);
}
None => {
let ea = ThreadedEnvelope {
id: "0",
message_id: "0",
subject: "",
from: "",
date: Default::default(),
};
graph.add_edge(ea, eb.as_threaded(), w);
}
}
}
graph
});
Ok(ThreadedEnvelopes(envelopes))
}
}
impl Deref for ThreadedEnvelopes {
type Target = email::envelope::ThreadedEnvelopes;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct EnvelopesTree {
config: Arc<AccountConfig>,
envelopes: ThreadedEnvelopes,
}
impl EnvelopesTree {
pub fn new(config: Arc<AccountConfig>, envelopes: ThreadedEnvelopes) -> Self {
Self { config, envelopes }
}
pub fn fmt(
f: &mut fmt::Formatter,
config: &AccountConfig,
graph: &DiGraphMap<ThreadedEnvelope<'_>, u8>,
parent: ThreadedEnvelope<'_>,
pad: String,
weight: u8,
) -> fmt::Result {
let edges = graph
.all_edges()
.filter_map(|(a, b, w)| {
if a == parent && *w == weight {
Some(b)
} else {
None
}
})
.collect::<Vec<_>>();
if parent.id == "0" {
f.write_str("root")?;
} else {
write!(f, "{}{}", parent.id.red(), ") ".dark_grey())?;
if !parent.subject.is_empty() {
write!(f, "{} ", parent.subject.green())?;
}
if !parent.from.is_empty() {
let left = "<".dark_grey();
let right = ">".dark_grey();
write!(f, "{left}{}{right}", parent.from.blue())?;
}
let date = parent.format_date(config);
let cursor_date_begin_col = terminal::size().unwrap().0 - date.len() as u16;
let dots =
"·".repeat((cursor_date_begin_col - cursor::position().unwrap().0 - 2) as usize);
write!(f, " {} {}", dots.dark_grey(), date.dark_yellow())?;
}
writeln!(f)?;
let edges_count = edges.len();
for (i, b) in edges.into_iter().enumerate() {
let is_last = edges_count == i + 1;
let (x, y) = if is_last {
(' ', '└')
} else {
('│', '├')
};
write!(f, "{pad}{y}─ ")?;
let pad = format!("{pad}{x} ");
Self::fmt(f, config, graph, b, pad, weight + 1)?;
}
Ok(())
}
}
impl fmt::Display for EnvelopesTree {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
EnvelopesTree::fmt(
f,
&self.config,
self.envelopes.0.graph(),
ThreadedEnvelope {
id: "0",
message_id: "0",
from: "",
subject: "",
date: Default::default(),
},
String::new(),
0,
)
}
}
impl Serialize for EnvelopesTree {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.envelopes.0.serialize(serializer)
}
}
impl Deref for EnvelopesTree {
type Target = ThreadedEnvelopes;
fn deref(&self) -> &Self::Target {
&self.envelopes
}
}

View file

@ -67,13 +67,13 @@ impl AttachmentDownloadCommand {
let attachments = email.attachments()?; let attachments = email.attachments()?;
if attachments.is_empty() { if attachments.is_empty() {
printer.print_log(format!("No attachment found for message {id}!"))?; printer.log(format!("No attachment found for message {id}!"))?;
continue; continue;
} else { } else {
emails_count += 1; emails_count += 1;
} }
printer.print_log(format!( printer.log(format!(
"{} attachment(s) found for message {id}!", "{} attachment(s) found for message {id}!",
attachments.len() attachments.len()
))?; ))?;
@ -84,7 +84,7 @@ impl AttachmentDownloadCommand {
.unwrap_or_else(|| Uuid::new_v4().to_string()) .unwrap_or_else(|| Uuid::new_v4().to_string())
.into(); .into();
let filepath = account_config.get_download_file_path(&filename)?; let filepath = account_config.get_download_file_path(&filename)?;
printer.print_log(format!("Downloading {:?}", filepath))?; printer.log(format!("Downloading {:?}", filepath))?;
fs::write(&filepath, &attachment.body) fs::write(&filepath, &attachment.body)
.with_context(|| format!("cannot save attachment at {filepath:?}"))?; .with_context(|| format!("cannot save attachment at {filepath:?}"))?;
attachments_count += 1; attachments_count += 1;
@ -92,9 +92,9 @@ impl AttachmentDownloadCommand {
} }
match attachments_count { match attachments_count {
0 => printer.print("No attachment found!"), 0 => printer.out("No attachment found!"),
1 => printer.print("Downloaded 1 attachment!"), 1 => printer.out("Downloaded 1 attachment!"),
n => printer.print(format!( n => printer.out(format!(
"Downloaded {} attachment(s) from {} messages(s)!", "Downloaded {} attachment(s) from {} messages(s)!",
n, emails_count, n, emails_count,
)), )),

View file

@ -60,7 +60,7 @@ impl MessageCopyCommand {
backend.copy_messages(source, target, ids).await?; backend.copy_messages(source, target, ids).await?;
printer.print(format!( printer.out(format!(
"Message(s) successfully copied from {source} to {target}!" "Message(s) successfully copied from {source} to {target}!"
)) ))
} }

View file

@ -58,6 +58,6 @@ impl MessageDeleteCommand {
backend.delete_messages(folder, ids).await?; backend.delete_messages(folder, ids).await?;
printer.print(format!("Message(s) successfully removed from {folder}!")) printer.out(format!("Message(s) successfully removed from {folder}!"))
} }
} }

View file

@ -7,10 +7,11 @@ pub mod read;
pub mod reply; pub mod reply;
pub mod save; pub mod save;
pub mod send; pub mod send;
pub mod thread;
pub mod write; pub mod write;
use color_eyre::Result;
use clap::Subcommand; use clap::Subcommand;
use color_eyre::Result;
use crate::{config::TomlConfig, printer::Printer}; use crate::{config::TomlConfig, printer::Printer};
@ -18,7 +19,7 @@ use self::{
copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand, copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand,
mailto::MessageMailtoCommand, r#move::MessageMoveCommand, read::MessageReadCommand, mailto::MessageMailtoCommand, r#move::MessageMoveCommand, read::MessageReadCommand,
reply::MessageReplyCommand, save::MessageSaveCommand, send::MessageSendCommand, reply::MessageReplyCommand, save::MessageSaveCommand, send::MessageSendCommand,
write::MessageWriteCommand, thread::MessageThreadCommand, write::MessageWriteCommand,
}; };
/// Manage messages. /// Manage messages.
@ -32,6 +33,9 @@ pub enum MessageSubcommand {
#[command(arg_required_else_help = true)] #[command(arg_required_else_help = true)]
Read(MessageReadCommand), Read(MessageReadCommand),
#[command(arg_required_else_help = true)]
Thread(MessageThreadCommand),
#[command(aliases = ["add", "create", "new", "compose"])] #[command(aliases = ["add", "create", "new", "compose"])]
Write(MessageWriteCommand), Write(MessageWriteCommand),
@ -66,6 +70,7 @@ impl MessageSubcommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self { match self {
Self::Read(cmd) => cmd.execute(printer, config).await, Self::Read(cmd) => cmd.execute(printer, config).await,
Self::Thread(cmd) => cmd.execute(printer, config).await,
Self::Write(cmd) => cmd.execute(printer, config).await, Self::Write(cmd) => cmd.execute(printer, config).await,
Self::Reply(cmd) => cmd.execute(printer, config).await, Self::Reply(cmd) => cmd.execute(printer, config).await,
Self::Forward(cmd) => cmd.execute(printer, config).await, Self::Forward(cmd) => cmd.execute(printer, config).await,

View file

@ -61,7 +61,7 @@ impl MessageMoveCommand {
backend.move_messages(source, target, ids).await?; backend.move_messages(source, target, ids).await?;
printer.print(format!( printer.out(format!(
"Message(s) successfully moved from {source} to {target}!" "Message(s) successfully moved from {source} to {target}!"
)) ))
} }

View file

@ -139,6 +139,6 @@ impl MessageReadCommand {
glue = "\n\n"; glue = "\n\n";
} }
printer.print(bodies) printer.out(bodies)
} }
} }

View file

@ -68,6 +68,6 @@ impl MessageSaveCommand {
backend.add_message(folder, msg.as_bytes()).await?; backend.add_message(folder, msg.as_bytes()).await?;
printer.print(format!("Message successfully saved to {folder}!")) printer.out(format!("Message successfully saved to {folder}!"))
} }
} }

View file

@ -68,6 +68,6 @@ impl MessageSendCommand {
backend.send_message_then_save_copy(msg.as_bytes()).await?; backend.send_message_then_save_copy(msg.as_bytes()).await?;
printer.print("Message successfully sent!") printer.out("Message successfully sent!")
} }
} }

View file

@ -0,0 +1,159 @@
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use mml::message::FilterParts;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::envelope::arg::ids::EnvelopeIdArg;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
};
/// Thread a message.
///
/// This command allows you to thread a message. When threading a message,
/// the "seen" flag is automatically applied to the corresponding
/// envelope. To prevent this behaviour, use the --preview flag.
#[derive(Debug, Parser)]
pub struct MessageThreadCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelope: EnvelopeIdArg,
/// Thread the message without applying the "seen" flag to its
/// corresponding envelope.
#[arg(long, short)]
pub preview: bool,
/// Thread the raw version of the given message.
///
/// The raw message represents the headers and the body as it is
/// on the backend, unedited: not decoded nor decrypted. This is
/// useful for debugging faulty messages, but also for
/// saving/sending/transfering messages.
#[arg(long, short)]
#[arg(conflicts_with = "no_headers")]
#[arg(conflicts_with = "headers")]
pub raw: bool,
/// Thread only body of text/html parts.
///
/// This argument is useful when you need to thread the HTML version
/// of a message. Combined with --no-headers, you can write it to
/// a .html file and open it with your favourite browser.
#[arg(long)]
#[arg(conflicts_with = "raw")]
pub html: bool,
/// Thread only the body of the message.
///
/// All headers will be removed from the message.
#[arg(long)]
#[arg(conflicts_with = "raw")]
#[arg(conflicts_with = "headers")]
pub no_headers: bool,
/// List of headers that should be visible at the top of the
/// message.
///
/// If a given header is not found in the message, it will not be
/// visible. If no header is given, defaults to the one set up in
/// your TOML configuration file.
#[arg(long = "header", short = 'H', value_name = "NAME")]
#[arg(conflicts_with = "raw")]
#[arg(conflicts_with = "no_headers")]
pub headers: Vec<String>,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageThreadCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing thread message(s) command");
let folder = &self.folder.name;
let id = &self.envelope.id;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let get_messages_kind = toml_account_config.get_messages_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config.clone(),
get_messages_kind,
|builder| {
builder.set_thread_envelopes(BackendFeatureSource::Context);
builder.set_get_messages(BackendFeatureSource::Context);
},
)
.await?;
let envelopes = backend
.thread_envelope(folder, *id, Default::default())
.await?;
let ids: Vec<_> = envelopes
.graph()
.nodes()
.map(|e| e.id.parse::<usize>().unwrap())
.collect();
let emails = if self.preview {
backend.peek_messages(folder, &ids).await
} else {
backend.get_messages(folder, &ids).await
}?;
let mut glue = "";
let mut bodies = String::default();
for (i, email) in emails.to_vec().iter().enumerate() {
bodies.push_str(glue);
bodies.push_str(&format!("-------- Message {} --------\n\n", ids[i + 1]));
if self.raw {
// emails do not always have valid utf8, uses "lossy" to
// display what can be displayed
bodies.push_str(&String::from_utf8_lossy(email.raw()?));
} else {
let tpl = email
.to_read_tpl(&account_config, |mut tpl| {
if self.no_headers {
tpl = tpl.with_hide_all_headers();
} else if !self.headers.is_empty() {
tpl = tpl.with_show_only_headers(&self.headers);
}
if self.html {
tpl = tpl.with_filter_parts(FilterParts::Only("text/html".into()));
}
tpl
})
.await?;
bodies.push_str(&tpl);
}
glue = "\n\n";
}
printer.out(bodies)
}
}

View file

@ -76,6 +76,6 @@ impl TemplateForwardCommand {
.build() .build()
.await?; .await?;
printer.print(tpl) printer.out(tpl)
} }
} }

View file

@ -81,6 +81,6 @@ impl TemplateReplyCommand {
.build() .build()
.await?; .await?;
printer.print(tpl) printer.out(tpl)
} }
} }

View file

@ -80,6 +80,6 @@ impl TemplateSaveCommand {
backend.add_message(folder, &msg).await?; backend.add_message(folder, &msg).await?;
printer.print(format!("Template successfully saved to {folder}!")) printer.out(format!("Template successfully saved to {folder}!"))
} }
} }

View file

@ -79,6 +79,6 @@ impl TemplateSendCommand {
backend.send_message_then_save_copy(&msg).await?; backend.send_message_then_save_copy(&msg).await?;
printer.print("Message successfully sent!") printer.out("Message successfully sent!")
} }
} }

View file

@ -47,6 +47,6 @@ impl TemplateWriteCommand {
.build() .build()
.await?; .await?;
printer.print(tpl) printer.out(tpl)
} }
} }

View file

@ -1,14 +1,2 @@
pub mod arg; pub mod arg;
pub mod command; pub mod command;
use color_eyre::Result;
use email::template::Template;
use crate::printer::{Print, WriteColor};
impl Print for Template {
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
self.as_str().print(writer)?;
Ok(writer.reset()?)
}
}

View file

@ -50,6 +50,6 @@ impl AddFolderCommand {
backend.add_folder(folder).await?; backend.add_folder(folder).await?;
printer.print(format!("Folder {folder} successfully created!")) printer.log(format!("Folder {folder} successfully created!"))
} }
} }

View file

@ -60,6 +60,6 @@ impl FolderDeleteCommand {
backend.delete_folder(folder).await?; backend.delete_folder(folder).await?;
printer.print(format!("Folder {folder} successfully deleted!")) printer.log(format!("Folder {folder} successfully deleted!"))
} }
} }

View file

@ -51,6 +51,6 @@ impl FolderExpungeCommand {
backend.expunge_folder(folder).await?; backend.expunge_folder(folder).await?;
printer.print(format!("Folder {folder} successfully expunged!")) printer.log(format!("Folder {folder} successfully expunged!"))
} }
} }

View file

@ -6,7 +6,10 @@ use tracing::info;
#[cfg(feature = "account-sync")] #[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag; use crate::cache::arg::disable::CacheDisableFlag;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig, folder::Folders, account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
folder::{Folders, FoldersTable},
printer::Printer, printer::Printer,
}; };
@ -51,9 +54,10 @@ impl FolderListCommand {
) )
.await?; .await?;
let folders: Folders = backend.list_folders().await?.into(); let folders = Folders::from(backend.list_folders().await?);
let table = FoldersTable::from(folders).with_some_width(self.table_max_width);
printer.print_table(folders, self.table_max_width)?; printer.log(table)?;
Ok(()) Ok(())
} }
} }

View file

@ -60,6 +60,6 @@ impl FolderPurgeCommand {
backend.purge_folder(folder).await?; backend.purge_folder(folder).await?;
printer.print(format!("Folder {folder} successfully purged!")) printer.log(format!("Folder {folder} successfully purged!"))
} }
} }

View file

@ -2,12 +2,9 @@ pub mod arg;
pub mod command; pub mod command;
pub mod config; pub mod config;
use color_eyre::Result; use comfy_table::{presets, Attribute, Cell, Row, Table};
use comfy_table::{presets, Attribute, Cell, ContentArrangement, Row, Table}; use serde::{Serialize, Serializer};
use serde::Serialize; use std::{fmt, ops::Deref};
use std::ops;
use crate::printer::{PrintTable, WriteColor};
#[derive(Clone, Debug, Default, Serialize)] #[derive(Clone, Debug, Default, Serialize)]
pub struct Folder { pub struct Folder {
@ -15,67 +12,46 @@ pub struct Folder {
pub desc: String, pub desc: String,
} }
impl From<&email::folder::Folder> for Folder { impl Folder {
fn from(folder: &email::folder::Folder) -> Self { pub fn to_row(&self) -> Row {
let mut row = Row::new();
row.add_cell(Cell::new(&self.name).fg(comfy_table::Color::Blue));
row.add_cell(Cell::new(&self.desc).fg(comfy_table::Color::Green));
row
}
}
impl From<email::folder::Folder> for Folder {
fn from(folder: email::folder::Folder) -> Self {
Folder { Folder {
name: folder.name.clone(), name: folder.name,
desc: folder.desc.clone(), desc: folder.desc,
} }
} }
} }
impl From<&Folder> for Row {
fn from(folder: &Folder) -> Self {
let mut row = Row::new();
row.add_cell(Cell::new(&folder.name).fg(comfy_table::Color::Blue));
row.add_cell(Cell::new(&folder.desc).fg(comfy_table::Color::Green));
row
}
}
impl From<Folder> for Row {
fn from(folder: Folder) -> Self {
let mut row = Row::new();
row.add_cell(Cell::new(folder.name).fg(comfy_table::Color::Blue));
row.add_cell(Cell::new(folder.desc).fg(comfy_table::Color::Green));
row
}
}
#[derive(Clone, Debug, Default, Serialize)] #[derive(Clone, Debug, Default, Serialize)]
pub struct Folders(Vec<Folder>); pub struct Folders(Vec<Folder>);
impl From<Folders> for Table { impl Folders {
fn from(folders: Folders) -> Self { pub fn to_table(&self) -> Table {
let mut table = Table::new(); let mut table = Table::new();
table table
.load_preset(presets::NOTHING) .load_preset(presets::NOTHING)
.set_header(Row::from([ .set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse), Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("DESC").add_attribute(Attribute::Reverse), Cell::new("DESC").add_attribute(Attribute::Reverse),
])) ]))
.add_rows(folders.0.into_iter().map(Row::from)); .add_rows(self.iter().map(Folder::to_row));
table table
} }
} }
impl From<&Folders> for Table { impl Deref for Folders {
fn from(folders: &Folders) -> Self {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(Row::from([
Cell::new("NAME").add_attribute(Attribute::Reverse),
Cell::new("DESC").add_attribute(Attribute::Reverse),
]))
.add_rows(folders.0.iter().map(Row::from));
table
}
}
impl ops::Deref for Folders {
type Target = Vec<Folder>; type Target = Vec<Folder>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
@ -85,19 +61,51 @@ impl ops::Deref for Folders {
impl From<email::folder::Folders> for Folders { impl From<email::folder::Folders> for Folders {
fn from(folders: email::folder::Folders) -> Self { fn from(folders: email::folder::Folders) -> Self {
Folders(folders.iter().map(Folder::from).collect()) Folders(folders.into_iter().map(Folder::from).collect())
} }
} }
impl PrintTable for Folders { pub struct FoldersTable {
fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option<u16>) -> Result<()> { folders: Folders,
let mut table = Table::from(self); width: Option<u16>,
if let Some(width) = table_max_width { }
impl FoldersTable {
pub fn with_some_width(mut self, width: Option<u16>) -> Self {
self.width = width;
self
}
}
impl From<Folders> for FoldersTable {
fn from(folders: Folders) -> Self {
Self {
folders,
width: None,
}
}
}
impl fmt::Display for FoldersTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = self.folders.to_table();
if let Some(width) = self.width {
table.set_width(width); table.set_width(width);
} }
writeln!(writer)?;
write!(writer, "{}", table)?; writeln!(f)?;
writeln!(writer)?; write!(f, "{table}")?;
writeln!(f)?;
Ok(()) Ok(())
} }
} }
impl Serialize for FoldersTable {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.folders.serialize(serializer)
}
}

View file

@ -36,7 +36,7 @@ async fn main() -> Result<()> {
} }
let cli = Cli::parse(); let cli = Cli::parse();
let mut printer = StdoutPrinter::new(cli.output, cli.color); let mut printer = StdoutPrinter::new(cli.output);
let mut res = match cli.command { let mut res = match cli.command {
Some(cmd) => cmd.execute(&mut printer, cli.config_paths.as_ref()).await, Some(cmd) => cmd.execute(&mut printer, cli.config_paths.as_ref()).await,
None => { None => {

View file

@ -33,7 +33,7 @@ impl ManualGenerateCommand {
Man::new(cmd).render(&mut buffer)?; Man::new(cmd).render(&mut buffer)?;
fs::create_dir_all(&self.dir)?; fs::create_dir_all(&self.dir)?;
printer.print_log(format!("Generating man page for command {cmd_name}"))?; printer.log(format!("Generating man page for command {cmd_name}"))?;
fs::write(self.dir.join(format!("{}.1", cmd_name)), buffer)?; fs::write(self.dir.join(format!("{}.1", cmd_name)), buffer)?;
for subcmd in subcmds { for subcmd in subcmds {
@ -42,14 +42,14 @@ impl ManualGenerateCommand {
let mut buffer = Vec::new(); let mut buffer = Vec::new();
Man::new(subcmd).render(&mut buffer)?; Man::new(subcmd).render(&mut buffer)?;
printer.print_log(format!("Generating man page for subcommand {subcmd_name}"))?; printer.log(format!("Generating man page for subcommand {subcmd_name}"))?;
fs::write( fs::write(
self.dir.join(format!("{}-{}.1", cmd_name, subcmd_name)), self.dir.join(format!("{}-{}.1", cmd_name, subcmd_name)),
buffer, buffer,
)?; )?;
} }
printer.print(format!( printer.log(format!(
"{subcmds_len} man page(s) successfully generated in {:?}!", "{subcmds_len} man page(s) successfully generated in {:?}!",
self.dir self.dir
))?; ))?;

View file

@ -1,4 +1,3 @@
pub mod args;
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
pub mod output; pub mod output;

View file

@ -1,12 +1,7 @@
use clap::ValueEnum; use clap::ValueEnum;
use color_eyre::{eyre::eyre, eyre::Error, Result}; use color_eyre::{eyre::eyre, eyre::Error, Result};
use serde::Serialize; use serde::Serialize;
use std::{ use std::{fmt, str::FromStr};
fmt,
io::{self, IsTerminal},
str::FromStr,
};
use termcolor::ColorChoice;
/// Represents the available output formats. /// Represents the available output formats.
#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, ValueEnum)] #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
@ -49,59 +44,3 @@ impl<T: Serialize> OutputJson<T> {
Self { response } Self { response }
} }
} }
/// Represent the available color configs.
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, ValueEnum)]
pub enum ColorFmt {
Never,
Always,
Ansi,
#[default]
Auto,
}
impl FromStr for ColorFmt {
type Err = Error;
fn from_str(fmt: &str) -> Result<Self, Self::Err> {
match fmt {
fmt if fmt.eq_ignore_ascii_case("never") => Ok(Self::Never),
fmt if fmt.eq_ignore_ascii_case("always") => Ok(Self::Always),
fmt if fmt.eq_ignore_ascii_case("ansi") => Ok(Self::Ansi),
fmt if fmt.eq_ignore_ascii_case("auto") => Ok(Self::Auto),
unknown => Err(eyre!("cannot parse color format {}", unknown)),
}
}
}
impl From<ColorFmt> for ColorChoice {
fn from(fmt: ColorFmt) -> Self {
match fmt {
ColorFmt::Never => Self::Never,
ColorFmt::Always => Self::Always,
ColorFmt::Ansi => Self::AlwaysAnsi,
ColorFmt::Auto => {
if io::stdout().is_terminal() {
// Otherwise let's `termcolor` decide by
// inspecting the environment. From the [doc]:
//
// * If `NO_COLOR` is set to any value, then
// colors will be suppressed.
//
// * If `TERM` is set to dumb, then colors will be
// suppressed.
//
// * In non-Windows environments, if `TERM` is not
// set, then colors will be suppressed.
//
// [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection
Self::Auto
} else {
// Colors should be deactivated if the terminal is
// not a tty.
Self::Never
}
}
}
}
}

73
src/printer.rs Normal file
View file

@ -0,0 +1,73 @@
use color_eyre::{eyre::Context, Result};
use std::{
fmt,
io::{self, Write},
};
use crate::output::OutputFmt;
pub trait PrintTable {
fn print(&self, writer: &mut dyn io::Write, table_max_width: Option<u16>) -> Result<()>;
}
pub trait Printer {
fn out<T: fmt::Display + serde::Serialize>(&mut self, data: T) -> Result<()>;
fn log<T: fmt::Display + serde::Serialize>(&mut self, data: T) -> Result<()> {
self.out(data)
}
fn is_json(&self) -> bool {
false
}
}
pub struct StdoutPrinter {
stdout: io::Stdout,
stderr: io::Stderr,
output: OutputFmt,
}
impl StdoutPrinter {
pub fn new(output: OutputFmt) -> Self {
Self {
stdout: io::stdout(),
stderr: io::stderr(),
output,
}
}
}
impl Default for StdoutPrinter {
fn default() -> Self {
Self::new(Default::default())
}
}
impl Printer for StdoutPrinter {
fn out<T: fmt::Display + serde::Serialize>(&mut self, data: T) -> Result<()> {
match self.output {
OutputFmt::Plain => {
write!(self.stdout, "{data}")?;
}
OutputFmt::Json => {
serde_json::to_writer(&mut self.stdout, &data)
.context("cannot write json to writer")?;
}
};
Ok(())
}
fn log<T: fmt::Display + serde::Serialize>(&mut self, data: T) -> Result<()> {
if let OutputFmt::Plain = self.output {
write!(&mut self.stderr, "{data}")?;
}
Ok(())
}
fn is_json(&self) -> bool {
self.output == OutputFmt::Json
}
}

View file

@ -1,13 +0,0 @@
pub mod print;
#[allow(clippy::module_inception)]
pub mod printer;
use std::io;
pub use print::*;
pub use printer::*;
use termcolor::StandardStream;
pub trait WriteColor: io::Write + termcolor::WriteColor {}
impl WriteColor for StandardStream {}

View file

@ -1,21 +0,0 @@
use color_eyre::{eyre::Context, Result};
use crate::printer::WriteColor;
pub trait Print {
fn print(&self, writer: &mut dyn WriteColor) -> Result<()>;
}
impl Print for &str {
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
writeln!(writer, "{}", self).context("cannot write string to writer")?;
Ok(writer.reset()?)
}
}
impl Print for String {
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
self.as_str().print(writer)?;
Ok(writer.reset()?)
}
}

View file

@ -1,102 +0,0 @@
use clap::ArgMatches;
use color_eyre::{eyre::Context, Report, Result};
use std::fmt::Debug;
use termcolor::StandardStream;
use crate::{
output::{args, ColorFmt, OutputFmt},
printer::{Print, WriteColor},
};
pub trait PrintTable {
fn print_table(&self, writer: &mut dyn WriteColor, table_max_width: Option<u16>) -> Result<()>;
}
pub trait Printer {
// TODO: rename end
fn print<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()>;
// TODO: rename log
fn print_log<T: Debug + Print>(&mut self, data: T) -> Result<()>;
// TODO: rename table
fn print_table<T: Debug + PrintTable>(
&mut self,
data: T,
table_max_width: Option<u16>,
) -> Result<()>;
fn is_json(&self) -> bool;
}
pub struct StdoutPrinter {
pub writer: Box<dyn WriteColor>,
pub fmt: OutputFmt,
}
impl Default for StdoutPrinter {
fn default() -> Self {
let fmt = OutputFmt::default();
let writer = Box::new(StandardStream::stdout(ColorFmt::default().into()));
Self { fmt, writer }
}
}
impl StdoutPrinter {
pub fn new(fmt: OutputFmt, color: ColorFmt) -> Self {
let writer = Box::new(StandardStream::stdout(color.into()));
Self { fmt, writer }
}
}
impl Printer for StdoutPrinter {
fn print_log<T: Debug + Print>(&mut self, data: T) -> Result<()> {
match self.fmt {
OutputFmt::Plain => data.print(self.writer.as_mut()),
OutputFmt::Json => Ok(()),
}
}
fn print<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()> {
match self.fmt {
OutputFmt::Plain => data.print(self.writer.as_mut()),
OutputFmt::Json => serde_json::to_writer(self.writer.as_mut(), &data)
.context("cannot write json to writer"),
}
}
fn is_json(&self) -> bool {
self.fmt == OutputFmt::Json
}
fn print_table<T: Debug + PrintTable>(
&mut self,
data: T,
table_max_width: Option<u16>,
) -> Result<()> {
data.print_table(self.writer.as_mut(), table_max_width)
}
}
impl From<OutputFmt> for StdoutPrinter {
fn from(fmt: OutputFmt) -> Self {
Self::new(fmt, ColorFmt::Auto)
}
}
impl TryFrom<&ArgMatches> for StdoutPrinter {
type Error = Report;
fn try_from(m: &ArgMatches) -> Result<Self, Self::Error> {
let fmt: OutputFmt = m
.get_one::<String>(args::ARG_OUTPUT)
.map(String::as_str)
.unwrap()
.parse()?;
let color: ColorFmt = m
.get_one::<String>(args::ARG_COLOR)
.map(String::as_str)
.unwrap()
.parse()?;
Ok(Self::new(fmt, color))
}
}

View file

@ -80,7 +80,7 @@ pub async fn edit_tpl_with_editor<P: Printer>(
loop { loop {
match choice::post_edit() { match choice::post_edit() {
Ok(PostEditChoice::Send) => { Ok(PostEditChoice::Send) => {
printer.print_log("Sending email…")?; printer.log("Sending email…")?;
#[allow(unused_mut)] #[allow(unused_mut)]
let mut compiler = MmlCompilerBuilder::new(); let mut compiler = MmlCompilerBuilder::new();
@ -93,7 +93,7 @@ pub async fn edit_tpl_with_editor<P: Printer>(
backend.send_message_then_save_copy(&email).await?; backend.send_message_then_save_copy(&email).await?;
remove_local_draft()?; remove_local_draft()?;
printer.print("Done!")?; printer.log("Done!")?;
break; break;
} }
Ok(PostEditChoice::Edit) => { Ok(PostEditChoice::Edit) => {
@ -101,7 +101,7 @@ pub async fn edit_tpl_with_editor<P: Printer>(
continue; continue;
} }
Ok(PostEditChoice::LocalDraft) => { Ok(PostEditChoice::LocalDraft) => {
printer.print("Email successfully saved locally")?; printer.log("Email successfully saved locally")?;
break; break;
} }
Ok(PostEditChoice::RemoteDraft) => { Ok(PostEditChoice::RemoteDraft) => {
@ -121,7 +121,7 @@ pub async fn edit_tpl_with_editor<P: Printer>(
) )
.await?; .await?;
remove_local_draft()?; remove_local_draft()?;
printer.print("Email successfully saved to drafts")?; printer.log("Email successfully saved to drafts")?;
break; break;
} }
Ok(PostEditChoice::Discard) => { Ok(PostEditChoice::Discard) => {