From c87512dbd4df0f231a91267e4e00086fd6252ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 27 Feb 2022 10:23:58 +0100 Subject: [PATCH] make maildir envelopes selectable by short md5 hash --- Cargo.lock | 7 ++ Cargo.toml | 1 + src/backends/maildir/maildir_backend.rs | 108 +++++++++++++++++++++-- src/backends/maildir/maildir_envelope.rs | 5 +- 4 files changed, 112 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d0a101..665ee9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -452,6 +452,7 @@ dependencies = [ "log", "maildir", "mailparse", + "md5", "native-tls", "notmuch", "regex", @@ -712,6 +713,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "1.0.2" diff --git a/Cargo.toml b/Cargo.toml index 54930aa..51d8451 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ lettre = { version = "0.10.0-rc.1", features = ["serde"] } log = "0.4.14" maildir = "0.6.0" mailparse = "0.13.6" +md5 = "0.7.0" native-tls = "0.2.8" notmuch = { version = "0.7.1", optional = true } regex = "1.5.4" diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index 19e3812..cd365c7 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -1,5 +1,13 @@ use anyhow::{anyhow, Context, Result}; -use std::{convert::TryInto, fs, path::PathBuf}; +use std::{ + collections::HashSet, + convert::TryInto, + env::temp_dir, + fs::{self, OpenOptions}, + io::{BufRead, BufReader, Write}, + iter::FromIterator, + path::PathBuf, +}; use crate::{ backends::{Backend, MaildirEnvelopes, MaildirFlags, MaildirMboxes}, @@ -55,6 +63,48 @@ impl<'a> MaildirBackend<'a> { .map(maildir::Maildir::from) } } + + fn write_envelopes_cache(cache: &[u8]) -> Result<()> { + OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(temp_dir().join("himalaya-msg-id-hash-map")) + .context("cannot open maildir id hash map cache")? + .write(cache) + .map(|_| ()) + .context("cannot write maildir id hash map cache") + } + + fn get_id_from_short_hash(short_hash: &str) -> Result { + let path = temp_dir().join("himalaya-msg-id-hash-map"); + let file = OpenOptions::new() + .read(true) + .open(path) + .context("cannot open id hash map file")?; + let reader = BufReader::new(file); + let mut id_found = None; + for line in reader.lines() { + let line = line.context("cannot read id hash map line")?; + let line = line + .split_once(' ') + .ok_or_else(|| anyhow!("cannot parse id hash map line {:?}", line)); + match line { + Ok((id, hash)) if hash.starts_with(short_hash) => { + if id_found.is_some() { + return Err(anyhow!( + "cannot find id from hash {:?}: multiple match found", + short_hash + )); + } else { + id_found = Some(id.to_owned()) + } + } + _ => continue, + } + } + id_found.ok_or_else(|| anyhow!("cannot find id from hash {:?}", short_hash)) + } } impl<'a> Backend<'a> for MaildirBackend<'a> { @@ -80,11 +130,48 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { page: usize, ) -> Result> { let mdir = self.get_mdir_from_name(mdir)?; + let mut envelopes: MaildirEnvelopes = mdir .list_cur() .try_into() .context("cannot parse maildir envelopes from {:?}")?; + + Self::write_envelopes_cache( + envelopes + .iter() + .map(|env| format!("{} {:x}", env.id, md5::compute(&env.id))) + .collect::>() + .join("\n") + .as_bytes(), + )?; + envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); + envelopes + .iter_mut() + .for_each(|env| env.id = format!("{:x}", md5::compute(&env.id))); + + let mut short_id_len = 2; + loop { + let short_ids: Vec<_> = envelopes + .iter() + .map(|env| env.id[0..short_id_len].to_string()) + .collect(); + let short_ids_set: HashSet = HashSet::from_iter(short_ids.iter().cloned()); + + if short_id_len > 32 { + break; + } + + if short_ids.len() == short_ids_set.len() { + break; + } + + short_id_len += 1; + } + + envelopes + .iter_mut() + .for_each(|env| env.id = env.id[0..short_id_len].to_string()); let page_begin = page * page_size; if page_begin > envelopes.len() { @@ -95,6 +182,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { } let page_end = envelopes.len().min(page_begin + page_size); envelopes.0 = envelopes[page_begin..page_end].to_owned(); + Ok(Box::new(envelopes)) } @@ -123,19 +211,25 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { Ok(Box::new(id)) } - fn get_msg(&mut self, mdir: &str, id: &str) -> Result { + fn get_msg(&mut self, mdir: &str, hash: &str) -> Result { let mdir = self.get_mdir_from_name(mdir)?; - let mut mail_entry = mdir - .find(id) - .ok_or_else(|| anyhow!("cannot find maildir message {:?} in {:?}", id, mdir.path()))?; + let id = Self::get_id_from_short_hash(hash) + .context(format!("cannot get msg from hash {:?}", hash))?; + let mut mail_entry = mdir.find(&id).ok_or_else(|| { + anyhow!( + "cannot find maildir message {:?} in {:?}", + hash, + mdir.path() + ) + })?; let parsed_mail = mail_entry.parsed().context(format!( "cannot parse maildir message {:?} in {:?}", - id, + hash, mdir.path() ))?; Msg::from_parsed_mail(parsed_mail, self.account_config).context(format!( "cannot parse maildir message {:?} from {:?}", - id, + hash, mdir.path() )) } diff --git a/src/backends/maildir/maildir_envelope.rs b/src/backends/maildir/maildir_envelope.rs index 4d35f3f..2f87825 100644 --- a/src/backends/maildir/maildir_envelope.rs +++ b/src/backends/maildir/maildir_envelope.rs @@ -72,7 +72,7 @@ pub struct MaildirEnvelope { impl Table for MaildirEnvelope { fn head() -> Row { Row::new() - .cell(Cell::new("IDENTIFIER").bold().underline().white()) + .cell(Cell::new("HASH").bold().underline().white()) .cell(Cell::new("FLAGS").bold().underline().white()) .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) .cell(Cell::new("SENDER").bold().underline().white()) @@ -80,7 +80,7 @@ impl Table for MaildirEnvelope { } fn row(&self) -> Row { - let id = self.id.to_string(); + let id = self.id.clone(); let unseen = !self.flags.contains(&MaildirFlag::Seen); let flags = self.flags.to_symbols_string(); let subject = &self.subject; @@ -110,6 +110,7 @@ impl<'a> TryFrom for MaildirEnvelopes { .context("cannot parse maildir mail entry")?; envelopes.push(envelope); } + Ok(MaildirEnvelopes(envelopes)) } }