diff --git a/src/backend/mod.rs b/src/backend/mod.rs index a549655..aeb47d6 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -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 { + 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)?; diff --git a/src/email/envelope/command/thread.rs b/src/email/envelope/command/thread.rs index cad69f0..a0c36d2 100644 --- a/src/email/envelope/command/thread.rs +++ b/src/email/envelope/command/thread.rs @@ -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, - #[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, + /// Show only threads that contain the given envelope identifier. + #[arg(long, short)] + pub id: Option, - /// 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 → filter envelopes that do not match the - /// condition - /// - /// • and → filter envelopes that match - /// both conditions - /// - /// • or → filter envelopes that match - /// one of the conditions - /// - /// ◦ date → filter envelopes that match the given - /// date - /// - /// ◦ before → filter envelopes with date strictly - /// before the given one - /// - /// ◦ after → filter envelopes with date stricly - /// after the given one - /// - /// ◦ from → filter envelopes with senders matching the - /// given pattern - /// - /// ◦ to → filter envelopes with recipients matching - /// the given pattern - /// - /// ◦ subject → filter envelopes with subject matching - /// the given pattern - /// - /// ◦ body → filter envelopes with text bodies matching - /// the given pattern - /// - /// ◦ 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 - /// - /// ◦ asc → sort envelopes by the given kind in ascending - /// order - /// - /// ◦ 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>, } -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( diff --git a/src/email/message/command/mod.rs b/src/email/message/command/mod.rs index dd99709..910c8d9 100644 --- a/src/email/message/command/mod.rs +++ b/src/email/message/command/mod.rs @@ -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, diff --git a/src/email/message/command/thread.rs b/src/email/message/command/thread.rs new file mode 100644 index 0000000..c4e2379 --- /dev/null +++ b/src/email/message/command/thread.rs @@ -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, + + #[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::().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) + } +}