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",
]
[[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",

View File

@ -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"

View File

@ -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::<usize>().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::<usize>().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::<usize>().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
}

View File

@ -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(())

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 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<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 {
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 get_mails<'a>(&self, entries: &'a mut [MailEntry]) -> Result<Vec<EasyMail<'a>>>;
}
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<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 {
@ -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,
}
}