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

View file

@ -54,6 +54,7 @@ clap_mangen = "0.2"
color-eyre = "0.6.3"
comfy-table = "7.1.1"
console = "0.15.2"
crossterm = "0.27"
dirs = "4"
email-lib = { version = "=0.24.1", default-features = false, features = ["derive", "tracing"] }
email_address = "0.2.4"
@ -65,6 +66,7 @@ md5 = "0.7"
mml-lib = { version = "=1.0.12", default-features = false, features = ["derive"] }
oauth-lib = "=0.1.1"
once_cell = "1.16"
petgraph = "0.6"
process-lib = { version = "=0.4.2", features = ["derive"] }
secret-lib = { version = "=0.4.4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
@ -72,7 +74,6 @@ serde-toml-merge = "0.3"
serde_json = "1"
shellexpand-utils = "=0.2.1"
sled = "=0.34.7"
termcolor = "1"
terminal_size = "0.1"
tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] }
toml = "0.8"
@ -85,9 +86,8 @@ url = "2.2"
uuid = { version = "0.8", features = ["v4"] }
[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" }
imap-client = { git = "https://github.com/soywod/imap-flow.git", branch = "session" }
tasks = { git = "https://github.com/soywod/imap-flow.git", branch = "session" }
imap-client = { git = "https://github.com/soywod/imap-client.git" }
imap-codec = { 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);
printer.print_log("Checking configuration integrity…")?;
printer.log("Checking configuration integrity…")?;
let (toml_account_config, account_config) = config.clone().into_account_configs(
account,
@ -33,7 +33,7 @@ impl AccountCheckUpCommand {
)?;
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(
toml_account_config.clone(),
@ -46,7 +46,7 @@ impl AccountCheckUpCommand {
#[cfg(feature = "maildir")]
{
printer.print_log("Checking Maildir integrity…")?;
printer.log("Checking Maildir integrity…")?;
let maildir = ctx_builder
.maildir
@ -61,7 +61,7 @@ impl AccountCheckUpCommand {
#[cfg(feature = "imap")]
{
printer.print_log("Checking IMAP integrity…")?;
printer.log("Checking IMAP integrity…")?;
let imap = ctx_builder
.imap
@ -76,7 +76,7 @@ impl AccountCheckUpCommand {
#[cfg(feature = "notmuch")]
{
printer.print_log("Checking Notmuch integrity…")?;
printer.print("Checking Notmuch integrity…")?;
let notmuch = ctx_builder
.notmuch
@ -91,7 +91,7 @@ impl AccountCheckUpCommand {
#[cfg(feature = "smtp")]
{
printer.print_log("Checking SMTP integrity…")?;
printer.log("Checking SMTP integrity…")?;
let smtp = ctx_builder
.smtp
@ -106,7 +106,7 @@ impl AccountCheckUpCommand {
#[cfg(feature = "sendmail")]
{
printer.print_log("Checking Sendmail integrity…")?;
printer.log("Checking Sendmail integrity…")?;
let sendmail = ctx_builder
.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?;
}
printer.print(format!(
printer.out(format!(
"Account {account} successfully {}configured!",
if self.reset { "re" } else { "" }
))

View file

@ -2,7 +2,11 @@ use clap::Parser;
use color_eyre::Result;
use tracing::info;
use crate::{account::Accounts, config::TomlConfig, printer::Printer};
use crate::{
account::{Accounts, AccountsTable},
config::TomlConfig,
printer::Printer,
};
/// List all accounts.
///
@ -23,9 +27,10 @@ impl AccountListCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
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(())
}
}

View file

@ -138,28 +138,28 @@ impl AccountSyncCommand {
let mut hunks_count = report.folder.patch.len();
if !report.folder.patch.is_empty() {
printer.print_log("Folders patch:")?;
printer.log("Folders 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() {
printer.print_log("Envelopes patch:")?;
printer.log("Envelopes patch:")?;
for (hunk, _) in report.email.patch {
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}"
))?;
} else if printer.is_json() {
sync_builder.sync().await?;
printer.print(format!("Account {account_name} successfully synchronized!"))?;
printer.out(format!("Account {account_name} successfully synchronized!"))?;
} else {
let multi = MultiProgress::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)))
.collect::<Vec<_>>();
if !folders_patch_err.is_empty() {
printer.print_log("")?;
printer.print_log("Errors occurred while applying the folders patch:")?;
printer.log("")?;
printer.log("Errors occurred while applying the folders patch:")?;
folders_patch_err
.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
@ -253,14 +253,14 @@ impl AccountSyncCommand {
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
.collect::<Vec<_>>();
if !envelopes_patch_err.is_empty() {
printer.print_log("")?;
printer.print_log("Errors occurred while applying the envelopes patch:")?;
printer.log("")?;
printer.log("Errors occurred while applying the envelopes patch:")?;
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(())

View file

@ -142,6 +142,14 @@ impl TomlAccountConfig {
.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> {
self.envelope
.as_ref()

View file

@ -3,13 +3,10 @@ pub mod command;
pub mod config;
pub(crate) mod wizard;
use color_eyre::Result;
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 crate::printer::{PrintTable, WriteColor};
use self::config::TomlAccountConfig;
/// Represents the printable account.
@ -31,6 +28,16 @@ impl Account {
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 {
@ -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.
#[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 {
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 {
fn from(map: Iter<'_, String, TomlAccountConfig>) -> Self {
let mut accounts: Vec<_> = map
@ -169,3 +130,48 @@ impl From<Iter<'_, String, TomlAccountConfig>> for 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::{
get::GetEnvelope,
list::{ListEnvelopes, ListEnvelopesOptions},
thread::ThreadEnvelopes,
watch::WatchEnvelopes,
Id, SingleId,
},
@ -45,7 +46,11 @@ use email::{
};
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)]
#[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>> {
match self.toml_account_config.watch_envelopes_kind() {
#[cfg(feature = "imap")]
@ -683,7 +705,36 @@ impl Backend {
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let envelopes = self.backend.list_envelopes(folder, opts).await?;
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)
}

View file

@ -14,7 +14,7 @@ use crate::{
attachment::command::AttachmentSubcommand, command::MessageSubcommand,
template::command::TemplateSubcommand,
},
output::{ColorFmt, OutputFmt},
output::OutputFmt,
printer::Printer,
};
@ -52,30 +52,6 @@ pub struct Cli {
#[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())]
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.
///
/// 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 {
list: c.list.map(|c| c.remote),
thread: c.thread.map(|c| c.remote),
watch: c.watch.map(|c| c.remote),
#[cfg(feature = "account-sync")]
sync: c.sync,

View file

@ -12,7 +12,7 @@ use tracing::info;
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
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.
@ -198,9 +198,9 @@ impl ListEnvelopesCommand {
};
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(())
}
}

View file

@ -1,12 +1,15 @@
pub mod list;
pub mod thread;
pub mod watch;
use color_eyre::Result;
use clap::Subcommand;
use color_eyre::Result;
use crate::{config::TomlConfig, printer::Printer};
use self::{list::ListEnvelopesCommand, watch::WatchEnvelopesCommand};
use self::{
list::ListEnvelopesCommand, thread::ThreadEnvelopesCommand, watch::WatchEnvelopesCommand,
};
/// Manage envelopes.
///
@ -19,6 +22,9 @@ pub enum EnvelopeSubcommand {
#[command(alias = "lst")]
List(ListEnvelopesCommand),
#[command()]
Thread(ThreadEnvelopesCommand),
#[command()]
Watch(WatchEnvelopesCommand),
}
@ -28,6 +34,7 @@ impl EnvelopeSubcommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self {
Self::List(cmd) => cmd.execute(printer, config).await,
Self::Thread(cmd) => cmd.execute(printer, config).await,
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?;
printer.print_log(format!(
printer.out(format!(
"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)]
pub struct EnvelopeConfig {
pub list: Option<ListEnvelopesConfig>,
pub thread: Option<ThreadEnvelopesConfig>,
pub watch: Option<WatchEnvelopesConfig>,
pub get: Option<GetEnvelopeConfig>,
#[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)]
pub struct WatchEnvelopesConfig {
pub backend: Option<BackendKind>,

View file

@ -58,6 +58,6 @@ impl FlagAddCommand {
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?;
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?;
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;
use color_eyre::Result;
use comfy_table::{presets, Attribute, Cell, Color, ContentArrangement, Row, Table};
use email::account::config::AccountConfig;
use serde::Serialize;
use std::ops;
use comfy_table::{presets, Attribute, Cell, ContentArrangement, Row, Table};
use crossterm::{cursor, style::Stylize, terminal};
use email::{account::config::AccountConfig, envelope::ThreadedEnvelope};
use petgraph::graphmap::DiGraphMap;
use serde::{Serialize, Serializer};
use std::{collections::HashMap, fmt, ops::Deref, sync::Arc};
use crate::{
cache::IdMapper,
flag::{Flag, Flags},
printer::{PrintTable, WriteColor},
};
#[derive(Clone, Debug, Default, Serialize)]
@ -60,17 +61,17 @@ impl From<Envelope> for Row {
row.add_cell(
Cell::new(envelope.id)
.add_attributes(all_attributes.clone())
.fg(Color::Red),
.fg(comfy_table::Color::Red),
)
.add_cell(
Cell::new(flags)
.add_attributes(all_attributes.clone())
.fg(Color::White),
.fg(comfy_table::Color::White),
)
.add_cell(
Cell::new(envelope.subject)
.add_attributes(all_attributes.clone())
.fg(Color::Green),
.fg(comfy_table::Color::Green),
)
.add_cell(
Cell::new(if let Some(name) = envelope.from.name {
@ -79,12 +80,12 @@ impl From<Envelope> for Row {
envelope.from.addr
})
.add_attributes(all_attributes.clone())
.fg(Color::Blue),
.fg(comfy_table::Color::Blue),
)
.add_cell(
Cell::new(envelope.date)
.add_attributes(all_attributes)
.fg(Color::Yellow),
.fg(comfy_table::Color::Yellow),
);
row
@ -121,17 +122,17 @@ impl From<&Envelope> for Row {
row.add_cell(
Cell::new(&envelope.id)
.add_attributes(all_attributes.clone())
.fg(Color::Red),
.fg(comfy_table::Color::Red),
)
.add_cell(
Cell::new(flags)
.add_attributes(all_attributes.clone())
.fg(Color::White),
.fg(comfy_table::Color::White),
)
.add_cell(
Cell::new(&envelope.subject)
.add_attributes(all_attributes.clone())
.fg(Color::Green),
.fg(comfy_table::Color::Green),
)
.add_cell(
Cell::new(if let Some(name) = &envelope.from.name {
@ -140,62 +141,23 @@ impl From<&Envelope> for Row {
&envelope.from.addr
})
.add_attributes(all_attributes.clone())
.fg(Color::Blue),
.fg(comfy_table::Color::Blue),
)
.add_cell(
Cell::new(&envelope.date)
.add_attributes(all_attributes)
.fg(Color::Yellow),
.fg(comfy_table::Color::Yellow),
);
row
}
}
/// Represents the list of envelopes.
#[derive(Clone, Debug, Default, Serialize)]
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 {
pub fn from_backend(
pub fn try_from_backend(
config: &AccountConfig,
id_mapper: &IdMapper,
envelopes: email::envelope::Envelopes,
@ -222,9 +184,27 @@ impl 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>;
fn deref(&self) -> &Self::Target {
@ -232,15 +212,230 @@ impl ops::Deref for Envelopes {
}
}
impl PrintTable for Envelopes {
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 {
pub struct EnvelopesTable {
envelopes: Envelopes,
width: Option<u16>,
}
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);
}
writeln!(writer)?;
write!(writer, "{}", table)?;
writeln!(writer)?;
writeln!(f)?;
write!(f, "{table}")?;
writeln!(f)?;
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()?;
if attachments.is_empty() {
printer.print_log(format!("No attachment found for message {id}!"))?;
printer.log(format!("No attachment found for message {id}!"))?;
continue;
} else {
emails_count += 1;
}
printer.print_log(format!(
printer.log(format!(
"{} attachment(s) found for message {id}!",
attachments.len()
))?;
@ -84,7 +84,7 @@ impl AttachmentDownloadCommand {
.unwrap_or_else(|| Uuid::new_v4().to_string())
.into();
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)
.with_context(|| format!("cannot save attachment at {filepath:?}"))?;
attachments_count += 1;
@ -92,9 +92,9 @@ impl AttachmentDownloadCommand {
}
match attachments_count {
0 => printer.print("No attachment found!"),
1 => printer.print("Downloaded 1 attachment!"),
n => printer.print(format!(
0 => printer.out("No attachment found!"),
1 => printer.out("Downloaded 1 attachment!"),
n => printer.out(format!(
"Downloaded {} attachment(s) from {} messages(s)!",
n, emails_count,
)),

View file

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

View file

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

View file

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

View file

@ -139,6 +139,6 @@ impl MessageReadCommand {
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?;
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?;
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()
.await?;
printer.print(tpl)
printer.out(tpl)
}
}

View file

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

View file

@ -80,6 +80,6 @@ impl TemplateSaveCommand {
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?;
printer.print("Message successfully sent!")
printer.out("Message successfully sent!")
}
}

View file

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

View file

@ -1,14 +1,2 @@
pub mod arg;
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?;
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?;
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?;
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")]
use crate::cache::arg::disable::CacheDisableFlag;
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,
};
@ -51,9 +54,10 @@ impl FolderListCommand {
)
.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(())
}
}

View file

@ -60,6 +60,6 @@ impl FolderPurgeCommand {
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 config;
use color_eyre::Result;
use comfy_table::{presets, Attribute, Cell, ContentArrangement, Row, Table};
use serde::Serialize;
use std::ops;
use crate::printer::{PrintTable, WriteColor};
use comfy_table::{presets, Attribute, Cell, Row, Table};
use serde::{Serialize, Serializer};
use std::{fmt, ops::Deref};
#[derive(Clone, Debug, Default, Serialize)]
pub struct Folder {
@ -15,67 +12,46 @@ pub struct Folder {
pub desc: String,
}
impl From<&email::folder::Folder> for Folder {
fn from(folder: &email::folder::Folder) -> Self {
impl Folder {
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 {
name: folder.name.clone(),
desc: folder.desc.clone(),
name: folder.name,
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)]
pub struct Folders(Vec<Folder>);
impl From<Folders> for Table {
fn from(folders: Folders) -> Self {
impl Folders {
pub fn to_table(&self) -> Table {
let mut table = Table::new();
table
.load_preset(presets::NOTHING)
.set_header(Row::from([
Cell::new("NAME").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
}
}
impl From<&Folders> for Table {
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 {
impl Deref for Folders {
type Target = Vec<Folder>;
fn deref(&self) -> &Self::Target {
@ -85,19 +61,51 @@ impl ops::Deref for Folders {
impl From<email::folder::Folders> for Folders {
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 {
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 {
pub struct FoldersTable {
folders: Folders,
width: Option<u16>,
}
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);
}
writeln!(writer)?;
write!(writer, "{}", table)?;
writeln!(writer)?;
writeln!(f)?;
write!(f, "{table}")?;
writeln!(f)?;
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 mut printer = StdoutPrinter::new(cli.output, cli.color);
let mut printer = StdoutPrinter::new(cli.output);
let mut res = match cli.command {
Some(cmd) => cmd.execute(&mut printer, cli.config_paths.as_ref()).await,
None => {

View file

@ -33,7 +33,7 @@ impl ManualGenerateCommand {
Man::new(cmd).render(&mut buffer)?;
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)?;
for subcmd in subcmds {
@ -42,14 +42,14 @@ impl ManualGenerateCommand {
let mut buffer = Vec::new();
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(
self.dir.join(format!("{}-{}.1", cmd_name, subcmd_name)),
buffer,
)?;
}
printer.print(format!(
printer.log(format!(
"{subcmds_len} man page(s) successfully generated in {:?}!",
self.dir
))?;

View file

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

View file

@ -1,12 +1,7 @@
use clap::ValueEnum;
use color_eyre::{eyre::eyre, eyre::Error, Result};
use serde::Serialize;
use std::{
fmt,
io::{self, IsTerminal},
str::FromStr,
};
use termcolor::ColorChoice;
use std::{fmt, str::FromStr};
/// Represents the available output formats.
#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
@ -49,59 +44,3 @@ impl<T: Serialize> OutputJson<T> {
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 {
match choice::post_edit() {
Ok(PostEditChoice::Send) => {
printer.print_log("Sending email…")?;
printer.log("Sending email…")?;
#[allow(unused_mut)]
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?;
remove_local_draft()?;
printer.print("Done!")?;
printer.log("Done!")?;
break;
}
Ok(PostEditChoice::Edit) => {
@ -101,7 +101,7 @@ pub async fn edit_tpl_with_editor<P: Printer>(
continue;
}
Ok(PostEditChoice::LocalDraft) => {
printer.print("Email successfully saved locally")?;
printer.log("Email successfully saved locally")?;
break;
}
Ok(PostEditChoice::RemoteDraft) => {
@ -121,7 +121,7 @@ pub async fn edit_tpl_with_editor<P: Printer>(
)
.await?;
remove_local_draft()?;
printer.print("Email successfully saved to drafts")?;
printer.log("Email successfully saved to drafts")?;
break;
}
Ok(PostEditChoice::Discard) => {