wip: design basic tree using petgraph

This commit is contained in:
Clément DOUIN 2024-05-17 23:22:06 +02:00
parent 7a951b4830
commit 90e12ddc51
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
8 changed files with 410 additions and 10 deletions

7
Cargo.lock generated
View file

@ -1396,7 +1396,6 @@ dependencies = [
[[package]] [[package]]
name = "email-lib" name = "email-lib"
version = "0.24.1" version = "0.24.1"
source = "git+https://git.sr.ht/~soywod/pimalaya#033ba2a2e193769e1272c9493aa1d6c975346eb5"
dependencies = [ dependencies = [
"advisory-lock", "advisory-lock",
"async-ctrlc", "async-ctrlc",
@ -1425,6 +1424,7 @@ dependencies = [
"once_cell", "once_cell",
"ouroboros", "ouroboros",
"paste", "paste",
"petgraph",
"pgp-lib", "pgp-lib",
"process-lib", "process-lib",
"rayon", "rayon",
@ -2074,6 +2074,7 @@ dependencies = [
"mml-lib", "mml-lib",
"oauth-lib", "oauth-lib",
"once_cell", "once_cell",
"petgraph",
"process-lib", "process-lib",
"secret-lib", "secret-lib",
"serde", "serde",
@ -2267,7 +2268,6 @@ dependencies = [
[[package]] [[package]]
name = "imap-client" name = "imap-client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff"
dependencies = [ dependencies = [
"imap-flow", "imap-flow",
"once_cell", "once_cell",
@ -2297,7 +2297,6 @@ dependencies = [
[[package]] [[package]]
name = "imap-flow" name = "imap-flow"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff"
dependencies = [ dependencies = [
"bounded-static", "bounded-static",
"bytes", "bytes",
@ -4577,7 +4576,6 @@ dependencies = [
[[package]] [[package]]
name = "tag-generator" name = "tag-generator"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff"
dependencies = [ dependencies = [
"imap-types", "imap-types",
"rand", "rand",
@ -4592,7 +4590,6 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
[[package]] [[package]]
name = "tasks" name = "tasks"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/soywod/imap-flow.git?branch=session#599cedbf9facd7d04eaacef2ec6710ee7f7f9eff"
dependencies = [ dependencies = [
"imap-flow", "imap-flow",
"imap-types", "imap-types",

View file

@ -65,6 +65,7 @@ md5 = "0.7"
mml-lib = { version = "=1.0.12", default-features = false, features = ["derive"] } mml-lib = { version = "=1.0.12", default-features = false, features = ["derive"] }
oauth-lib = "=0.1.1" oauth-lib = "=0.1.1"
once_cell = "1.16" once_cell = "1.16"
petgraph = "0.6"
process-lib = { version = "=0.4.2", features = ["derive"] } process-lib = { version = "=0.4.2", features = ["derive"] }
secret-lib = { version = "=0.4.4", features = ["derive"] } secret-lib = { version = "=0.4.4", features = ["derive"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
@ -86,8 +87,11 @@ uuid = { version = "0.8", features = ["v4"] }
[patch.crates-io] [patch.crates-io]
# WIP: transition from `imap` to `imap-codec` # WIP: transition from `imap` to `imap-codec`
email-lib = { git = "https://git.sr.ht/~soywod/pimalaya" } email-lib = { path = "/home/soywod/sourcehut/pimalaya/email" }
imap-client = { git = "https://github.com/soywod/imap-flow.git", branch = "session" } imap-client = { path = "/home/soywod/code/imap-flow/client" }
tasks = { git = "https://github.com/soywod/imap-flow.git", branch = "session" } tasks = { path = "/home/soywod/code/imap-flow/tasks" }
# 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-codec = { git = "https://github.com/duesee/imap-codec.git" } imap-codec = { git = "https://github.com/duesee/imap-codec.git" }
imap-types = { git = "https://github.com/duesee/imap-codec.git" } imap-types = { git = "https://github.com/duesee/imap-codec.git" }

View file

@ -142,6 +142,14 @@ impl TomlAccountConfig {
.or(self.backend.as_ref()) .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> { pub fn watch_envelopes_kind(&self) -> Option<&BackendKind> {
self.envelope self.envelope
.as_ref() .as_ref()

View file

@ -3,6 +3,7 @@ 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")]
@ -23,6 +24,7 @@ use email::{
envelope::{ envelope::{
get::GetEnvelope, get::GetEnvelope,
list::{ListEnvelopes, ListEnvelopesOptions}, list::{ListEnvelopes, ListEnvelopesOptions},
thread::ThreadEnvelopes,
watch::WatchEnvelopes, watch::WatchEnvelopes,
Id, SingleId, Id, SingleId,
}, },
@ -337,6 +339,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>> { fn watch_envelopes(&self) -> Option<BackendFeature<Self::Context, dyn WatchEnvelopes>> {
match self.toml_account_config.watch_envelopes_kind() { match self.toml_account_config.watch_envelopes_kind() {
#[cfg(feature = "imap")] #[cfg(feature = "imap")]
@ -687,6 +706,19 @@ impl Backend {
Ok(envelopes) Ok(envelopes)
} }
pub async fn thread_envelopes(
&self,
folder: &str,
opts: ListEnvelopesOptions,
) -> Result<DiGraphMap<u32, u32>> {
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 =
// 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)?;

View file

@ -244,6 +244,7 @@ impl TomlConfig {
}), }),
envelope: config.envelope.map(|c| EnvelopeConfig { envelope: config.envelope.map(|c| EnvelopeConfig {
list: c.list.map(|c| c.remote), list: c.list.map(|c| c.remote),
thread: c.thread.map(|c| c.remote),
watch: c.watch.map(|c| c.remote), watch: c.watch.map(|c| c.remote),
#[cfg(feature = "account-sync")] #[cfg(feature = "account-sync")]
sync: c.sync, sync: c.sync,

View file

@ -1,12 +1,15 @@
pub mod list; pub mod list;
pub mod thread;
pub mod watch; pub mod watch;
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};
use self::{list::ListEnvelopesCommand, watch::WatchEnvelopesCommand}; use self::{
list::ListEnvelopesCommand, thread::ThreadEnvelopesCommand, watch::WatchEnvelopesCommand,
};
/// Manage envelopes. /// Manage envelopes.
/// ///
@ -19,6 +22,9 @@ pub enum EnvelopeSubcommand {
#[command(alias = "lst")] #[command(alias = "lst")]
List(ListEnvelopesCommand), List(ListEnvelopesCommand),
#[command()]
Thread(ThreadEnvelopesCommand),
#[command()] #[command()]
Watch(WatchEnvelopesCommand), Watch(WatchEnvelopesCommand),
} }
@ -28,6 +34,7 @@ impl EnvelopeSubcommand {
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::List(cmd) => cmd.execute(printer, config).await, Self::List(cmd) => cmd.execute(printer, config).await,
Self::Thread(cmd) => cmd.execute(printer, config).await,
Self::Watch(cmd) => cmd.execute(printer, config).await, Self::Watch(cmd) => cmd.execute(printer, config).await,
} }
} }

View file

@ -0,0 +1,330 @@
use ariadne::{Color, Label, Report, ReportKind, Source};
use clap::Parser;
use color_eyre::Result;
use email::{
backend::feature::BackendFeatureSource,
email::search_query,
envelope::list::ListEnvelopesOptions,
search_query::{filter::SearchEmailsFilterQuery, SearchEmailsQuery},
};
use petgraph::{graphmap::DiGraphMap, visit::IntoNodeIdentifiers, Direction};
use std::{
collections::{HashMap, HashSet},
io::Write,
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,
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,
/// 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,
#[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>,
/// 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<()> {
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 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(
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(Color::Red),
// )
// .finish()
// .eprint((source, Source::from(&query)))
// .unwrap();
// }
// exit(0)
// }
// };
let opts = ListEnvelopesOptions {
page,
page_size,
query: None,
};
let graph = backend.thread_envelopes(folder, opts).await?;
println!("graph: {graph:#?}");
let mut stdout = std::io::stdout();
write_tree(&mut stdout, &graph, 0, String::new(), 0)?;
stdout.flush()?;
// printer.print_table(envelopes, self.table_max_width)?;
Ok(())
}
}
pub fn write_tree(
w: &mut impl std::io::Write,
graph: &DiGraphMap<u32, u32>,
parent: u32,
pad: String,
weight: u32,
) -> std::io::Result<()> {
let edges = graph
.all_edges()
.filter_map(|(a, b, w)| {
if a == parent && *w == weight {
Some(b)
} else {
None
}
})
.collect::<Vec<_>>();
writeln!(w, "{parent}")?;
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!(w, "{pad}{y}─ ")?;
let pad = format!("{pad}{x} ");
write_tree(w, graph, b, pad, weight + 1)?;
}
Ok(())
}
#[cfg(test)]
mod test {
use petgraph::graphmap::DiGraphMap;
use super::write_tree;
#[test]
fn tree_1() {
let mut buf = Vec::new();
let mut graph = DiGraphMap::new();
graph.add_edge(0, 1, 0);
graph.add_edge(0, 2, 0);
graph.add_edge(0, 3, 0);
write_tree(&mut buf, &graph, 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 mut buf = Vec::new();
let mut graph = DiGraphMap::new();
graph.add_edge(0, 1, 0);
graph.add_edge(1, 2, 1);
graph.add_edge(1, 3, 1);
write_tree(&mut buf, &graph, 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 mut buf = Vec::new();
let mut graph = DiGraphMap::new();
graph.add_edge(0, 1, 0);
graph.add_edge(1, 2, 1);
graph.add_edge(2, 22, 2);
graph.add_edge(1, 3, 1);
graph.add_edge(0, 4, 0);
graph.add_edge(4, 5, 1);
graph.add_edge(5, 6, 2);
write_tree(&mut buf, &graph, 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

@ -8,6 +8,7 @@ use crate::backend::BackendKind;
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct EnvelopeConfig { pub struct EnvelopeConfig {
pub list: Option<ListEnvelopesConfig>, pub list: Option<ListEnvelopesConfig>,
pub thread: Option<ThreadEnvelopesConfig>,
pub watch: Option<WatchEnvelopesConfig>, pub watch: Option<WatchEnvelopesConfig>,
pub get: Option<GetEnvelopeConfig>, pub get: Option<GetEnvelopeConfig>,
#[cfg(feature = "account-sync")] #[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)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct WatchEnvelopesConfig { pub struct WatchEnvelopesConfig {
pub backend: Option<BackendKind>, pub backend: Option<BackendKind>,