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 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)?;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
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