diff --git a/Cargo.lock b/Cargo.lock index 48ba417..04cb9d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" + [[package]] name = "arrayvec" version = "0.5.2" @@ -265,6 +271,7 @@ dependencies = [ name = "inboxid" version = "0.1.0" dependencies = [ + "anyhow", "ascii_table", "chrono", "imap", @@ -288,9 +295,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.49" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc15e39392125075f60c95ba416f5381ff6c3a948ff02ab12464715adf56c821" +checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c" dependencies = [ "wasm-bindgen", ] @@ -316,9 +323,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7" +checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" [[package]] name = "libsqlite3-sys" @@ -367,7 +374,7 @@ checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] name = "moins" version = "0.5.0" -source = "git+https://github.com/FliegendeWurst/moins?branch=master#fd6d6ccebbe33effe49d13fd26ff2214f1655d41" +source = "git+https://github.com/FliegendeWurst/moins?branch=master#46df794709b7fdc23e83e61d97c75b11e8248f0f" dependencies = [ "termion", ] @@ -772,9 +779,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe8f61dba8e5d645a4d8132dc7a0a66861ed5e1045d2c0ed940fab33bac0fbe" +checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -782,9 +789,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046ceba58ff062da072c7cb4ba5b22a37f00a302483f7e2a6cdc18fedbdc1fd3" +checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae" dependencies = [ "bumpalo", "lazy_static", @@ -797,9 +804,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9aa01d36cda046f797c57959ff5f3c615c9cc63997a8d545831ec7976819b" +checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -807,9 +814,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96eb45c1b2ee33545a813a92dbb53856418bf7eb54ab34f7f7ff1448a5b3735d" +checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c" dependencies = [ "proc-macro2", "quote", @@ -820,15 +827,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7148f4696fb4960a346eaa60bbfb42a1ac4ebba21f750f75fc1375b098d5ffa" +checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489" [[package]] name = "web-sys" -version = "0.3.49" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fe19d70f5dacc03f6e46777213facae5ac3801575d56ca6cbd4c93dcd12310" +checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 5a195b0..b384236 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ chrono = "0.4.19" rusqlite = { git = "https://github.com/rusqlite/rusqlite", branch = "master", features = ["bundled"] } rustyline = "8.0.0" moins = { git = "https://github.com/FliegendeWurst/moins", branch = "master" } +anyhow = "1.0.40" diff --git a/src/bin/list.rs b/src/bin/list.rs index af4c57b..36cbe67 100644 --- a/src/bin/list.rs +++ b/src/bin/list.rs @@ -1,10 +1,8 @@ -use std::{collections::HashMap, env}; +use std::{array::IntoIter, 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<()> { @@ -19,57 +17,29 @@ fn main() -> Result<()> { 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); - } - } + let mut mails = Vec::new(); + for x in maildir.list_cur() { + mails.push(x?); } - 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 mails = maildir.get_mails(&mut mails)?; + mails.sort_by_key(|x| x.date); + + let mut rows = Vec::new(); + for (i, mail) in mails.iter().enumerate() { + let flags = &mail.flags; + 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(IntoIter::new([(mails.len() - i).to_string(), flags_display, mail.from.clone(), mail.subject.clone(), mail.date_iso.clone()])); } let mut ascii_table = AsciiTable::default(); @@ -98,9 +68,10 @@ fn show_listing(mailbox: &str) -> Result<()> { 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 { + 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 } diff --git a/src/bin/new.rs b/src/bin/new.rs index 3522bdb..db53dfb 100644 --- a/src/bin/new.rs +++ b/src/bin/new.rs @@ -1,9 +1,7 @@ -use std::env; +use std::{array::IntoIter, env}; use ascii_table::{Align, AsciiTable, Column}; -use chrono::{Local, NaiveDateTime, TimeZone}; use itertools::Itertools; -use mailparse::{MailHeaderMap, dateparse}; use inboxid::*; @@ -19,32 +17,16 @@ fn main() -> Result<()> { fn show_listing(mailbox: &str) -> Result<()> { let maildir = get_maildir(mailbox)?; + let mut mails = Vec::new(); + for x in maildir.list_new() { + mails.push(x?); + } + let mut mails = maildir.get_mails(&mut mails)?; + mails.sort_by_key(|x| x.id); + let mut rows = Vec::new(); - let mut seen = Vec::new(); - for mut maile in maildir.list_new_sorted(Box::new(|name| { - // sort by UID - 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)) => { - 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); - rows.push(vec![from, subj, date]); - seen.push(maile.as_ref().unwrap().id().to_owned()); - } - Ok(Err(e)) => { - println!("error parsing mail: {:?}", e); - } - Err(e) => { - println!("error: {:?}", e); - } - } + for mail in &mails { + rows.push(IntoIter::new([mail.from.clone(), mail.subject.clone(), mail.date_iso.clone()])); } let mut ascii_table = AsciiTable::default(); @@ -64,8 +46,8 @@ fn show_listing(mailbox: &str) -> Result<()> { ascii_table.print(rows); // prints a 0 if empty :) // only after the user saw the new mail, move it out of 'new' - for seen in seen { - maildir.move_new_to_cur(&seen)?; + for seen in mails { + maildir.move_new_to_cur(&seen.id.to_string())?; } Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 8b13984..12818ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,10 @@ -use std::{borrow::Cow, env, fs, io, net::TcpStream}; +use std::{borrow::Cow, convert::{TryFrom, TryInto}, env, fs, io, net::TcpStream, ops::Deref}; +use anyhow::Context; +use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; use imap::{Session, types::Flag}; -use maildir::Maildir; +use maildir::{MailEntry, Maildir}; +use mailparse::{MailHeaderMap, ParsedMail, dateparse}; use rusqlite::{Connection, params}; use rustls_connector::{RustlsConnector, rustls::{ClientSession, StreamOwned}}; @@ -48,9 +51,54 @@ pub fn gen_id(uid_validity: u32, uid: u32) -> String { format!("{}_{}", uid_validity, uid) } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct MaildirID { + uid_validity: u32, + uid: u32, +} + +impl TryFrom<&str> for MaildirID { + type Error = Box; + + fn try_from(id: &str) -> Result { + let mut parts = id.splitn(2, '_'); + let uid_validity = parts.next().context("invalid ID")?.parse()?; + let uid = parts.next().context("invalid ID")?.parse()?; + Ok(MaildirID { + uid_validity, + uid, + }) + } +} + +impl ToString for MaildirID { + fn to_string(&self) -> String { + format!("{}_{}", self.uid_validity, self.uid) + } +} + +pub struct EasyMail<'a> { + pub mail: ParsedMail<'a>, + pub id: MaildirID, + pub flags: String, + pub from: String, + pub subject: String, + pub date: DateTime, + pub date_iso: String, +} + +impl<'a> Deref for EasyMail<'a> { + type Target = ParsedMail<'a>; + + fn deref(&self) -> &Self::Target { + &self.mail + } +} + 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>; + fn get_mails<'a>(&self, entries: &'a mut [MailEntry]) -> Result>>; } impl MaildirExtension for Maildir { @@ -61,6 +109,32 @@ impl MaildirExtension for Maildir { fn save_file(&self, name: &str, content: &str) -> std::result::Result<(), io::Error> { fs::write(self.path().join(name), content) } + + fn get_mails<'a>(&self, entries: &'a mut [MailEntry]) -> Result>> { + 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, + 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 { @@ -73,13 +147,13 @@ pub fn load_i64(x: i64) -> u64 { pub fn remove_cow<'a>(x: &Flag<'a>) -> Flag<'static> { match x { - Flag::Custom(x) => Flag::Custom(Cow::Owned(x.to_string())), - Flag::Seen => Flag::Seen, - Flag::Answered => Flag::Answered, - Flag::Flagged => Flag::Flagged, - Flag::Deleted => Flag::Deleted, - Flag::Draft => Flag::Draft, - Flag::Recent => Flag::Recent, - Flag::MayCreate => Flag::MayCreate, + Flag::Custom(x) => Flag::Custom(Cow::Owned(x.to_string())), + Flag::Seen => Flag::Seen, + Flag::Answered => Flag::Answered, + Flag::Flagged => Flag::Flagged, + Flag::Deleted => Flag::Deleted, + Flag::Draft => Flag::Draft, + Flag::Recent => Flag::Recent, + Flag::MayCreate => Flag::MayCreate, } }