wip: add message thread command

This commit is contained in:
Clément DOUIN 2024-05-22 11:07:40 +02:00
parent 2eff215934
commit 6cbfc57c83
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
4 changed files with 197 additions and 123 deletions

View file

@ -3,7 +3,6 @@ pub(crate) mod wizard;
use async_trait::async_trait;
use color_eyre::Result;
use petgraph::graphmap::DiGraphMap;
use std::{fmt::Display, ops::Deref, sync::Arc};
#[cfg(feature = "imap")]
@ -719,6 +718,23 @@ impl Backend {
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 envelopes = self
.backend
.thread_envelope(folder, SingleId::from(id), opts)
.await?;
// let envelopes =
// Envelopes::from_backend(&self.backend.account_config, &id_mapper, envelopes)?;
Ok(envelopes)
}
pub async fn add_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> {
let backend_kind = self.toml_account_config.add_flags_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;

View file

@ -3,7 +3,7 @@ use clap::Parser;
use color_eyre::Result;
use crossterm::{
cursor::{self, MoveToColumn},
style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor},
style::{Color, Print, ResetColor, SetForegroundColor},
terminal, ExecutableCommand,
};
use email::{
@ -11,14 +11,10 @@ use email::{
backend::feature::BackendFeatureSource,
email::search_query,
envelope::{list::ListEnvelopesOptions, ThreadedEnvelope},
search_query::{filter::SearchEmailsFilterQuery, SearchEmailsQuery},
};
use petgraph::{graphmap::DiGraphMap, visit::IntoNodeIdentifiers, Direction};
use std::{
collections::{HashMap, HashSet},
io::Write,
process::exit,
search_query::SearchEmailsQuery,
};
use petgraph::graphmap::DiGraphMap;
use std::{io::Write, process::exit};
use tracing::info;
#[cfg(feature = "account-sync")]
@ -37,19 +33,6 @@ pub struct ThreadEnvelopesCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
/// The page number.
///
/// The page number starts from 1 (which is the default). Giving a
/// page number to big will result in a out of bound error.
#[arg(long, short, value_name = "NUMBER", default_value = "1")]
pub page: usize,
/// The page size.
///
/// Determine the amount of envelopes a page should contain.
#[arg(long, short = 's', value_name = "NUMBER")]
pub page_size: Option<usize>,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
@ -57,103 +40,16 @@ pub struct ThreadEnvelopesCommand {
#[command(flatten)]
pub account: AccountNameFlag,
/// The maximum width the table should not exceed.
///
/// This argument will force the table not to exceed the given
/// width in pixels. Columns may shrink with ellipsis in order to
/// fit the width.
#[arg(long = "max-width", short = 'w')]
#[arg(name = "table_max_width", value_name = "PIXELS")]
pub table_max_width: Option<u16>,
/// Show only threads that contain the given envelope identifier.
#[arg(long, short)]
pub id: Option<usize>,
/// The thread envelopes filter and sort query.
///
/// The query can be a filter query, a sort query or both
/// together.
///
/// A filter query is composed of operators and conditions. There
/// is 3 operators and 8 conditions:
///
/// • not <condition> → filter envelopes that do not match the
/// condition
///
/// • <condition> and <condition> → filter envelopes that match
/// both conditions
///
/// • <condition> or <condition> → filter envelopes that match
/// one of the conditions
///
/// ◦ date <yyyy-mm-dd> → filter envelopes that match the given
/// date
///
/// ◦ before <yyyy-mm-dd> → filter envelopes with date strictly
/// before the given one
///
/// ◦ after <yyyy-mm-dd> → filter envelopes with date stricly
/// after the given one
///
/// ◦ from <pattern> → filter envelopes with senders matching the
/// given pattern
///
/// ◦ to <pattern> → filter envelopes with recipients matching
/// the given pattern
///
/// ◦ subject <pattern> → filter envelopes with subject matching
/// the given pattern
///
/// ◦ body <pattern> → filter envelopes with text bodies matching
/// the given pattern
///
/// ◦ flag <flag> → filter envelopes matching the given flag
///
/// A sort query starts by "order by", and is composed of kinds
/// and orders. There is 4 kinds and 2 orders:
///
/// • date [order] → sort envelopes by date
///
/// • from [order] → sort envelopes by sender
///
/// • to [order] → sort envelopes by recipient
///
/// • subject [order] → sort envelopes by subject
///
/// ◦ <kind> asc → sort envelopes by the given kind in ascending
/// order
///
/// ◦ <kind> desc → sort envelopes by the given kind in
/// descending order
///
/// Examples:
///
/// subject foo and body bar → filter envelopes containing "foo"
/// in their subject and "bar" in their text bodies
///
/// order by date desc subject → sort envelopes by descending date
/// (most recent first), then by ascending subject
///
/// subject foo and body bar order by date desc subject →
/// combination of the 2 previous examples
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
pub query: Option<Vec<String>>,
}
impl Default for ThreadEnvelopesCommand {
fn default() -> Self {
Self {
folder: Default::default(),
page: 1,
page_size: Default::default(),
#[cfg(feature = "account-sync")]
cache: Default::default(),
account: Default::default(),
query: Default::default(),
table_max_width: Default::default(),
}
}
}
impl ThreadEnvelopesCommand {
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 thread envelopes command");
let (toml_account_config, account_config) = config.clone().into_account_configs(
@ -163,11 +59,6 @@ impl ThreadEnvelopesCommand {
)?;
let folder = &self.folder.name;
let page = 1.max(self.page) - 1;
let page_size = self
.page_size
.unwrap_or_else(|| account_config.get_envelope_thread_page_size());
let thread_envelopes_kind = toml_account_config.thread_envelopes_kind();
let backend = Backend::new(
@ -205,12 +96,15 @@ impl ThreadEnvelopesCommand {
};
let opts = ListEnvelopesOptions {
page,
page_size,
page: 0,
page_size: 0,
query,
};
let envelopes = backend.thread_envelopes(folder, opts).await?;
let envelopes = match self.id {
Some(id) => backend.thread_envelope(folder, id, opts).await,
None => backend.thread_envelopes(folder, opts).await,
}?;
let mut stdout = std::io::stdout();
write_tree(

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

@ -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.print(bodies)
}
}