mirror of
https://github.com/soywod/himalaya.git
synced 2024-09-28 20:21:13 +00:00
wip: add message thread command
This commit is contained in:
parent
2eff215934
commit
6cbfc57c83
|
@ -3,7 +3,6 @@ pub(crate) mod wizard;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use petgraph::graphmap::DiGraphMap;
|
|
||||||
use std::{fmt::Display, ops::Deref, sync::Arc};
|
use std::{fmt::Display, ops::Deref, sync::Arc};
|
||||||
|
|
||||||
#[cfg(feature = "imap")]
|
#[cfg(feature = "imap")]
|
||||||
|
@ -719,6 +718,23 @@ impl Backend {
|
||||||
Ok(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 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<()> {
|
pub async fn add_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> {
|
||||||
let backend_kind = self.toml_account_config.add_flags_kind();
|
let backend_kind = self.toml_account_config.add_flags_kind();
|
||||||
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||||
|
|
|
@ -3,7 +3,7 @@ use clap::Parser;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor::{self, MoveToColumn},
|
cursor::{self, MoveToColumn},
|
||||||
style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor},
|
style::{Color, Print, ResetColor, SetForegroundColor},
|
||||||
terminal, ExecutableCommand,
|
terminal, ExecutableCommand,
|
||||||
};
|
};
|
||||||
use email::{
|
use email::{
|
||||||
|
@ -11,14 +11,10 @@ use email::{
|
||||||
backend::feature::BackendFeatureSource,
|
backend::feature::BackendFeatureSource,
|
||||||
email::search_query,
|
email::search_query,
|
||||||
envelope::{list::ListEnvelopesOptions, ThreadedEnvelope},
|
envelope::{list::ListEnvelopesOptions, ThreadedEnvelope},
|
||||||
search_query::{filter::SearchEmailsFilterQuery, SearchEmailsQuery},
|
search_query::SearchEmailsQuery,
|
||||||
};
|
|
||||||
use petgraph::{graphmap::DiGraphMap, visit::IntoNodeIdentifiers, Direction};
|
|
||||||
use std::{
|
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
io::Write,
|
|
||||||
process::exit,
|
|
||||||
};
|
};
|
||||||
|
use petgraph::graphmap::DiGraphMap;
|
||||||
|
use std::{io::Write, process::exit};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
#[cfg(feature = "account-sync")]
|
#[cfg(feature = "account-sync")]
|
||||||
|
@ -37,19 +33,6 @@ pub struct ThreadEnvelopesCommand {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub folder: FolderNameOptionalFlag,
|
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")]
|
#[cfg(feature = "account-sync")]
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub cache: CacheDisableFlag,
|
pub cache: CacheDisableFlag,
|
||||||
|
@ -57,103 +40,16 @@ pub struct ThreadEnvelopesCommand {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub account: AccountNameFlag,
|
pub account: AccountNameFlag,
|
||||||
|
|
||||||
/// The maximum width the table should not exceed.
|
/// Show only threads that contain the given envelope identifier.
|
||||||
///
|
#[arg(long, short)]
|
||||||
/// This argument will force the table not to exceed the given
|
pub id: Option<usize>,
|
||||||
/// 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>,
|
|
||||||
|
|
||||||
/// 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)]
|
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
|
||||||
pub query: Option<Vec<String>>,
|
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 {
|
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");
|
info!("executing thread envelopes command");
|
||||||
|
|
||||||
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
let (toml_account_config, account_config) = config.clone().into_account_configs(
|
||||||
|
@ -163,11 +59,6 @@ impl ThreadEnvelopesCommand {
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let folder = &self.folder.name;
|
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 thread_envelopes_kind = toml_account_config.thread_envelopes_kind();
|
||||||
|
|
||||||
let backend = Backend::new(
|
let backend = Backend::new(
|
||||||
|
@ -205,12 +96,15 @@ impl ThreadEnvelopesCommand {
|
||||||
};
|
};
|
||||||
|
|
||||||
let opts = ListEnvelopesOptions {
|
let opts = ListEnvelopesOptions {
|
||||||
page,
|
page: 0,
|
||||||
page_size,
|
page_size: 0,
|
||||||
query,
|
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();
|
let mut stdout = std::io::stdout();
|
||||||
write_tree(
|
write_tree(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
159
src/email/message/command/thread.rs
Normal file
159
src/email/message/command/thread.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue