From a171abfdae4579de2b4321eac02ba9402de68650 Mon Sep 17 00:00:00 2001 From: FliegendeWurst <2012gdwu+github@posteo.de> Date: Mon, 5 Apr 2021 12:24:00 +0200 Subject: [PATCH] browse: mail viewing --- src/bin/browse.rs | 150 +++++++++++++++++++++++++++++++++++++++++----- src/lib.rs | 55 ++++++++++++++++- 2 files changed, 189 insertions(+), 16 deletions(-) diff --git a/src/bin/browse.rs b/src/bin/browse.rs index ccb0f8d..b210186 100644 --- a/src/bin/browse.rs +++ b/src/bin/browse.rs @@ -1,19 +1,37 @@ -use std::{array::IntoIter, cell::RefCell, cmp, collections::{HashMap, HashSet}, env}; +#![feature(internal_output_capture)] + +use std::{array::IntoIter, cell::RefCell, cmp, collections::{HashMap, HashSet}, env, fmt::Display, io, sync::{Arc, Mutex}}; use cursive::{Cursive, CursiveExt}; -use cursive::views::{ListView, TextView}; +use cursive::traits::Identifiable; +use cursive::view::{Scrollable, SizeConstraint}; +use cursive::views::{LinearLayout, ResizedView, TextView}; use cursive_tree_view::{Placement, TreeView}; use inboxid::*; +use io::Write; use itertools::Itertools; use mailparse::ParsedMail; use petgraph::{EdgeDirection, graph::{DiGraph, NodeIndex}, visit::{Dfs, IntoNodeReferences}}; fn main() -> Result<()> { - let args = env::args().collect_vec(); - if args.len() > 1 { - show_listing(&args[1]) - } else { - show_listing("INBOX") + let sink = Arc::new(Mutex::new(Vec::new())); + std::io::set_output_capture(Some(sink.clone())); + let result = std::panic::catch_unwind(|| { + let args = env::args().collect_vec(); + if args.len() > 1 { + show_listing(&args[1]) + } else { + show_listing("INBOX") + } + }); + match result { + Ok(res) => res, + Err(_) => { + if let Err(e) = io::stderr().lock().write_all(&sink.lock().unwrap()) { + println!("{:?}", e); + } + Err("panicked".into()) // not displayed + } } } @@ -24,8 +42,10 @@ fn show_listing(mailbox: &str) -> Result<()> { for x in maildir.list_cur() { mails.push(x?); } - let mut mails = maildir.get_mails(&mut mails)?; + let mails = Box::leak(Box::new(mails.into_iter().map(Box::new).map(Box::leak).collect_vec())); + let mut mails = maildir.get_mails2(mails)?; mails.sort_by_key(|x| x.date); + let mails = Box::leak(Box::new(mails.into_iter().map(Box::new).map(Box::leak).collect_vec())); let mut rows = Vec::new(); for (i, mail) in mails.iter().enumerate() { @@ -47,7 +67,8 @@ fn show_listing(mailbox: &str) -> Result<()> { let mut mails_by_id = HashMap::new(); let mut threads: HashMap<_, Vec<_>> = HashMap::new(); - for mail in &mails { + for i in 0..mails.len() { + let mail = &*mails[i]; let mid = mail.get_header("Message-ID"); threads.entry(mid.clone()).or_default().push(mail); if mails_by_id.insert(mid, mail).is_some() { @@ -71,12 +92,14 @@ fn show_listing(mailbox: &str) -> Result<()> { let mut graph = DiGraph::new(); let mut nodes = HashMap::new(); let mut nodes_inv = HashMap::new(); - for mail in &mails { + for i in 0..mails.len() { + let mail = &*mails[i]; let node = graph.add_node(mail); nodes.insert(mail, node); nodes_inv.insert(node, mail); } - for mail in &mails { + for i in 0..mails.len() { + let mail = &*mails[i]; 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) { @@ -109,15 +132,14 @@ fn show_listing(mailbox: &str) -> Result<()> { return; } //println!("{}{}", " ".repeat(depth), mail.subject); - let line = mail.subject.clone(); - let entry = tree.borrow_mut().insert_item(line, placement, parent); + let entry = tree.borrow_mut().insert_item(mail, placement, parent); 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]; + let other = &nodes_inv[&idx]; maximum = cmp::max(maximum, &other.date); } maximum @@ -135,7 +157,86 @@ fn show_listing(mailbox: &str) -> Result<()> { x = y } - siv.add_layer(tree.into_inner()); + let mut tree = tree.into_inner(); + tree.set_on_select(|siv, row| { + let item = siv.call_on_name("tree", |tree: &mut TreeView<&EasyMail>| { + *tree.borrow_item(row).unwrap() + }).unwrap(); + if item.is_pseudo() { + return; + } + let mut mail_struct = DiGraph::new(); + item.get_tree_structure(&mut mail_struct, None); + if let Some(mail) = siv.call_on_name("part_select", |view: &mut TreeView| { + view.clear(); + let mut part_to_display = None; + let mut idx_select = 0; + let mut idxes = HashMap::new(); + let mut i = 0; + for idx in mail_struct.node_indices() { + let part = mail_struct[idx]; + let mime = &part.ctype.mimetype; + let incoming = mail_struct.neighbors_directed(idx, EdgeDirection::Incoming).next(); + let tree_idx = if let Some(parent) = incoming { + let parent_idx = idxes[&parent]; + let tree_idx = view.insert_item(MailPart::from(part), Placement::LastChild, parent_idx).unwrap(); + tree_idx + } else { + let tree_idx = view.insert_item(MailPart::from(part), Placement::After, i).unwrap(); + i = tree_idx; + tree_idx + }; + idxes.insert(idx, tree_idx); + if mime.starts_with("text/") { + if part_to_display.is_none() { + part_to_display = Some(part); + idx_select = tree_idx; + } else if mime == "text/plain" { + if let Some(part) = part_to_display.as_ref() { + if part.ctype.mimetype != "text/plain" { + part_to_display = Some(part); + idx_select = tree_idx; + } + } + } + } + } + if part_to_display.is_some() { + view.set_selected_row(idx_select); + } + part_to_display + }).unwrap() { + siv.call_on_name("mail", |view: &mut TextView| { + view.set_content(mail.get_body().unwrap()); + }); + } + }); + tree.set_on_submit(|siv, _row| { + siv.focus_name("mail").unwrap(); + }); + let tree = tree.with_name("tree").scrollable(); + let tree_resized = ResizedView::new(SizeConstraint::AtMost(120), SizeConstraint::Free, tree); + let mail_content = TextView::new("").with_name("mail").scrollable(); + let mut mail_part_select = TreeView::::new(); + mail_part_select.set_on_select(|siv, row| { + let item = siv.call_on_name("part_select", |tree: &mut TreeView| { + tree.borrow_item(row).unwrap().part + }).unwrap(); + siv.call_on_name("mail", |view: &mut TextView| { + view.set_content(item.get_body().unwrap()); + }); + }); + mail_part_select.set_on_submit(|siv, _row| { + siv.focus_name("mail").unwrap(); + }); + let mail_wrapper = LinearLayout::vertical() + .child(ResizedView::with_full_height(mail_content)) + .child(mail_part_select.with_name("part_select")); + let mail_content_resized = ResizedView::new(SizeConstraint::Full, SizeConstraint::Free, mail_wrapper); + let main = LinearLayout::horizontal() + .child(tree_resized) + .child(mail_content_resized); + siv.add_fullscreen_layer(ResizedView::with_full_screen(main)); siv.add_global_callback('q', |s| s.quit()); @@ -143,3 +244,22 @@ fn show_listing(mailbox: &str) -> Result<()> { Ok(()) } + +#[derive(Debug)] +struct MailPart { + part: &'static ParsedMail<'static> +} + +impl Display for MailPart { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.part.ctype.mimetype) + } +} + +impl From<&'static ParsedMail<'static>> for MailPart { + fn from(part: &'static ParsedMail<'static>) -> Self { + Self { + part + } + } +} diff --git a/src/lib.rs b/src/lib.rs index b00eb32..3d35cdf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,11 @@ -use std::{borrow::Cow, convert::{TryFrom, TryInto}, env, fmt::Debug, fs, hash::Hash, io, net::TcpStream, ops::Deref}; +use std::{borrow::Cow, convert::{TryFrom, TryInto}, env, fmt::{Debug, Display}, fs, hash::Hash, io, net::TcpStream, ops::Deref}; use anyhow::Context; use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; use imap::{Session, types::Flag}; use maildir::{MailEntry, Maildir}; use mailparse::{MailHeaderMap, ParsedMail, dateparse}; +use petgraph::{Graph, graph::NodeIndex}; use rusqlite::{Connection, params}; use rustls_connector::{RustlsConnector, rustls::{ClientSession, StreamOwned}}; @@ -114,6 +115,10 @@ impl EasyMail<'_> { } } + pub fn is_pseudo(&self) -> bool { + self.mail.is_none() + } + pub fn get_header(&self, header: &str) -> String { self.get_headers().get_all_values(header).join(" ") } @@ -129,6 +134,12 @@ impl Debug for EasyMail<'_> { } } +impl Display for EasyMail<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.subject) + } +} + impl PartialEq for EasyMail<'_> { fn eq(&self, other: &Self) -> bool { self.id == other.id && self.from == other.from && self.subject == other.subject @@ -154,11 +165,25 @@ impl<'a> Deref for EasyMail<'a> { } pub trait MailExtension { + fn get_tree_structure<'a>(&'a self, graph: &mut Graph<&'a ParsedMail<'a>, ()>, parent: Option); fn print_tree_structure(&self, depth: usize, counter: &mut usize); fn get_tree_part(&self, counter: &mut usize, target: usize) -> Option<&ParsedMail>; } impl MailExtension for ParsedMail<'_> { + fn get_tree_structure<'a>(&'a self, graph: &mut Graph<&'a ParsedMail<'a>, ()>, parent: Option) { + let parent = if parent.is_none() { + graph.add_node(&self) + } else { + parent.unwrap() + }; + for mail in &self.subparts { + let new = graph.add_node(mail); + graph.add_edge(parent, new, ()); + mail.get_tree_structure(graph, Some(new)); + } + } + fn print_tree_structure(&self, depth: usize, counter: &mut usize) { if depth == 0 { println!("{}", self.ctype.mimetype); @@ -188,6 +213,7 @@ pub trait MaildirExtension { fn get_file(&self, name: &str) -> std::result::Result; fn save_file(&self, name: &str, content: &str) -> std::result::Result<(), io::Error>; fn get_mails<'a>(&self, entries: &'a mut [MailEntry]) -> Result>>; + fn get_mails2<'a>(&self, entries: &'a mut [&'a mut MailEntry]) -> Result>>; } impl MaildirExtension for Maildir { @@ -224,6 +250,33 @@ impl MaildirExtension for Maildir { } Ok(mails) } + + // TODO this should be unified with the above + fn get_mails2<'a>(&self, entries: &'a mut [&'a mut MailEntry]) -> Result>> { + let mut mails = Vec::new(); + for maile in entries { + let id = maile.id().try_into()?; + let flags = maile.flags().to_owned(); + let mail = maile.parsed()?; + let headers = mail.get_headers(); + let from = headers.get_all_values("From").join(" "); + let subject = headers.get_all_values("Subject").join(" "); + let date = headers.get_all_values("Date").join(" "); + let date = dateparse(&date).map(|x| + Local.from_utc_datetime(&NaiveDateTime::from_timestamp(x, 0)) + )?; + mails.push(EasyMail { + mail: Some(mail), + flags, + id, + from, + subject, + date_iso: date.format("%Y-%m-%d %H:%M").to_string(), + date, + }); + } + Ok(mails) + } } pub fn store_i64(x: u64) -> i64 {