From f0def7776bbaa2947ecde165efd6368271a9a6aa Mon Sep 17 00:00:00 2001 From: FliegendeWurst <2012gdwu+github@posteo.de> Date: Wed, 31 Mar 2021 20:44:32 +0200 Subject: [PATCH] list: multipart message reading --- Cargo.lock | 38 ++++++++++++++++++++-- Cargo.toml | 2 ++ src/bin/list.rs | 86 +++++++++++++++++++++++++++++++++++++++++++------ src/lib.rs | 31 ++++++++++++++++++ 4 files changed, 144 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0374068..1826c6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,10 +279,12 @@ dependencies = [ "maildir", "mailparse", "mailproc", + "mime2ext", "moins", "rusqlite", "rustls-connector", "rustyline", + "subprocess", ] [[package]] @@ -294,6 +296,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + [[package]] name = "js-sys" version = "0.3.50" @@ -398,10 +406,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "mime2ext" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88947611258697e12f8602a44003b0885ca5fe30f27132d63c8f47fe98f2f2e" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "moins" version = "0.5.0" -source = "git+https://github.com/FliegendeWurst/moins?branch=master#46df794709b7fdc23e83e61d97c75b11e8248f0f" +source = "git+https://github.com/FliegendeWurst/moins?branch=master#d30052f34176336d27c35c8e62fa56fce70b03ea" dependencies = [ "termion", ] @@ -501,9 +519,9 @@ dependencies = [ [[package]] name = "quoted_printable" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2" +checksum = "1238256b09923649ec89b08104c4dfe9f6cb2fea734a5db5384e44916d59e9c5" [[package]] name = "radix_trie" @@ -709,6 +727,9 @@ name = "serde" version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_derive" @@ -721,6 +742,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "smallvec" version = "1.6.1" diff --git a/Cargo.toml b/Cargo.toml index 7fe9dc1..ad93709 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,5 @@ rustyline = "8.0.0" moins = { git = "https://github.com/FliegendeWurst/moins", branch = "master" } anyhow = "1.0.40" mailproc = { git = "https://github.com/FliegendeWurst/mailproc.git", branch = "master" } +subprocess = "0.2.6" +mime2ext = "0.1.2" diff --git a/src/bin/list.rs b/src/bin/list.rs index 36cbe67..40606da 100644 --- a/src/bin/list.rs +++ b/src/bin/list.rs @@ -1,8 +1,9 @@ -use std::{array::IntoIter, env}; +use std::{array::IntoIter, collections::HashSet, env, fs}; use ascii_table::{Align, AsciiTable, Column}; use inboxid::*; use itertools::Itertools; +use mailparse::ParsedMail; use rustyline::{Editor, error::ReadlineError}; fn main() -> Result<()> { @@ -64,19 +65,71 @@ fn show_listing(mailbox: &str) -> Result<()> { return Ok(()); } let mut rl = Editor::<()>::new(); + let mut state = Initial; + let mut to_delete = HashSet::new(); loop { - let readline = rl.readline(">> "); + let readline = rl.readline(&match state { + Initial => ">> ".to_owned(), + MailSelected(x) => format!("{} >> ", mails.len() - x), + AwaitingSave(_, _) => "out? >> ".to_owned() + }); match readline { Ok(line) => { - let idx = mails.len() - line.trim().parse::().unwrap(); - let mail = &mails[idx]; - println!("{}", std::str::from_utf8(&mail.get_body_raw().unwrap()).unwrap()); - for x in &mail.subparts { - if x.ctype.mimetype == "text/html" { - continue; // TODO + let input_idx = line.trim().parse::(); + match state { + Initial => { + if let Ok(idx) = input_idx { + let idx = mails.len() - idx; + let mail = &mails[idx]; + if mail.ctype.mimetype.starts_with("text/") { + let raw_body = mail.get_body_raw(); + let content = std::str::from_utf8(raw_body.as_deref().unwrap())?; + moins::Moins::run(content, None); + } else if mail.ctype.mimetype.starts_with("multipart/") { + mail.print_tree_structure(0, &mut 1); + state = MailSelected(idx); + } else { + state = AwaitingSave(&*mail, None); + } + continue; + } + }, + MailSelected(mail_idx) => { + let mail = &mails[mail_idx]; + if let Ok(idx) = input_idx { + let part = mail.get_tree_part(&mut 1, idx).unwrap(); + if part.ctype.mimetype.starts_with("text/") { + let raw_body = part.get_body_raw(); + let content = std::str::from_utf8(raw_body.as_deref().unwrap())?; + moins::Moins::run(content, None); + } else { + state = AwaitingSave(part, Some(mail_idx)); + } + continue; + } else if line.is_empty() { + state = Initial; + continue; + } + }, + AwaitingSave(mail, idx) => { + if line == "open" { + let path = if let Some(ext) = mime2ext::mime2ext(&mail.ctype.mimetype) { + format!("/tmp/mail_content.{}", ext) + } else { + "/tmp/mail_content".to_owned() + }; + fs::write(&path, &mail.get_body_raw()?)?; + let mut p = subprocess::Popen::create(&["xdg-open", &path], Default::default())?; + p.wait()?; + to_delete.insert(path); + state = if let Some(idx) = idx { + MailSelected(idx) + } else { + Initial + }; + continue; + } } - let mut content = x.get_body().unwrap(); - moins::Moins::run(&mut content, None); } }, Err(ReadlineError::Interrupted) => { @@ -90,7 +143,20 @@ fn show_listing(mailbox: &str) -> Result<()> { break } } + println!("unknown command!"); + } + + for x in to_delete { + let _ = fs::remove_file(x); } Ok(()) } + +enum State<'a> { + Initial, + MailSelected(usize), + AwaitingSave(&'a ParsedMail<'a>, Option) +} + +use State::*; diff --git a/src/lib.rs b/src/lib.rs index 5c363e8..c06fd6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,6 +109,37 @@ impl<'a> Deref for EasyMail<'a> { } } +pub trait MailExtension { + 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 print_tree_structure(&self, depth: usize, counter: &mut usize) { + if depth == 0 { + println!("{}", self.ctype.mimetype); + } + for mail in &self.subparts { + println!("{}-> {} [{}]", " ".repeat(depth), mail.ctype.mimetype, counter); + *counter += 1; + mail.print_tree_structure(depth + 1, counter); + } + } + + fn get_tree_part(&self, counter: &mut usize, target: usize) -> Option<&ParsedMail> { + for mail in &self.subparts { + if *counter == target { + return Some(mail); + } + *counter += 1; + if let Some(x) = mail.get_tree_part(counter, target) { + return Some(x); + } + } + None + } +} + 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>;