Refactor maildir reading into lib

This commit is contained in:
FliegendeWurst 2021-03-31 10:55:34 +02:00 committed by Arne Keller
parent 19347d3249
commit a349925f78
5 changed files with 148 additions and 113 deletions

41
Cargo.lock generated
View File

@ -17,6 +17,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "anyhow"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.5.2" version = "0.5.2"
@ -265,6 +271,7 @@ dependencies = [
name = "inboxid" name = "inboxid"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"ascii_table", "ascii_table",
"chrono", "chrono",
"imap", "imap",
@ -288,9 +295,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.49" version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc15e39392125075f60c95ba416f5381ff6c3a948ff02ab12464715adf56c821" checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
@ -316,9 +323,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.91" version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7" checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714"
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
@ -367,7 +374,7 @@ checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
[[package]] [[package]]
name = "moins" name = "moins"
version = "0.5.0" 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 = [ dependencies = [
"termion", "termion",
] ]
@ -772,9 +779,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.72" version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fe8f61dba8e5d645a4d8132dc7a0a66861ed5e1045d2c0ed940fab33bac0fbe" checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"wasm-bindgen-macro", "wasm-bindgen-macro",
@ -782,9 +789,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.72" version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046ceba58ff062da072c7cb4ba5b22a37f00a302483f7e2a6cdc18fedbdc1fd3" checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"lazy_static", "lazy_static",
@ -797,9 +804,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.72" version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef9aa01d36cda046f797c57959ff5f3c615c9cc63997a8d545831ec7976819b" checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -807,9 +814,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.72" version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96eb45c1b2ee33545a813a92dbb53856418bf7eb54ab34f7f7ff1448a5b3735d" checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -820,15 +827,15 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.72" version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7148f4696fb4960a346eaa60bbfb42a1ac4ebba21f750f75fc1375b098d5ffa" checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489"
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.49" version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fe19d70f5dacc03f6e46777213facae5ac3801575d56ca6cbd4c93dcd12310" checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",

View File

@ -19,3 +19,4 @@ chrono = "0.4.19"
rusqlite = { git = "https://github.com/rusqlite/rusqlite", branch = "master", features = ["bundled"] } rusqlite = { git = "https://github.com/rusqlite/rusqlite", branch = "master", features = ["bundled"] }
rustyline = "8.0.0" rustyline = "8.0.0"
moins = { git = "https://github.com/FliegendeWurst/moins", branch = "master" } moins = { git = "https://github.com/FliegendeWurst/moins", branch = "master" }
anyhow = "1.0.40"

View File

@ -1,10 +1,8 @@
use std::{collections::HashMap, env}; use std::{array::IntoIter, env};
use ascii_table::{Align, AsciiTable, Column}; use ascii_table::{Align, AsciiTable, Column};
use chrono::{Local, NaiveDateTime, TimeZone};
use inboxid::*; use inboxid::*;
use itertools::Itertools; use itertools::Itertools;
use mailparse::{MailHeaderMap, dateparse};
use rustyline::{Editor, error::ReadlineError}; use rustyline::{Editor, error::ReadlineError};
fn main() -> Result<()> { fn main() -> Result<()> {
@ -19,57 +17,29 @@ fn main() -> Result<()> {
fn show_listing(mailbox: &str) -> Result<()> { fn show_listing(mailbox: &str) -> Result<()> {
let maildir = get_maildir(mailbox)?; let maildir = get_maildir(mailbox)?;
let mut rows = Vec::new(); let mut mails = Vec::new();
let mut mail_list = Vec::new(); for x in maildir.list_cur() {
let mut i = 0; mails.push(x?);
// 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 mut mails = maildir.get_mails(&mut mails)?;
let count = rows.len(); mails.sort_by_key(|x| x.date);
let mut mails = HashMap::new();
for (i, row) in rows.iter_mut().enumerate() { let mut rows = Vec::new();
mails.insert(count - i, &mail_list[row[0].parse::<usize>().unwrap()]); for (i, mail) in mails.iter().enumerate() {
row[0] = (count - i).to_string(); 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(); let mut ascii_table = AsciiTable::default();
@ -98,9 +68,10 @@ fn show_listing(mailbox: &str) -> Result<()> {
let readline = rl.readline(">> "); let readline = rl.readline(">> ");
match readline { match readline {
Ok(line) => { Ok(line) => {
let mail = &mails[&line.trim().parse::<usize>().unwrap()]; let idx = mails.len() - line.trim().parse::<usize>().unwrap();
println!("{}", std::str::from_utf8(&mail.2.get_body_raw().unwrap()).unwrap()); let mail = &mails[idx];
for x in &mail.2.subparts { println!("{}", std::str::from_utf8(&mail.get_body_raw().unwrap()).unwrap());
for x in &mail.subparts {
if x.ctype.mimetype == "text/html" { if x.ctype.mimetype == "text/html" {
continue; // TODO continue; // TODO
} }

View File

@ -1,9 +1,7 @@
use std::env; use std::{array::IntoIter, env};
use ascii_table::{Align, AsciiTable, Column}; use ascii_table::{Align, AsciiTable, Column};
use chrono::{Local, NaiveDateTime, TimeZone};
use itertools::Itertools; use itertools::Itertools;
use mailparse::{MailHeaderMap, dateparse};
use inboxid::*; use inboxid::*;
@ -19,32 +17,16 @@ fn main() -> Result<()> {
fn show_listing(mailbox: &str) -> Result<()> { fn show_listing(mailbox: &str) -> Result<()> {
let maildir = get_maildir(mailbox)?; 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 rows = Vec::new();
let mut seen = Vec::new(); for mail in &mails {
for mut maile in maildir.list_new_sorted(Box::new(|name| { rows.push(IntoIter::new([mail.from.clone(), mail.subject.clone(), mail.date_iso.clone()]));
// 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);
}
}
} }
let mut ascii_table = AsciiTable::default(); 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 :) ascii_table.print(rows); // prints a 0 if empty :)
// only after the user saw the new mail, move it out of 'new' // only after the user saw the new mail, move it out of 'new'
for seen in seen { for seen in mails {
maildir.move_new_to_cur(&seen)?; maildir.move_new_to_cur(&seen.id.to_string())?;
} }
Ok(()) Ok(())

View File

@ -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 imap::{Session, types::Flag};
use maildir::Maildir; use maildir::{MailEntry, Maildir};
use mailparse::{MailHeaderMap, ParsedMail, dateparse};
use rusqlite::{Connection, params}; use rusqlite::{Connection, params};
use rustls_connector::{RustlsConnector, rustls::{ClientSession, StreamOwned}}; 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) 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<dyn std::error::Error>;
fn try_from(id: &str) -> Result<Self> {
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<Local>,
pub date_iso: String,
}
impl<'a> Deref for EasyMail<'a> {
type Target = ParsedMail<'a>;
fn deref(&self) -> &Self::Target {
&self.mail
}
}
pub trait MaildirExtension { 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>>>;
} }
impl MaildirExtension for Maildir { 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> { fn save_file(&self, name: &str, content: &str) -> std::result::Result<(), io::Error> {
fs::write(self.path().join(name), content) fs::write(self.path().join(name), content)
} }
fn get_mails<'a>(&self, entries: &'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,
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 {
@ -73,13 +147,13 @@ pub fn load_i64(x: i64) -> u64 {
pub fn remove_cow<'a>(x: &Flag<'a>) -> Flag<'static> { pub fn remove_cow<'a>(x: &Flag<'a>) -> Flag<'static> {
match x { match x {
Flag::Custom(x) => Flag::Custom(Cow::Owned(x.to_string())), Flag::Custom(x) => Flag::Custom(Cow::Owned(x.to_string())),
Flag::Seen => Flag::Seen, Flag::Seen => Flag::Seen,
Flag::Answered => Flag::Answered, Flag::Answered => Flag::Answered,
Flag::Flagged => Flag::Flagged, Flag::Flagged => Flag::Flagged,
Flag::Deleted => Flag::Deleted, Flag::Deleted => Flag::Deleted,
Flag::Draft => Flag::Draft, Flag::Draft => Flag::Draft,
Flag::Recent => Flag::Recent, Flag::Recent => Flag::Recent,
Flag::MayCreate => Flag::MayCreate, Flag::MayCreate => Flag::MayCreate,
} }
} }