mirror of
https://github.com/FliegendeWurst/inboxid.git
synced 2024-11-09 10:50:40 +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::{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 args = env::args().collect_vec();
|
let sink = Arc::new(Mutex::new(Vec::new()));
|
||||||
if args.len() > 1 {
|
std::io::set_output_capture(Some(sink.clone()));
|
||||||
show_listing(&args[1])
|
let result = std::panic::catch_unwind(|| {
|
||||||
} else {
|
let args = env::args().collect_vec();
|
||||||
show_listing("INBOX")
|
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() {
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user