browse: mail viewing

This commit is contained in:
FliegendeWurst 2021-04-05 12:24:00 +02:00 committed by Arne Keller
parent 264cd5f94b
commit a171abfdae
2 changed files with 189 additions and 16 deletions

View File

@ -1,20 +1,38 @@
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::{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 cursive_tree_view::{Placement, TreeView};
use inboxid::*; use inboxid::*;
use io::Write;
use itertools::Itertools; use itertools::Itertools;
use mailparse::ParsedMail; use mailparse::ParsedMail;
use petgraph::{EdgeDirection, graph::{DiGraph, NodeIndex}, visit::{Dfs, IntoNodeReferences}}; use petgraph::{EdgeDirection, graph::{DiGraph, NodeIndex}, visit::{Dfs, IntoNodeReferences}};
fn main() -> Result<()> { fn main() -> Result<()> {
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(); let args = env::args().collect_vec();
if args.len() > 1 { if args.len() > 1 {
show_listing(&args[1]) show_listing(&args[1])
} else { } else {
show_listing("INBOX") 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
}
}
} }
fn show_listing(mailbox: &str) -> Result<()> { fn show_listing(mailbox: &str) -> Result<()> {
@ -24,8 +42,10 @@ fn show_listing(mailbox: &str) -> Result<()> {
for x in maildir.list_cur() { for x in maildir.list_cur() {
mails.push(x?); 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); 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(); let mut rows = Vec::new();
for (i, mail) in mails.iter().enumerate() { 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 mails_by_id = HashMap::new();
let mut threads: HashMap<_, Vec<_>> = 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"); let mid = mail.get_header("Message-ID");
threads.entry(mid.clone()).or_default().push(mail); threads.entry(mid.clone()).or_default().push(mail);
if mails_by_id.insert(mid, mail).is_some() { 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 graph = DiGraph::new();
let mut nodes = HashMap::new(); let mut nodes = HashMap::new();
let mut nodes_inv = 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); let node = graph.add_node(mail);
nodes.insert(mail, node); nodes.insert(mail, node);
nodes_inv.insert(node, mail); 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 value in mail.get_header_values("In-Reply-To") {
for mid in value.split(' ').map(ToOwned::to_owned) { for mid in value.split(' ').map(ToOwned::to_owned) {
if let Some(other_mail) = mails_by_id.get(&mid) { if let Some(other_mail) = mails_by_id.get(&mid) {
@ -109,15 +132,14 @@ fn show_listing(mailbox: &str) -> Result<()> {
return; return;
} }
//println!("{}{}", " ".repeat(depth), mail.subject); //println!("{}{}", " ".repeat(depth), mail.subject);
let line = mail.subject.clone(); let entry = tree.borrow_mut().insert_item(mail, placement, parent);
let entry = tree.borrow_mut().insert_item(line, placement, parent);
mails_printed.borrow_mut().insert(mail); mails_printed.borrow_mut().insert(mail);
let mut replies = graph.neighbors_directed(node, EdgeDirection::Outgoing).collect_vec(); let mut replies = graph.neighbors_directed(node, EdgeDirection::Outgoing).collect_vec();
replies.sort_unstable_by_key(|&idx| { replies.sort_unstable_by_key(|&idx| {
let mut maximum = &nodes_inv[&idx].date; let mut maximum = &nodes_inv[&idx].date;
let mut dfs = Dfs::new(&graph, idx); let mut dfs = Dfs::new(&graph, idx);
while let Some(idx) = dfs.next(&graph) { while let Some(idx) = dfs.next(&graph) {
let other = nodes_inv[&idx]; let other = &nodes_inv[&idx];
maximum = cmp::max(maximum, &other.date); maximum = cmp::max(maximum, &other.date);
} }
maximum maximum
@ -135,7 +157,86 @@ fn show_listing(mailbox: &str) -> Result<()> {
x = y 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<MailPart>| {
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::<MailPart>::new();
mail_part_select.set_on_select(|siv, row| {
let item = siv.call_on_name("part_select", |tree: &mut TreeView<MailPart>| {
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()); siv.add_global_callback('q', |s| s.quit());
@ -143,3 +244,22 @@ fn show_listing(mailbox: &str) -> Result<()> {
Ok(()) 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
}
}
}

View File

@ -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 anyhow::Context;
use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
use imap::{Session, types::Flag}; use imap::{Session, types::Flag};
use maildir::{MailEntry, Maildir}; use maildir::{MailEntry, Maildir};
use mailparse::{MailHeaderMap, ParsedMail, dateparse}; use mailparse::{MailHeaderMap, ParsedMail, dateparse};
use petgraph::{Graph, graph::NodeIndex};
use rusqlite::{Connection, params}; use rusqlite::{Connection, params};
use rustls_connector::{RustlsConnector, rustls::{ClientSession, StreamOwned}}; 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 { pub fn get_header(&self, header: &str) -> String {
self.get_headers().get_all_values(header).join(" ") 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<'_> { impl PartialEq for EasyMail<'_> {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.from == other.from && self.subject == other.subject 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 { pub trait MailExtension {
fn get_tree_structure<'a>(&'a self, graph: &mut Graph<&'a ParsedMail<'a>, ()>, parent: Option<NodeIndex>);
fn print_tree_structure(&self, depth: usize, counter: &mut usize); fn print_tree_structure(&self, depth: usize, counter: &mut usize);
fn get_tree_part(&self, counter: &mut usize, target: usize) -> Option<&ParsedMail>; fn get_tree_part(&self, counter: &mut usize, target: usize) -> Option<&ParsedMail>;
} }
impl MailExtension for ParsedMail<'_> { impl MailExtension for ParsedMail<'_> {
fn get_tree_structure<'a>(&'a self, graph: &mut Graph<&'a ParsedMail<'a>, ()>, parent: Option<NodeIndex>) {
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) { fn print_tree_structure(&self, depth: usize, counter: &mut usize) {
if depth == 0 { if depth == 0 {
println!("{}", self.ctype.mimetype); println!("{}", self.ctype.mimetype);
@ -188,6 +213,7 @@ pub trait MaildirExtension {
fn get_file(&self, name: &str) -> std::result::Result<String, io::Error>; fn get_file(&self, name: &str) -> std::result::Result<String, io::Error>;
fn save_file(&self, name: &str, content: &str) -> std::result::Result<(), io::Error>; fn save_file(&self, name: &str, content: &str) -> std::result::Result<(), io::Error>;
fn get_mails<'a>(&self, entries: &'a mut [MailEntry]) -> Result<Vec<EasyMail<'a>>>; fn get_mails<'a>(&self, entries: &'a mut [MailEntry]) -> Result<Vec<EasyMail<'a>>>;
fn get_mails2<'a>(&self, entries: &'a mut [&'a mut MailEntry]) -> Result<Vec<EasyMail<'a>>>;
} }
impl MaildirExtension for Maildir { impl MaildirExtension for Maildir {
@ -224,6 +250,33 @@ impl MaildirExtension for Maildir {
} }
Ok(mails) Ok(mails)
} }
// TODO this should be unified with the above
fn get_mails2<'a>(&self, entries: &'a mut [&'a mut MailEntry]) -> Result<Vec<EasyMail<'a>>> {
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 { pub fn store_i64(x: u64) -> i64 {