list: thread tree listing

This commit is contained in:
FliegendeWurst 2021-04-01 12:03:46 +02:00 committed by Arne Keller
parent aa3c350b71
commit f264b7230d
4 changed files with 160 additions and 6 deletions

27
Cargo.lock generated
View File

@ -194,6 +194,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fixedbitset"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
[[package]]
name = "fs2"
version = "0.4.3"
@ -281,12 +287,23 @@ dependencies = [
"mailproc",
"mime2ext",
"moins",
"petgraph",
"rusqlite",
"rustls-connector",
"rustyline",
"subprocess",
]
[[package]]
name = "indexmap"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "itertools"
version = "0.10.0"
@ -493,6 +510,16 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "pkg-config"
version = "0.3.19"

View File

@ -23,3 +23,4 @@ anyhow = "1.0.40"
mailproc = { git = "https://github.com/FliegendeWurst/mailproc.git", branch = "master" }
subprocess = "0.2.6"
mime2ext = "0.1.2"
petgraph = "0.5.1"

View File

@ -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 inboxid::*;
use itertools::Itertools;
use mailparse::ParsedMail;
use petgraph::{EdgeDirection, dot::Dot, graph::{DiGraph, NodeIndex}, visit::{Dfs, IntoNodeReferences, Walker}};
use rustyline::{Editor, error::ReadlineError};
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()]));
}
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();
ascii_table.draw_lines = false;
ascii_table.max_width = usize::MAX;

View File

@ -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 chrono::{DateTime, Local, NaiveDateTime, TimeZone};
@ -52,7 +52,7 @@ pub fn gen_id(uid_validity: u32, uid: u32) -> String {
format!("{}_{}", uid_validity, uid)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct MaildirID {
uid_validity: u32,
pub uid: u32,
@ -92,7 +92,7 @@ impl MaildirID {
}
pub struct EasyMail<'a> {
pub mail: ParsedMail<'a>,
mail: Option<ParsedMail<'a>>,
pub id: MaildirID,
pub flags: String,
pub from: String,
@ -101,11 +101,55 @@ pub struct EasyMail<'a> {
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> {
type Target = ParsedMail<'a>;
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))
)?;
mails.push(EasyMail {
mail,
mail: Some(mail),
flags,
id,
from,