mirror of
https://github.com/FliegendeWurst/inboxid.git
synced 2024-11-09 10:50:40 +00:00
list: thread tree listing
This commit is contained in:
parent
aa3c350b71
commit
f264b7230d
27
Cargo.lock
generated
27
Cargo.lock
generated
@ -194,6 +194,12 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fixedbitset"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fs2"
|
name = "fs2"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@ -281,12 +287,23 @@ dependencies = [
|
|||||||
"mailproc",
|
"mailproc",
|
||||||
"mime2ext",
|
"mime2ext",
|
||||||
"moins",
|
"moins",
|
||||||
|
"petgraph",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"rustls-connector",
|
"rustls-connector",
|
||||||
"rustyline",
|
"rustyline",
|
||||||
"subprocess",
|
"subprocess",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "1.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@ -493,6 +510,16 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
|
checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "petgraph"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7"
|
||||||
|
dependencies = [
|
||||||
|
"fixedbitset",
|
||||||
|
"indexmap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.19"
|
version = "0.3.19"
|
||||||
|
@ -23,3 +23,4 @@ anyhow = "1.0.40"
|
|||||||
mailproc = { git = "https://github.com/FliegendeWurst/mailproc.git", branch = "master" }
|
mailproc = { git = "https://github.com/FliegendeWurst/mailproc.git", branch = "master" }
|
||||||
subprocess = "0.2.6"
|
subprocess = "0.2.6"
|
||||||
mime2ext = "0.1.2"
|
mime2ext = "0.1.2"
|
||||||
|
petgraph = "0.5.1"
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
use std::{array::IntoIter, collections::HashSet, env, fs};
|
use std::{array::IntoIter, cell::RefCell, cmp, collections::{HashMap, HashSet}, env, fs};
|
||||||
|
|
||||||
use ascii_table::{Align, AsciiTable, Column};
|
use ascii_table::{Align, AsciiTable, Column};
|
||||||
use inboxid::*;
|
use inboxid::*;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use mailparse::ParsedMail;
|
use mailparse::ParsedMail;
|
||||||
|
use petgraph::{EdgeDirection, dot::Dot, graph::{DiGraph, NodeIndex}, visit::{Dfs, IntoNodeReferences, Walker}};
|
||||||
use rustyline::{Editor, error::ReadlineError};
|
use rustyline::{Editor, error::ReadlineError};
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
@ -43,6 +44,87 @@ fn show_listing(mailbox: &str) -> Result<()> {
|
|||||||
rows.push(IntoIter::new([(mails.len() - i).to_string(), flags_display, mail.from.clone(), mail.subject.clone(), mail.date_iso.clone()]));
|
rows.push(IntoIter::new([(mails.len() - i).to_string(), flags_display, mail.from.clone(), mail.subject.clone(), mail.date_iso.clone()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut mails_by_id = HashMap::new();
|
||||||
|
let mut threads: HashMap<_, Vec<_>> = HashMap::new();
|
||||||
|
for mail in &mails {
|
||||||
|
let mid = mail.get_header("Message-ID");
|
||||||
|
threads.entry(mid.clone()).or_default().push(mail);
|
||||||
|
if mails_by_id.insert(mid, mail).is_some() {
|
||||||
|
println!("error: missing/duplicate Message-ID");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
for value in mail.get_header_values("References") {
|
||||||
|
for mid in value.split(' ').map(ToOwned::to_owned) {
|
||||||
|
threads.entry(mid).or_default().push(mail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for value in mail.get_header_values("In-Reply-To") {
|
||||||
|
for mid in value.split(' ').map(ToOwned::to_owned) {
|
||||||
|
threads.entry(mid).or_default().push(mail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut threads = threads.into_iter().collect_vec();
|
||||||
|
threads.sort_unstable_by_key(|(_, mails)| mails.len());
|
||||||
|
threads.reverse();
|
||||||
|
let mut graph = DiGraph::new();
|
||||||
|
let mut nodes = HashMap::new();
|
||||||
|
let mut nodes_inv = HashMap::new();
|
||||||
|
for mail in &mails {
|
||||||
|
let node = graph.add_node(mail);
|
||||||
|
nodes.insert(mail, node);
|
||||||
|
nodes_inv.insert(node, mail);
|
||||||
|
}
|
||||||
|
for mail in &mails {
|
||||||
|
for value in mail.get_header_values("In-Reply-To") {
|
||||||
|
for mid in value.split(' ').map(ToOwned::to_owned) {
|
||||||
|
if let Some(other_mail) = mails_by_id.get(&mid) {
|
||||||
|
graph.add_edge(nodes[other_mail], nodes[mail], ());
|
||||||
|
} else {
|
||||||
|
let pseudomail = Box::leak(Box::new(EasyMail::new_pseudo(mid.clone())));
|
||||||
|
let node = graph.add_node(pseudomail);
|
||||||
|
nodes.insert(pseudomail, node);
|
||||||
|
nodes_inv.insert(node, pseudomail);
|
||||||
|
graph.add_edge(node, nodes[mail], ());
|
||||||
|
mails_by_id.insert(mid, pseudomail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut roots = graph.node_references().filter(|x| graph.neighbors_directed(x.0, EdgeDirection::Incoming).count() == 0).collect_vec();
|
||||||
|
roots.sort_unstable_by_key(|x| x.1.date);
|
||||||
|
let mails_printed = RefCell::new(HashSet::new());
|
||||||
|
|
||||||
|
struct PrintThread<'a> {
|
||||||
|
f: &'a dyn Fn(&PrintThread, NodeIndex, usize)
|
||||||
|
}
|
||||||
|
let print_thread = |this: &PrintThread, node, depth| {
|
||||||
|
let mail = nodes_inv[&node];
|
||||||
|
if mails_printed.borrow().contains(mail) && depth == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
println!("{}{}", " ".repeat(depth), mail.subject);
|
||||||
|
mails_printed.borrow_mut().insert(mail);
|
||||||
|
let mut replies = graph.neighbors_directed(node, EdgeDirection::Outgoing).collect_vec();
|
||||||
|
replies.sort_unstable_by_key(|&idx| {
|
||||||
|
let mut maximum = &nodes_inv[&idx].date;
|
||||||
|
let mut dfs = Dfs::new(&graph, idx);
|
||||||
|
while let Some(idx) = dfs.next(&graph) {
|
||||||
|
let other = nodes_inv[&idx];
|
||||||
|
maximum = cmp::max(maximum, &other.date);
|
||||||
|
}
|
||||||
|
maximum
|
||||||
|
});
|
||||||
|
for r in replies {
|
||||||
|
(this.f)(this, r, depth + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let print_thread = PrintThread { f: &print_thread };
|
||||||
|
|
||||||
|
for root in roots {
|
||||||
|
(print_thread.f)(&print_thread, root.0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
let mut ascii_table = AsciiTable::default();
|
let mut ascii_table = AsciiTable::default();
|
||||||
ascii_table.draw_lines = false;
|
ascii_table.draw_lines = false;
|
||||||
ascii_table.max_width = usize::MAX;
|
ascii_table.max_width = usize::MAX;
|
||||||
|
54
src/lib.rs
54
src/lib.rs
@ -1,4 +1,4 @@
|
|||||||
use std::{borrow::Cow, convert::{TryFrom, TryInto}, env, fs, io, net::TcpStream, ops::Deref};
|
use std::{borrow::Cow, convert::{TryFrom, TryInto}, env, fmt::Debug, fs, hash::Hash, io, net::TcpStream, ops::Deref};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
|
use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
|
||||||
@ -52,7 +52,7 @@ pub fn gen_id(uid_validity: u32, uid: u32) -> String {
|
|||||||
format!("{}_{}", uid_validity, uid)
|
format!("{}_{}", uid_validity, uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct MaildirID {
|
pub struct MaildirID {
|
||||||
uid_validity: u32,
|
uid_validity: u32,
|
||||||
pub uid: u32,
|
pub uid: u32,
|
||||||
@ -92,7 +92,7 @@ impl MaildirID {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct EasyMail<'a> {
|
pub struct EasyMail<'a> {
|
||||||
pub mail: ParsedMail<'a>,
|
mail: Option<ParsedMail<'a>>,
|
||||||
pub id: MaildirID,
|
pub id: MaildirID,
|
||||||
pub flags: String,
|
pub flags: String,
|
||||||
pub from: String,
|
pub from: String,
|
||||||
@ -101,11 +101,55 @@ pub struct EasyMail<'a> {
|
|||||||
pub date_iso: String,
|
pub date_iso: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EasyMail<'_> {
|
||||||
|
pub fn new_pseudo(subject: String) -> Self {
|
||||||
|
Self {
|
||||||
|
mail: None,
|
||||||
|
id: MaildirID::new(0, 0),
|
||||||
|
flags: "S".to_owned(),
|
||||||
|
from: String::new(),
|
||||||
|
subject,
|
||||||
|
date: Local.from_utc_datetime(&NaiveDateTime::from_timestamp(0, 0)),
|
||||||
|
date_iso: "????-??-??".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_header(&self, header: &str) -> String {
|
||||||
|
self.get_headers().get_all_values(header).join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_header_values(&self, header: &str) -> Vec<String> {
|
||||||
|
self.get_headers().get_all_values(header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for EasyMail<'_> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Mail[ID={},Subject={:?}]", self.id.uid, self.subject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for EasyMail<'_> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id && self.from == other.from && self.subject == other.subject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for EasyMail<'_> {}
|
||||||
|
|
||||||
|
impl Hash for EasyMail<'_> {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.id.hash(state);
|
||||||
|
self.from.hash(state);
|
||||||
|
self.subject.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> Deref for EasyMail<'a> {
|
impl<'a> Deref for EasyMail<'a> {
|
||||||
type Target = ParsedMail<'a>;
|
type Target = ParsedMail<'a>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.mail
|
&self.mail.as_ref().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +213,7 @@ impl MaildirExtension for Maildir {
|
|||||||
Local.from_utc_datetime(&NaiveDateTime::from_timestamp(x, 0))
|
Local.from_utc_datetime(&NaiveDateTime::from_timestamp(x, 0))
|
||||||
)?;
|
)?;
|
||||||
mails.push(EasyMail {
|
mails.push(EasyMail {
|
||||||
mail,
|
mail: Some(mail),
|
||||||
flags,
|
flags,
|
||||||
id,
|
id,
|
||||||
from,
|
from,
|
||||||
|
Loading…
Reference in New Issue
Block a user