From 19347d3249dc3088260ba27e001b87ec66df9aa1 Mon Sep 17 00:00:00 2001 From: FliegendeWurst <2012gdwu+github@posteo.de> Date: Mon, 29 Mar 2021 16:35:15 +0200 Subject: [PATCH] list: mail viewer --- Cargo.lock | 174 +++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 + src/bin/list.rs | 125 ++++++++++++++++++++++++++++++++++ src/bin/new.rs | 2 +- src/bin/sync.rs | 8 +-- 5 files changed, 303 insertions(+), 8 deletions(-) create mode 100644 src/bin/list.rs diff --git a/Cargo.lock b/Cargo.lock index 88a3d7c..48ba417 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,6 +134,27 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "either" version = "1.6.1" @@ -149,6 +170,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -161,6 +188,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "gethostname" version = "0.2.1" @@ -171,6 +208,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "getrandom" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.9.1" @@ -223,8 +271,10 @@ dependencies = [ "itertools", "maildir", "mailparse", + "moins", "rusqlite", "rustls-connector", + "rustyline", ] [[package]] @@ -314,6 +364,35 @@ version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +[[package]] +name = "moins" +version = "0.5.0" +source = "git+https://github.com/FliegendeWurst/moins?branch=master#fd6d6ccebbe33effe49d13fd26ff2214f1655d41" +dependencies = [ + "termion", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", +] + [[package]] name = "nom" version = "5.1.2" @@ -344,6 +423,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + [[package]] name = "once_cell" version = "1.7.2" @@ -386,6 +471,44 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2" +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "redox_syscall" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_termios" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" +dependencies = [ + "redox_syscall", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + [[package]] name = "regex" version = "1.4.5" @@ -469,6 +592,29 @@ dependencies = [ "security-framework", ] +[[package]] +name = "rustyline" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e1b597fcd1eeb1d6b25b493538e5aa19629eb08932184b85fef931ba87e893" +dependencies = [ + "bitflags", + "cfg-if", + "dirs-next", + "fs2", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "scopeguard", + "smallvec", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + [[package]] name = "ryu" version = "1.0.5" @@ -485,6 +631,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "sct" version = "0.6.0" @@ -538,15 +690,27 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "1.0.65" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1d708c221c5a612956ef9f75b37e454e88d1f7b899fbd3a18d4252012d663" +checksum = "6498a9efc342871f91cc2d0d694c674368b4ceb40f62b65a7a08c3792935e702" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] +[[package]] +name = "termion" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] + [[package]] name = "time" version = "0.1.44" @@ -582,6 +746,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + [[package]] name = "vcpkg" version = "0.2.11" diff --git a/Cargo.toml b/Cargo.toml index db2f71b..5a195b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,5 @@ ascii_table = { git = "https://gitlab.com/arnekeller/ascii-table.git", branch = chrono = "0.4.19" # remove when 0.24.3 is released rusqlite = { git = "https://github.com/rusqlite/rusqlite", branch = "master", features = ["bundled"] } +rustyline = "8.0.0" +moins = { git = "https://github.com/FliegendeWurst/moins", branch = "master" } diff --git a/src/bin/list.rs b/src/bin/list.rs new file mode 100644 index 0000000..af4c57b --- /dev/null +++ b/src/bin/list.rs @@ -0,0 +1,125 @@ +use std::{collections::HashMap, env}; + +use ascii_table::{Align, AsciiTable, Column}; +use chrono::{Local, NaiveDateTime, TimeZone}; +use inboxid::*; +use itertools::Itertools; +use mailparse::{MailHeaderMap, dateparse}; +use rustyline::{Editor, error::ReadlineError}; + +fn main() -> Result<()> { + let args = env::args().collect_vec(); + if args.len() > 1 { + show_listing(&args[1]) + } else { + show_listing("INBOX") + } +} + +fn show_listing(mailbox: &str) -> Result<()> { + let maildir = get_maildir(mailbox)?; + + let mut rows = Vec::new(); + let mut mail_list = Vec::new(); + let mut i = 0; + // TODO(refactor) merge with new + let mut list = maildir.list_cur_sorted(Box::new(|name| { + // sort by UID + name.splitn(2, '_').nth(1).map(|x| x.parse().unwrap_or(0)).unwrap_or(0) + })).collect_vec(); + let list = list.iter_mut().map( + |x| x.as_mut().map(|x| (x.flags().to_owned(), x.id().to_owned(), x.parsed()))).collect_vec(); + for maile in &list { + match maile { + Ok((flags, id, Ok(mail))) => { + let headers = mail.get_headers(); + let from = headers.get_all_values("From").join(" "); + let subj = headers.get_all_values("Subject").join(" "); + let date = headers.get_all_values("Date").join(" "); + let date = dateparse(&date).map(|x| { + let dt = Local.from_utc_datetime(&NaiveDateTime::from_timestamp(x, 0)); + dt.format("%Y-%m-%d %H:%M").to_string() + }).unwrap_or(date); + let mut flags_display = String::new(); + if flags.contains('F') { + flags_display.push('+'); + } + if flags.contains('R') { + flags_display.push('R'); + } + if flags.contains('S') { + flags_display.push(' '); + } else { + flags_display.push('*'); + } + rows.push(vec![i.to_string(), flags_display, from, subj, date]); + i += 1; + mail_list.push((flags, id, mail)); + } + Ok((_, _, Err(e))) => { + println!("error parsing mail: {:?}", e); + } + Err(e) => { + println!("error: {:?}", e); + } + } + } + rows.sort_unstable_by(|x, y| x[4].cmp(&y[4])); + let count = rows.len(); + let mut mails = HashMap::new(); + for (i, row) in rows.iter_mut().enumerate() { + mails.insert(count - i, &mail_list[row[0].parse::().unwrap()]); + row[0] = (count - i).to_string(); + } + + let mut ascii_table = AsciiTable::default(); + ascii_table.draw_lines = false; + ascii_table.max_width = usize::MAX; + for (i, &(header, align)) in [ + ("i", Align::Right), + ("---", Align::Right), + ("From", Align::Left), + ("Subject", Align::Left), + ("Date", Align::Left), + ].iter().enumerate() { + let mut column = Column::default(); + column.header = header.to_owned(); + column.align = align; + column.max_width = usize::MAX; + ascii_table.columns.insert(i, column); + } + ascii_table.print(rows); // prints a 0 if empty :) + + if mails.is_empty() { + return Ok(()); + } + let mut rl = Editor::<()>::new(); + loop { + let readline = rl.readline(">> "); + match readline { + Ok(line) => { + let mail = &mails[&line.trim().parse::().unwrap()]; + println!("{}", std::str::from_utf8(&mail.2.get_body_raw().unwrap()).unwrap()); + for x in &mail.2.subparts { + if x.ctype.mimetype == "text/html" { + continue; // TODO + } + let mut content = x.get_body().unwrap(); + moins::Moins::run(&mut content, None); + } + }, + Err(ReadlineError::Interrupted) => { + break + }, + Err(ReadlineError::Eof) => { + break + }, + Err(err) => { + println!("Error: {:?}", err); + break + } + } + } + + Ok(()) +} diff --git a/src/bin/new.rs b/src/bin/new.rs index 46c1b89..3522bdb 100644 --- a/src/bin/new.rs +++ b/src/bin/new.rs @@ -23,7 +23,7 @@ fn show_listing(mailbox: &str) -> Result<()> { let mut seen = Vec::new(); for mut maile in maildir.list_new_sorted(Box::new(|name| { // sort by UID - name.splitn(2, '_').nth(1).map(|x| x.parse().unwrap_or(0)).unwrap_or(0) + u32::MAX - name.splitn(2, '_').nth(1).map(|x| x.parse().unwrap_or(0)).unwrap_or(0) })) { match maile.as_mut().map(|x| x.parsed()) { Ok(Ok(mail)) => { diff --git a/src/bin/sync.rs b/src/bin/sync.rs index 824bda5..b0787bb 100644 --- a/src/bin/sync.rs +++ b/src/bin/sync.rs @@ -1,5 +1,3 @@ -#![feature(string_remove_matches)] - use std::{collections::HashMap, env}; use imap::types::Flag; @@ -130,17 +128,17 @@ fn sync( if flags.contains(&Flag::Seen) { f.push('S'); } else { - f.remove_matches('S'); + f = f.replace('S', ""); } if flags.contains(&Flag::Answered) { f.push('R'); } else { - f.remove_matches('R'); + f = f.replace('R', ""); } if flags.contains(&Flag::Flagged) { f.push('F'); } else { - f.remove_matches('F'); + f = f.replace('F', ""); } Maildir::normalize_flags(&f) });