mirror of
https://github.com/FliegendeWurst/inboxid.git
synced 2024-11-08 10:20:39 +00:00
browse: mail viewing
This commit is contained in:
parent
264cd5f94b
commit
a171abfdae
@ -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<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());
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
55
src/lib.rs
55
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<NodeIndex>);
|
||||
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<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) {
|
||||
if depth == 0 {
|
||||
println!("{}", self.ctype.mimetype);
|
||||
@ -188,6 +213,7 @@ pub trait MaildirExtension {
|
||||
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 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 {
|
||||
@ -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<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 {
|
||||
|
Loading…
Reference in New Issue
Block a user