mirror of
https://github.com/FliegendeWurst/inboxid.git
synced 2024-11-21 16:34:59 +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"
|
||||
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"
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
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 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,
|
||||
|
Loading…
Reference in New Issue
Block a user