From 00728b88e474c4c579b78a0bcedf45d77f1d089b Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Wed, 23 Feb 2022 12:03:51 +0200 Subject: [PATCH 01/26] fix broken link to wiki on binary installation (#304) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9e961d..d52595d 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ curl -sSL https://raw.githubusercontent.com/soywod/himalaya/master/install.sh | ``` *See the -[wiki](https://github.com/soywod/himalaya/wiki/Installation:from-binary) +[wiki](https://github.com/soywod/himalaya/wiki/Installation:binary) for other installation methods.* ## Configuration From b146d9b7e12e9b8e6b790167f78a1aad153ea5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 23 Feb 2022 11:29:09 +0100 Subject: [PATCH 02/26] rollback rust edition (#303) --- CHANGELOG.md | 5 +++++ Cargo.toml | 2 +- src/backends/imap/imap_flag.rs | 6 +++++- src/msg/msg_entity.rs | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecc87fd..15f6e63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Rust edition transition [#303] + ## [0.5.6] - 2022-02-22 ### Added @@ -433,3 +437,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#280]: https://github.com/soywod/himalaya/issues/280 [#288]: https://github.com/soywod/himalaya/issues/288 [#289]: https://github.com/soywod/himalaya/issues/289 +[#303]: https://github.com/soywod/himalaya/issues/303 diff --git a/Cargo.toml b/Cargo.toml index 13c36e0..9bb05f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "himalaya" description = "Command-line interface for email management" version = "0.5.6" authors = ["soywod "] -edition = "2021" +edition = "2018" license-file = "LICENSE" readme = "README.md" categories = ["command-line-interface", "command-line-utilities", "email"] diff --git a/src/backends/imap/imap_flag.rs b/src/backends/imap/imap_flag.rs index a946fee..87bb8b9 100644 --- a/src/backends/imap/imap_flag.rs +++ b/src/backends/imap/imap_flag.rs @@ -1,5 +1,9 @@ use anyhow::{anyhow, Error, Result}; -use std::{convert::TryFrom, fmt, ops::Deref}; +use std::{ + convert::{TryFrom, TryInto}, + fmt, + ops::Deref, +}; /// Represents the imap flag variants. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] diff --git a/src/msg/msg_entity.rs b/src/msg/msg_entity.rs index e4f3b5c..2ca5441 100644 --- a/src/msg/msg_entity.rs +++ b/src/msg/msg_entity.rs @@ -5,7 +5,7 @@ use html_escape; use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart}; use log::{debug, info, trace}; use regex::Regex; -use std::{collections::HashSet, env::temp_dir, fmt::Debug, fs, path::PathBuf}; +use std::{collections::HashSet, convert::TryInto, env::temp_dir, fmt::Debug, fs, path::PathBuf}; use uuid::Uuid; use crate::{ From d5a494a01d94411dee96f5fe2372c30dffd5ed5a Mon Sep 17 00:00:00 2001 From: TornaxO7 <50843046+TornaxO7@users.noreply.github.com> Date: Wed, 23 Feb 2022 23:24:17 +0100 Subject: [PATCH 03/26] fix maildir expansion (#307) * Applied shellexpand to maildir_dir * change to account_config: applied shellexpand to maildir_dir * rustfmt: removing required version, formatted project * changing type of `maildir_dir` to `String` and adding shellexpand to `maildir_dir` --- rustfmt.toml | 1 - src/config/account_config.rs | 2 +- src/config/deserialized_account_config.rs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/rustfmt.toml b/rustfmt.toml index c39d2eb..a04de65 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -61,7 +61,6 @@ use_field_init_shorthand = false force_explicit_abi = true condense_wildcard_suffixes = false color = "Auto" -required_version = "1.4.37" unstable_features = false disable_all_formatting = false skip_children = false diff --git a/src/config/account_config.rs b/src/config/account_config.rs index 44c02c0..03bcc6a 100644 --- a/src/config/account_config.rs +++ b/src/config/account_config.rs @@ -191,7 +191,7 @@ impl<'a> AccountConfig { }), DeserializedAccountConfig::Maildir(config) => { BackendConfig::Maildir(MaildirBackendConfig { - maildir_dir: config.maildir_dir.clone(), + maildir_dir: shellexpand::full(&config.maildir_dir)?.to_string().into(), }) } }; diff --git a/src/config/deserialized_account_config.rs b/src/config/deserialized_account_config.rs index 595e50b..becfa81 100644 --- a/src/config/deserialized_account_config.rs +++ b/src/config/deserialized_account_config.rs @@ -121,4 +121,4 @@ make_account_config!( imap_passwd_cmd: String ); -make_account_config!(DeserializedMaildirAccountConfig, maildir_dir: PathBuf); +make_account_config!(DeserializedMaildirAccountConfig, maildir_dir: String); From 2b203b615cfe1651e42a836cb8092ab3cca86108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 23 Feb 2022 23:44:41 +0100 Subject: [PATCH 04/26] update changelog with #305 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15f6e63..fd171ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Rust edition transition [#303] +- No tilde expansion in `maildir-dir` [#305] ## [0.5.6] - 2022-02-22 @@ -438,3 +439,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#288]: https://github.com/soywod/himalaya/issues/288 [#289]: https://github.com/soywod/himalaya/issues/289 [#303]: https://github.com/soywod/himalaya/issues/303 +[#305]: https://github.com/soywod/himalaya/issues/305 From bd15e7d979bdfcc53b43c0637de4af339893b3c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 25 Feb 2022 18:55:42 +0100 Subject: [PATCH 05/26] pin imap version (#303) --- CHANGELOG.md | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd171ad..3880cb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Rust edition transition [#303] +- Build failure due to `imap` version [#303] - No tilde expansion in `maildir-dir` [#305] ## [0.5.6] - 2022-02-22 diff --git a/Cargo.toml b/Cargo.toml index 9bb05f0..ba79a24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ clap = { version = "2.33.3", default-features = false, features = ["suggestions" env_logger = "0.8.3" erased-serde = "0.3.18" html-escape = "0.2.9" -imap = "3.0.0-alpha.4" +imap = "=3.0.0-alpha.4" imap-proto = "0.14.3" lettre = { version = "0.10.0-rc.1", features = ["serde"] } log = "0.4.14" From e4aa5694580136291e0395a0607c71ee41e34727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 25 Feb 2022 18:52:42 +0100 Subject: [PATCH 06/26] init notmuch backend --- Cargo.lock | 81 +++++++++- Cargo.toml | 1 + src/backends/notmuch/notmuch_backend.rs | 101 +++++++++++++ src/backends/notmuch/notmuch_envelope.rs | 172 ++++++++++++++++++++++ src/config/account_config.rs | 18 +++ src/config/deserialized_account_config.rs | 7 + src/lib.rs | 9 ++ src/main.rs | 12 +- 8 files changed, 399 insertions(+), 2 deletions(-) create mode 100644 src/backends/notmuch/notmuch_backend.rs create mode 100644 src/backends/notmuch/notmuch_envelope.rs diff --git a/Cargo.lock b/Cargo.lock index 3f41163..3d0a101 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,7 +153,7 @@ dependencies = [ "ansi_term", "atty", "bitflags", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width", ] @@ -183,6 +183,41 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.9.3", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -281,6 +316,27 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "from_variants" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221a1eb1a3c98980bc1b740f462b3dcf73f4e371cda294986bac72497995a4e3" +dependencies = [ + "from_variants_impl", +] + +[[package]] +name = "from_variants_impl" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e08079fa3c89edec9160ceaa9e7172785468c26c053d12924cce0d5a55c241a" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "funty" version = "1.1.0" @@ -397,6 +453,7 @@ dependencies = [ "maildir", "mailparse", "native-tls", + "notmuch", "regex", "rfc2047-decoder", "serde", @@ -457,6 +514,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -732,6 +795,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "notmuch" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0941fd9af5b8529e3d42494f56efafb909b76190a7a454cde9d6e397390cf9" +dependencies = [ + "from_variants", + "libc", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -1241,6 +1314,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + [[package]] name = "syn" version = "1.0.81" diff --git a/Cargo.toml b/Cargo.toml index ba79a24..240dd9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ log = "0.4.14" maildir = "0.6.0" mailparse = "0.13.6" native-tls = "0.2.8" +notmuch = "0.7.1" regex = "1.5.4" rfc2047-decoder = "0.1.2" serde = { version = "1.0.118", features = ["derive"] } diff --git a/src/backends/notmuch/notmuch_backend.rs b/src/backends/notmuch/notmuch_backend.rs new file mode 100644 index 0000000..30ed63e --- /dev/null +++ b/src/backends/notmuch/notmuch_backend.rs @@ -0,0 +1,101 @@ +use std::convert::TryInto; + +use anyhow::{Context, Result}; + +use crate::{ + backends::Backend, + config::{AccountConfig, NotmuchBackendConfig}, + mbox::Mboxes, + msg::{Envelopes, Msg}, +}; + +use super::NotmuchEnvelopes; + +pub struct NotmuchBackend<'a> { + account_config: &'a AccountConfig, + db: notmuch::Database, +} + +impl<'a> NotmuchBackend<'a> { + pub fn new( + account_config: &'a AccountConfig, + notmuch_config: &'a NotmuchBackendConfig, + ) -> Result { + Ok(Self { + account_config, + db: notmuch::Database::open( + notmuch_config.notmuch_database_dir.clone(), + notmuch::DatabaseMode::ReadWrite, + ) + .context(format!( + "cannot open notmuch database at {:?}", + notmuch_config.notmuch_database_dir + ))?, + }) + } +} + +impl<'a> Backend<'a> for NotmuchBackend<'a> { + fn add_mbox(&mut self, mdir: &str) -> Result<()> { + unimplemented!(); + } + + fn get_mboxes(&mut self) -> Result> { + unimplemented!(); + } + + fn del_mbox(&mut self, mdir: &str) -> Result<()> { + unimplemented!(); + } + + fn get_envelopes( + &mut self, + mdir: &str, + _sort: &str, + filter: &str, + page_size: usize, + page: usize, + ) -> Result> { + let query = self + .db + .create_query(filter) + .context("cannot create query")?; + let msgs: NotmuchEnvelopes = query + .search_messages() + .context("cannot get messages")? + .try_into()?; + Ok(Box::new(msgs)) + } + + fn add_msg(&mut self, mdir: &str, msg: &[u8], flags: &str) -> Result> { + unimplemented!(); + } + + fn get_msg(&mut self, mdir: &str, id: &str) -> Result { + unimplemented!(); + } + + fn copy_msg(&mut self, mdir_src: &str, mdir_dst: &str, id: &str) -> Result<()> { + unimplemented!(); + } + + fn move_msg(&mut self, mdir_src: &str, mdir_dst: &str, id: &str) -> Result<()> { + unimplemented!(); + } + + fn del_msg(&mut self, mdir: &str, id: &str) -> Result<()> { + unimplemented!(); + } + + fn add_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> { + unimplemented!(); + } + + fn set_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> { + unimplemented!(); + } + + fn del_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> { + unimplemented!(); + } +} diff --git a/src/backends/notmuch/notmuch_envelope.rs b/src/backends/notmuch/notmuch_envelope.rs new file mode 100644 index 0000000..559191c --- /dev/null +++ b/src/backends/notmuch/notmuch_envelope.rs @@ -0,0 +1,172 @@ +//! Notmuch mailbox module. +//! +//! This module provides Notmuch types and conversion utilities +//! related to the envelope + +use anyhow::{anyhow, Context, Error, Result}; +use chrono::DateTime; +use log::{info, trace}; +use std::{ + convert::{TryFrom, TryInto}, + ops::{Deref, DerefMut}, +}; + +use crate::{ + msg::{from_slice_to_addrs, Addr}, + output::{PrintTable, PrintTableOpts, WriteColor}, + ui::{Cell, Row, Table}, +}; + +/// Represents a list of envelopes. +#[derive(Debug, Default, serde::Serialize)] +pub struct NotmuchEnvelopes(pub Vec); + +impl Deref for NotmuchEnvelopes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for NotmuchEnvelopes { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl PrintTable for NotmuchEnvelopes { + fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writter)?; + Table::print(writter, self, opts)?; + writeln!(writter)?; + Ok(()) + } +} + +/// Represents the envelope. The envelope is just a message subset, +/// and is mostly used for listings. +#[derive(Debug, Default, Clone, serde::Serialize)] +pub struct NotmuchEnvelope { + /// Represents the id of the message. + pub id: String, + + /// Represents the tags of the message. + pub flags: Vec, + + /// Represents the subject of the message. + pub subject: String, + + /// Represents the first sender of the message. + pub sender: String, + + /// Represents the date of the message. + pub date: String, +} + +impl Table for NotmuchEnvelope { + fn head() -> Row { + Row::new() + .cell(Cell::new("ID").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()) + .cell(Cell::new("DATE").bold().underline().white()) + } + + fn row(&self) -> Row { + let id = self.id.to_string(); + let unseen = !self.flags.contains(&String::from("unread")); + let flags = String::new(); + let subject = &self.subject; + let sender = &self.sender; + let date = &self.date; + Row::new() + .cell(Cell::new(id).bold_if(unseen).red()) + .cell(Cell::new(flags).bold_if(unseen).white()) + .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) + .cell(Cell::new(sender).bold_if(unseen).blue()) + .cell(Cell::new(date).bold_if(unseen).yellow()) + } +} + +/// Represents a list of raw envelopees returned by the `notmuch` crate. +pub type RawNotmuchEnvelopes = notmuch::Messages; + +impl<'a> TryFrom for NotmuchEnvelopes { + type Error = Error; + + fn try_from(raw_envelopes: RawNotmuchEnvelopes) -> Result { + let mut envelopes = vec![]; + for raw_envelope in raw_envelopes { + let envelope: NotmuchEnvelope = raw_envelope + .try_into() + .context("cannot parse notmuch mail entry")?; + envelopes.push(envelope); + } + Ok(NotmuchEnvelopes(envelopes)) + } +} + +/// Represents the raw envelope returned by the `notmuch` crate. +pub type RawNotmuchEnvelope = notmuch::Message; + +impl<'a> TryFrom for NotmuchEnvelope { + type Error = Error; + + fn try_from(raw_envelope: RawNotmuchEnvelope) -> Result { + info!("begin: try building envelope from notmuch parsed mail"); + + let id = raw_envelope.id().trim().to_string(); + let subject = raw_envelope + .header("subject") + .context("cannot get header \"Subject\" from notmuch message")? + .unwrap_or_default() + .to_string(); + let sender = raw_envelope + .header("from") + .context("cannot get header \"From\" from notmuch message")? + .ok_or_else(|| anyhow!("cannot parse sender from notmuch message {:?}", id))? + .to_string(); + let sender = from_slice_to_addrs(sender)? + .and_then(|senders| { + if senders.is_empty() { + None + } else { + Some(senders) + } + }) + .map(|senders| match &senders[0] { + Addr::Single(mailparse::SingleInfo { display_name, addr }) => { + display_name.as_ref().unwrap_or_else(|| addr).to_owned() + } + Addr::Group(mailparse::GroupInfo { group_name, .. }) => group_name.to_owned(), + }) + .ok_or_else(|| anyhow!("cannot find sender"))?; + let date = raw_envelope + .header("date") + .context("cannot get header \"Date\" from notmuch message")? + .ok_or_else(|| anyhow!("cannot parse date of notmuch message {:?}", id))? + .to_string(); + let date = + DateTime::parse_from_rfc2822(date.split_at(date.find(" (").unwrap_or(date.len())).0) + .context(format!( + "cannot parse message date {:?} of notmuch message {:?}", + date, id + ))? + .naive_local() + .to_string(); + + let envelope = Self { + id, + flags: raw_envelope.tags().collect(), + subject, + sender, + date, + }; + trace!("envelope: {:?}", envelope); + + info!("end: try building envelope from notmuch parsed mail"); + Ok(envelope) + } +} diff --git a/src/config/account_config.rs b/src/config/account_config.rs index 03bcc6a..81c1fa6 100644 --- a/src/config/account_config.rs +++ b/src/config/account_config.rs @@ -73,6 +73,9 @@ impl<'a> AccountConfig { DeserializedAccountConfig::Maildir(account) => { account.default.unwrap_or_default() } + DeserializedAccountConfig::Notmuch(account) => { + account.default.unwrap_or_default() + } }) .map(|(name, account)| (name.to_owned(), account)) .ok_or_else(|| anyhow!("cannot find default account")), @@ -194,6 +197,13 @@ impl<'a> AccountConfig { maildir_dir: shellexpand::full(&config.maildir_dir)?.to_string().into(), }) } + DeserializedAccountConfig::Notmuch(config) => { + BackendConfig::Notmuch(NotmuchBackendConfig { + notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir)? + .to_string() + .into(), + }) + } }; trace!("backend config: {:?}", backend_config); @@ -321,6 +331,7 @@ impl<'a> AccountConfig { pub enum BackendConfig { Imap(ImapBackendConfig), Maildir(MaildirBackendConfig), + Notmuch(NotmuchBackendConfig), } /// Represents the IMAP backend. @@ -358,6 +369,13 @@ pub struct MaildirBackendConfig { pub maildir_dir: PathBuf, } +/// Represents the Notmuch backend. +#[derive(Debug, Default, Clone)] +pub struct NotmuchBackendConfig { + /// Represents the Notmuch database path. + pub notmuch_database_dir: PathBuf, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/config/deserialized_account_config.rs b/src/config/deserialized_account_config.rs index becfa81..024cfca 100644 --- a/src/config/deserialized_account_config.rs +++ b/src/config/deserialized_account_config.rs @@ -11,6 +11,7 @@ pub trait ToDeserializedBaseAccountConfig { pub enum DeserializedAccountConfig { Imap(DeserializedImapAccountConfig), Maildir(DeserializedMaildirAccountConfig), + Notmuch(DeserializedNotmuchAccountConfig), } impl ToDeserializedBaseAccountConfig for DeserializedAccountConfig { @@ -18,6 +19,7 @@ impl ToDeserializedBaseAccountConfig for DeserializedAccountConfig { match self { Self::Imap(config) => config.to_base(), Self::Maildir(config) => config.to_base(), + Self::Notmuch(config) => config.to_base(), } } } @@ -122,3 +124,8 @@ make_account_config!( ); make_account_config!(DeserializedMaildirAccountConfig, maildir_dir: String); + +make_account_config!( + DeserializedNotmuchAccountConfig, + notmuch_database_dir: String +); diff --git a/src/lib.rs b/src/lib.rs index 625792f..898f536 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,6 +75,15 @@ pub mod backends { pub mod maildir_flag; pub use maildir_flag::*; } + + pub use self::notmuch::*; + pub mod notmuch { + pub mod notmuch_backend; + pub use notmuch_backend::*; + + pub mod notmuch_envelope; + pub use notmuch_envelope::*; + } } pub mod smtp { diff --git a/src/main.rs b/src/main.rs index c11fee0..e880ee3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use std::{convert::TryFrom, env}; use url::Url; use himalaya::{ - backends::{imap_arg, imap_handler, Backend, ImapBackend, MaildirBackend}, + backends::{imap_arg, imap_handler, Backend, ImapBackend, MaildirBackend, NotmuchBackend}, compl::{compl_arg, compl_handler}, config::{account_args, config_args, AccountConfig, BackendConfig, DeserializedConfig}, mbox::{mbox_arg, mbox_handler}, @@ -45,6 +45,7 @@ fn main() -> Result<()> { let mut imap; let mut maildir; + let mut notmuch; let backend: Box<&mut dyn Backend> = match backend_config { BackendConfig::Imap(ref imap_config) => { imap = ImapBackend::new(&account_config, imap_config); @@ -54,6 +55,10 @@ fn main() -> Result<()> { maildir = MaildirBackend::new(&account_config, maildir_config); Box::new(&mut maildir) } + BackendConfig::Notmuch(ref notmuch_config) => { + notmuch = NotmuchBackend::new(&account_config, notmuch_config)?; + Box::new(&mut notmuch) + } }; return msg_handler::mailto(&url, &account_config, &mut printer, backend, &mut smtp); @@ -81,6 +86,7 @@ fn main() -> Result<()> { let mut printer = StdoutPrinter::try_from(m.value_of("output"))?; let mut imap; let mut maildir; + let mut notmuch; let backend: Box<&mut dyn Backend> = match backend_config { BackendConfig::Imap(ref imap_config) => { imap = ImapBackend::new(&account_config, imap_config); @@ -90,6 +96,10 @@ fn main() -> Result<()> { maildir = MaildirBackend::new(&account_config, maildir_config); Box::new(&mut maildir) } + BackendConfig::Notmuch(ref notmuch_config) => { + notmuch = NotmuchBackend::new(&account_config, notmuch_config)?; + Box::new(&mut notmuch) + } }; let mut smtp = LettreService::from(&account_config); From 34ab0f4fa5d178e52df8f761e9ec77fcf93c4d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 25 Feb 2022 21:21:48 +0100 Subject: [PATCH 07/26] fix sort command not found (#308) --- CHANGELOG.md | 2 + src/backends/backend.rs | 8 ++- src/backends/imap/imap_backend.rs | 72 ++++++++++++++++++++----- src/backends/maildir/maildir_backend.rs | 22 +++++--- src/backends/notmuch/notmuch_backend.rs | 26 ++++++--- src/msg/msg_handler.rs | 6 +-- 6 files changed, 105 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3880cb3..8acd964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Build failure due to `imap` version [#303] - No tilde expansion in `maildir-dir` [#305] +- Unknown command SORT [#308] ## [0.5.6] - 2022-02-22 @@ -440,3 +441,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#289]: https://github.com/soywod/himalaya/issues/289 [#303]: https://github.com/soywod/himalaya/issues/303 [#305]: https://github.com/soywod/himalaya/issues/305 +[#308]: https://github.com/soywod/himalaya/issues/308 diff --git a/src/backends/backend.rs b/src/backends/backend.rs index 8162822..b7194f6 100644 --- a/src/backends/backend.rs +++ b/src/backends/backend.rs @@ -21,8 +21,14 @@ pub trait Backend<'a> { fn get_envelopes( &mut self, mbox: &str, + page_size: usize, + page: usize, + ) -> Result>; + fn find_envelopes( + &mut self, + mbox: &str, + query: &str, sort: &str, - filter: &str, page_size: usize, page: usize, ) -> Result>; diff --git a/src/backends/imap/imap_backend.rs b/src/backends/imap/imap_backend.rs index 8ed0b3b..5d328bf 100644 --- a/src/backends/imap/imap_backend.rs +++ b/src/backends/imap/imap_backend.rs @@ -229,8 +229,42 @@ impl<'a> Backend<'a> for ImapBackend<'a> { fn get_envelopes( &mut self, mbox: &str, + page_size: usize, + page: usize, + ) -> Result> { + let last_seq = self + .sess()? + .select(mbox) + .context(format!("cannot select mailbox {:?}", mbox))? + .exists as usize; + debug!("last sequence number: {:?}", last_seq); + if last_seq == 0 { + return Ok(Box::new(ImapEnvelopes::default())); + } + + let range = if page_size > 0 { + let cursor = page * page_size; + let begin = 1.max(last_seq - cursor); + let end = begin - begin.min(page_size) + 1; + format!("{}:{}", end, begin) + } else { + String::from("1:*") + }; + debug!("range: {:?}", range); + + let fetches = self + .sess()? + .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") + .context(format!("cannot fetch messages within range {:?}", range))?; + let envelopes: ImapEnvelopes = fetches.try_into()?; + Ok(Box::new(envelopes)) + } + + fn find_envelopes( + &mut self, + mbox: &str, + query: &str, sort: &str, - filter: &str, page_size: usize, page: usize, ) -> Result> { @@ -239,24 +273,36 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .select(mbox) .context(format!("cannot select mailbox {:?}", mbox))? .exists; + debug!("last sequence number: {:?}", last_seq); if last_seq == 0 { return Ok(Box::new(ImapEnvelopes::default())); } - let sort: SortCriteria = sort.try_into()?; - let charset = imap::extensions::sort::SortCharset::Utf8; let begin = page * page_size; let end = begin + (page_size - 1); - let seqs: Vec = self - .sess()? - .sort(&sort, charset, filter) - .context(format!( - "cannot search in {:?} with query {:?}", - mbox, filter - ))? - .iter() - .map(|seq| seq.to_string()) - .collect(); + let seqs: Vec = if sort.is_empty() { + self.sess()? + .search(query) + .context(format!( + "cannot find envelopes in {:?} with query {:?}", + mbox, query + ))? + .iter() + .map(|seq| seq.to_string()) + .collect() + } else { + let sort: SortCriteria = sort.try_into()?; + let charset = imap::extensions::sort::SortCharset::Utf8; + self.sess()? + .sort(&sort, charset, query) + .context(format!( + "cannot find envelopes in {:?} with query {:?}", + mbox, query + ))? + .iter() + .map(|seq| seq.to_string()) + .collect() + }; if seqs.is_empty() { return Ok(Box::new(ImapEnvelopes::default())); } diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index 0fd184c..44618f2 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -69,17 +69,12 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { fn get_envelopes( &mut self, mdir: &str, - _sort: &str, - filter: &str, page_size: usize, page: usize, ) -> Result> { let mdir = self.get_mdir_from_name(mdir)?; - let mail_entries = match filter { - "new" => mdir.list_new(), - _ => mdir.list_cur(), - }; - let mut envelopes: MaildirEnvelopes = mail_entries + let mut envelopes: MaildirEnvelopes = mdir + .list_cur() .try_into() .context("cannot parse maildir envelopes from {:?}")?; envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); @@ -96,6 +91,19 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { Ok(Box::new(envelopes)) } + fn find_envelopes( + &mut self, + _mdir: &str, + _query: &str, + _sort: &str, + _page_size: usize, + _page: usize, + ) -> Result> { + Err(anyhow!( + "cannot find maildir envelopes: feature not implemented" + )) + } + fn add_msg(&mut self, mdir: &str, msg: &[u8], flags: &str) -> Result> { let mdir = self.get_mdir_from_name(mdir)?; let flags: MaildirFlags = flags.try_into()?; diff --git a/src/backends/notmuch/notmuch_backend.rs b/src/backends/notmuch/notmuch_backend.rs index 30ed63e..99ff475 100644 --- a/src/backends/notmuch/notmuch_backend.rs +++ b/src/backends/notmuch/notmuch_backend.rs @@ -51,18 +51,30 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { fn get_envelopes( &mut self, mdir: &str, - _sort: &str, - filter: &str, page_size: usize, page: usize, ) -> Result> { - let query = self + unimplemented!(); + } + + fn find_envelopes( + &mut self, + _mdir: &str, + query: &str, + _sort: &str, + _page_size: usize, + _page: usize, + ) -> Result> { + let query_builder = self .db - .create_query(filter) - .context("cannot create query")?; - let msgs: NotmuchEnvelopes = query + .create_query(query) + .context("cannot create notmuch query")?; + let msgs: NotmuchEnvelopes = query_builder .search_messages() - .context("cannot get messages")? + .context(format!( + "cannot find notmuch envelopes with query {:?}", + query + ))? .try_into()?; Ok(Box::new(msgs)) } diff --git a/src/msg/msg_handler.rs b/src/msg/msg_handler.rs index 3bd5dd3..7cd093d 100644 --- a/src/msg/msg_handler.rs +++ b/src/msg/msg_handler.rs @@ -108,7 +108,7 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( ) -> Result<()> { let page_size = page_size.unwrap_or(config.default_page_size); debug!("page size: {}", page_size); - let msgs = imap.get_envelopes(mbox, "arrival:desc", "all", page_size, page)?; + let msgs = imap.get_envelopes(mbox, page_size, page)?; trace!("envelopes: {:?}", msgs); printer.print_table(msgs, PrintTableOpts { max_width }) } @@ -273,7 +273,7 @@ pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>( ) -> Result<()> { let page_size = page_size.unwrap_or(config.default_page_size); debug!("page size: {}", page_size); - let msgs = backend.get_envelopes(mbox, "arrival:desc", &query, page_size, page)?; + let msgs = backend.find_envelopes(mbox, &query, "", page_size, page)?; trace!("messages: {:#?}", msgs); printer.print_table(msgs, PrintTableOpts { max_width }) } @@ -292,7 +292,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>( ) -> Result<()> { let page_size = page_size.unwrap_or(config.default_page_size); debug!("page size: {}", page_size); - let msgs = backend.get_envelopes(mbox, &sort, &query, page_size, page)?; + let msgs = backend.find_envelopes(mbox, &query, &sort, page_size, page)?; trace!("envelopes: {:#?}", msgs); printer.print_table(msgs, PrintTableOpts { max_width }) } From b855c445087c082880e06d0df73e30459e4df538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 25 Feb 2022 21:56:48 +0100 Subject: [PATCH 08/26] replace `xxx-folder` config props by `mailboxes` --- CHANGELOG.md | 4 +++ src/backends/maildir/maildir_backend.rs | 11 +++++++-- src/config/account_config.rs | 30 ++++------------------- src/config/deserialized_account_config.rs | 17 ++++++------- src/config/deserialized_config.rs | 10 ++------ src/main.rs | 8 ++++-- src/msg/msg_entity.rs | 21 ++++++++++------ src/msg/msg_handler.rs | 12 ++++++--- 8 files changed, 56 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8acd964..0ec8fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - No tilde expansion in `maildir-dir` [#305] - Unknown command SORT [#308] +### Changed + +- [**BREAKING**] Replace `inbox-folder`, `sent-folder` and `draft-folder` by a generic hashmap `mailboxes` + ## [0.5.6] - 2022-02-22 ### Added diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index 44618f2..19e3812 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -3,7 +3,7 @@ use std::{convert::TryInto, fs, path::PathBuf}; use crate::{ backends::{Backend, MaildirEnvelopes, MaildirFlags, MaildirMboxes}, - config::{AccountConfig, MaildirBackendConfig}, + config::{AccountConfig, MaildirBackendConfig, DEFAULT_INBOX_FOLDER}, mbox::Mboxes, msg::{Envelopes, Msg}, }; @@ -36,7 +36,14 @@ impl<'a> MaildirBackend<'a> { } fn get_mdir_from_name(&self, mdir: &str) -> Result { - if mdir == self.account_config.inbox_folder { + let inbox_folder = self + .account_config + .mailboxes + .get("inbox") + .map(|s| s.as_str()) + .unwrap_or(DEFAULT_INBOX_FOLDER); + + if mdir == inbox_folder { self.validate_mdir_path(self.mdir.path().to_owned()) .map(maildir::Maildir::from) } else { diff --git a/src/config/account_config.rs b/src/config/account_config.rs index 81c1fa6..b222abd 100644 --- a/src/config/account_config.rs +++ b/src/config/account_config.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Context, Result}; use lettre::transport::smtp::authentication::Credentials as SmtpCredentials; use log::{debug, info, trace}; use mailparse::MailAddr; -use std::{env, ffi::OsStr, fs, path::PathBuf}; +use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf}; use crate::{config::*, output::run_cmd}; @@ -23,12 +23,6 @@ pub struct AccountConfig { pub sig: Option, /// Represents the default page size for listings. pub default_page_size: usize, - /// Represents the inbox folder name for this account. - pub inbox_folder: String, - /// Represents the sent folder name for this account. - pub sent_folder: String, - /// Represents the draft folder name for this account. - pub draft_folder: String, /// Represents the notify command. pub notify_cmd: Option, /// Overrides the default IMAP query "NEW" used to fetch new messages @@ -36,6 +30,9 @@ pub struct AccountConfig { /// Represents the watch commands. pub watch_cmds: Vec, + /// Represents mailbox aliases. + pub mailboxes: HashMap, + /// Represents the SMTP host. pub smtp_host: String, /// Represents the SMTP port. @@ -137,24 +134,6 @@ impl<'a> AccountConfig { downloads_dir, sig, default_page_size, - inbox_folder: base_account - .inbox_folder - .as_deref() - .or_else(|| config.inbox_folder.as_deref()) - .unwrap_or(DEFAULT_INBOX_FOLDER) - .to_string(), - sent_folder: base_account - .sent_folder - .as_deref() - .or_else(|| config.sent_folder.as_deref()) - .unwrap_or(DEFAULT_SENT_FOLDER) - .to_string(), - draft_folder: base_account - .draft_folder - .as_deref() - .or_else(|| config.draft_folder.as_deref()) - .unwrap_or(DEFAULT_DRAFT_FOLDER) - .to_string(), notify_cmd: base_account.notify_cmd.clone(), notify_query: base_account .notify_query @@ -168,6 +147,7 @@ impl<'a> AccountConfig { .or_else(|| config.watch_cmds.as_ref()) .unwrap_or(&vec![]) .to_owned(), + mailboxes: base_account.mailboxes.clone(), default: base_account.default.unwrap_or_default(), email: base_account.email.to_owned(), diff --git a/src/config/deserialized_account_config.rs b/src/config/deserialized_account_config.rs index 024cfca..4f5c440 100644 --- a/src/config/deserialized_account_config.rs +++ b/src/config/deserialized_account_config.rs @@ -1,5 +1,5 @@ use serde::Deserialize; -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; pub trait ToDeserializedBaseAccountConfig { fn to_base(&self) -> DeserializedBaseAccountConfig; @@ -39,12 +39,6 @@ macro_rules! make_account_config { pub signature_delimiter: Option, /// Overrides the default page size for this account. pub default_page_size: Option, - /// Overrides the inbox folder name for this account. - pub inbox_folder: Option, - /// Overrides the sent folder name for this account. - pub sent_folder: Option, - /// Overrides the draft folder name for this account. - pub draft_folder: Option, /// Overrides the notify command for this account. pub notify_cmd: Option, /// Overrides the IMAP query used to fetch new messages for this account. @@ -75,6 +69,10 @@ macro_rules! make_account_config { /// Represents the command used to decrypt a message. pub pgp_decrypt_cmd: Option, + /// Represents mailbox aliases. + #[serde(default)] + pub mailboxes: HashMap, + $(pub $element: $ty),* } @@ -86,9 +84,6 @@ macro_rules! make_account_config { signature: self.signature.clone(), signature_delimiter: self.signature_delimiter.clone(), default_page_size: self.default_page_size.clone(), - inbox_folder: self.inbox_folder.clone(), - sent_folder: self.sent_folder.clone(), - draft_folder: self.draft_folder.clone(), notify_cmd: self.notify_cmd.clone(), notify_query: self.notify_query.clone(), watch_cmds: self.watch_cmds.clone(), @@ -105,6 +100,8 @@ macro_rules! make_account_config { pgp_encrypt_cmd: self.pgp_encrypt_cmd.clone(), pgp_decrypt_cmd: self.pgp_decrypt_cmd.clone(), + + mailboxes: self.mailboxes.clone(), } } } diff --git a/src/config/deserialized_config.rs b/src/config/deserialized_config.rs index 280b6fb..7067162 100644 --- a/src/config/deserialized_config.rs +++ b/src/config/deserialized_config.rs @@ -27,12 +27,6 @@ pub struct DeserializedConfig { pub signature_delimiter: Option, /// Represents the default page size for listings. pub default_page_size: Option, - /// Overrides the default inbox folder name "INBOX". - pub inbox_folder: Option, - /// Overrides the default sent folder name "Sent". - pub sent_folder: Option, - /// Overrides the default draft folder name "Drafts". - pub draft_folder: Option, /// Represents the notify command. pub notify_cmd: Option, /// Overrides the default IMAP query "NEW" used to fetch new messages @@ -48,12 +42,12 @@ pub struct DeserializedConfig { impl DeserializedConfig { /// Tries to create a config from an optional path. pub fn from_opt_path(path: Option<&str>) -> Result { - info!("begin: trying to parse config from path"); + info!("begin: try to parse config from path"); debug!("path: {:?}", path); let path = path.map(|s| s.into()).unwrap_or(Self::path()?); let content = fs::read_to_string(path).context("cannot read config file")?; let config = toml::from_str(&content).context("cannot parse config file")?; - info!("end: trying to parse config from path"); + info!("end: try to parse config from path"); trace!("config: {:?}", config); Ok(config) } diff --git a/src/main.rs b/src/main.rs index e880ee3..f4db6b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,10 @@ use url::Url; use himalaya::{ backends::{imap_arg, imap_handler, Backend, ImapBackend, MaildirBackend, NotmuchBackend}, compl::{compl_arg, compl_handler}, - config::{account_args, config_args, AccountConfig, BackendConfig, DeserializedConfig}, + config::{ + account_args, config_args, AccountConfig, BackendConfig, DeserializedConfig, + DEFAULT_INBOX_FOLDER, + }, mbox::{mbox_arg, mbox_handler}, msg::{flag_arg, flag_handler, msg_arg, msg_handler, tpl_arg, tpl_handler}, output::{output_arg, OutputFmt, StdoutPrinter}, @@ -82,7 +85,8 @@ fn main() -> Result<()> { AccountConfig::from_config_and_opt_account_name(&config, m.value_of("account"))?; let mbox = m .value_of("mbox-source") - .unwrap_or(&account_config.inbox_folder); + .or_else(|| account_config.mailboxes.get("inbox").map(|s| s.as_str())) + .unwrap_or(DEFAULT_INBOX_FOLDER); let mut printer = StdoutPrinter::try_from(m.value_of("output"))?; let mut imap; let mut maildir; diff --git a/src/msg/msg_entity.rs b/src/msg/msg_entity.rs index 2ca5441..3fa4483 100644 --- a/src/msg/msg_entity.rs +++ b/src/msg/msg_entity.rs @@ -10,7 +10,7 @@ use uuid::Uuid; use crate::{ backends::Backend, - config::{AccountConfig, DEFAULT_SIG_DELIM}, + config::{AccountConfig, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER, DEFAULT_SIG_DELIM}, msg::{ from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, msg_utils, Addrs, BinaryPart, Part, Parts, TextPlainPart, TplOverride, @@ -340,7 +340,12 @@ impl Msg { match choice::post_edit() { Ok(PostEditChoice::Send) => { let sent_msg = smtp.send_msg(account, &self)?; - backend.add_msg(&account.sent_folder, &sent_msg.formatted(), "seen")?; + let sent_folder = account + .mailboxes + .get("sent") + .map(|s| s.as_str()) + .unwrap_or(DEFAULT_SENT_FOLDER); + backend.add_msg(&sent_folder, &sent_msg.formatted(), "seen")?; msg_utils::remove_local_draft()?; printer.print("Message successfully sent")?; break; @@ -355,12 +360,14 @@ impl Msg { } Ok(PostEditChoice::RemoteDraft) => { let tpl = self.to_tpl(TplOverride::default(), account)?; - backend.add_msg(&account.draft_folder, tpl.as_bytes(), "seen draft")?; + let draft_folder = account + .mailboxes + .get("draft") + .map(|s| s.as_str()) + .unwrap_or(DEFAULT_DRAFT_FOLDER); + backend.add_msg(&draft_folder, tpl.as_bytes(), "seen draft")?; msg_utils::remove_local_draft()?; - printer.print(format!( - "Message successfully saved to {}", - account.draft_folder - ))?; + printer.print(format!("Message successfully saved to {}", draft_folder))?; break; } Ok(PostEditChoice::Discard) => { diff --git a/src/msg/msg_handler.rs b/src/msg/msg_handler.rs index 7cd093d..300bcff 100644 --- a/src/msg/msg_handler.rs +++ b/src/msg/msg_handler.rs @@ -16,7 +16,7 @@ use url::Url; use crate::{ backends::Backend, - config::AccountConfig, + config::{AccountConfig, DEFAULT_SENT_FOLDER}, msg::{Msg, Part, Parts, TextPlainPart}, output::{PrintTableOpts, PrinterService}, smtp::SmtpService, @@ -312,6 +312,13 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( let is_json = printer.is_json(); debug!("is json: {}", is_json); + let sent_folder = config + .mailboxes + .get("sent") + .map(|s| s.as_str()) + .unwrap_or(DEFAULT_SENT_FOLDER); + debug!("sent folder: {:?}", sent_folder); + let raw_msg = if is_tty || is_json { raw_msg.replace("\r", "").replace("\n", "\r\n") } else { @@ -325,9 +332,8 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( trace!("raw message: {:?}", raw_msg); let envelope: lettre::address::Envelope = Msg::from_tpl(&raw_msg)?.try_into()?; trace!("envelope: {:?}", envelope); - smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?; - backend.add_msg(&config.sent_folder, raw_msg.as_bytes(), "seen")?; + backend.add_msg(&sent_folder, raw_msg.as_bytes(), "seen")?; Ok(()) } From 8766d8862a8b9e1f0275dff51eb8cbebe9ad0f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 25 Feb 2022 23:31:03 +0100 Subject: [PATCH 09/26] impl notmuch get_envelopes --- src/backends/notmuch/notmuch_backend.rs | 88 ++++++++++++++++++------- 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/src/backends/notmuch/notmuch_backend.rs b/src/backends/notmuch/notmuch_backend.rs index 99ff475..4d31e33 100644 --- a/src/backends/notmuch/notmuch_backend.rs +++ b/src/backends/notmuch/notmuch_backend.rs @@ -1,6 +1,6 @@ use std::convert::TryInto; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use crate::{ backends::Backend, @@ -36,7 +36,7 @@ impl<'a> NotmuchBackend<'a> { } impl<'a> Backend<'a> for NotmuchBackend<'a> { - fn add_mbox(&mut self, mdir: &str) -> Result<()> { + fn add_mbox(&mut self, _mbox: &str) -> Result<()> { unimplemented!(); } @@ -44,70 +44,108 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { unimplemented!(); } - fn del_mbox(&mut self, mdir: &str) -> Result<()> { + fn del_mbox(&mut self, _mbox: &str) -> Result<()> { unimplemented!(); } fn get_envelopes( &mut self, - mdir: &str, + mbox: &str, page_size: usize, page: usize, ) -> Result> { - unimplemented!(); - } - - fn find_envelopes( - &mut self, - _mdir: &str, - query: &str, - _sort: &str, - _page_size: usize, - _page: usize, - ) -> Result> { + let query = self + .account_config + .mailboxes + .get(mbox) + .map(|s| s.as_str()) + .unwrap_or("all"); let query_builder = self .db .create_query(query) .context("cannot create notmuch query")?; - let msgs: NotmuchEnvelopes = query_builder + let mut envelopes: NotmuchEnvelopes = query_builder .search_messages() .context(format!( "cannot find notmuch envelopes with query {:?}", query ))? .try_into()?; - Ok(Box::new(msgs)) + envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); + let page_begin = page * page_size; + if page_begin > envelopes.len() { + return Err(anyhow!(format!( + "cannot find notmuch envelopes at page {:?} (out of bounds)", + page_begin + 1, + ))); + } + let page_end = envelopes.len().min(page_begin + page_size); + envelopes.0 = envelopes[page_begin..page_end].to_owned(); + Ok(Box::new(envelopes)) } - fn add_msg(&mut self, mdir: &str, msg: &[u8], flags: &str) -> Result> { + fn find_envelopes( + &mut self, + _mbox: &str, + query: &str, + _sort: &str, + page_size: usize, + page: usize, + ) -> Result> { + let query_builder = self + .db + .create_query(query) + .context("cannot create notmuch query")?; + let mut envelopes: NotmuchEnvelopes = query_builder + .search_messages() + .context(format!( + "cannot find notmuch envelopes with query {:?}", + query + ))? + .try_into()?; + // TODO: use sort from parameters instead + envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); + let page_begin = page * page_size; + if page_begin > envelopes.len() { + return Err(anyhow!(format!( + "cannot find notmuch envelopes at page {:?} (out of bounds)", + page_begin + 1, + ))); + } + let page_end = envelopes.len().min(page_begin + page_size); + envelopes.0 = envelopes[page_begin..page_end].to_owned(); + Ok(Box::new(envelopes)) + } + + fn add_msg(&mut self, _mbox: &str, _msg: &[u8], _flags: &str) -> Result> { unimplemented!(); } - fn get_msg(&mut self, mdir: &str, id: &str) -> Result { + fn get_msg(&mut self, _mbox: &str, _id: &str) -> Result { unimplemented!(); } - fn copy_msg(&mut self, mdir_src: &str, mdir_dst: &str, id: &str) -> Result<()> { + fn copy_msg(&mut self, _mbox_src: &str, _mbox_dst: &str, _id: &str) -> Result<()> { unimplemented!(); } - fn move_msg(&mut self, mdir_src: &str, mdir_dst: &str, id: &str) -> Result<()> { + fn move_msg(&mut self, _mbox_src: &str, _mbox_dst: &str, _id: &str) -> Result<()> { unimplemented!(); } - fn del_msg(&mut self, mdir: &str, id: &str) -> Result<()> { + fn del_msg(&mut self, _mbox: &str, _id: &str) -> Result<()> { unimplemented!(); } - fn add_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> { + fn add_flags(&mut self, _mbox: &str, _id: &str, _flags_str: &str) -> Result<()> { unimplemented!(); } - fn set_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> { + fn set_flags(&mut self, _mbox: &str, _id: &str, _flags_str: &str) -> Result<()> { unimplemented!(); } - fn del_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> { + fn del_flags(&mut self, _mbox: &str, _id: &str, _flags_str: &str) -> Result<()> { unimplemented!(); } } From 7093cfc715433088669193210bdb2f1b373933b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 25 Feb 2022 23:52:18 +0100 Subject: [PATCH 10/26] implement notmuch get_msg --- src/backends/notmuch/notmuch_backend.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/backends/notmuch/notmuch_backend.rs b/src/backends/notmuch/notmuch_backend.rs index 4d31e33..74c10c1 100644 --- a/src/backends/notmuch/notmuch_backend.rs +++ b/src/backends/notmuch/notmuch_backend.rs @@ -1,16 +1,14 @@ -use std::convert::TryInto; +use std::{convert::TryInto, fs}; use anyhow::{anyhow, Context, Result}; use crate::{ - backends::Backend, + backends::{Backend, NotmuchEnvelopes}, config::{AccountConfig, NotmuchBackendConfig}, mbox::Mboxes, msg::{Envelopes, Msg}, }; -use super::NotmuchEnvelopes; - pub struct NotmuchBackend<'a> { account_config: &'a AccountConfig, db: notmuch::Database, @@ -121,8 +119,18 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { unimplemented!(); } - fn get_msg(&mut self, _mbox: &str, _id: &str) -> Result { - unimplemented!(); + fn get_msg(&mut self, _mbox: &str, id: &str) -> Result { + let msg_filepath = self + .db + .find_message(id) + .context(format!("cannot find notmuch message {:?}", id))? + .ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))? + .filename() + .to_owned(); + let raw_msg = fs::read(&msg_filepath) + .context(format!("cannot read message from file {:?}", msg_filepath))?; + let msg = Msg::from_parsed_mail(mailparse::parse_mail(&raw_msg)?, &self.account_config)?; + Ok(msg) } fn copy_msg(&mut self, _mbox_src: &str, _mbox_dst: &str, _id: &str) -> Result<()> { From da0e7889a34db2567ee90998c5e7f6203d44e27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 25 Feb 2022 23:59:05 +0100 Subject: [PATCH 11/26] implement notmuch del_msg --- src/backends/notmuch/notmuch_backend.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/backends/notmuch/notmuch_backend.rs b/src/backends/notmuch/notmuch_backend.rs index 74c10c1..dc8306a 100644 --- a/src/backends/notmuch/notmuch_backend.rs +++ b/src/backends/notmuch/notmuch_backend.rs @@ -141,8 +141,17 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { unimplemented!(); } - fn del_msg(&mut self, _mbox: &str, _id: &str) -> Result<()> { - unimplemented!(); + fn del_msg(&mut self, _mbox: &str, id: &str) -> Result<()> { + let msg_filepath = self + .db + .find_message(id) + .context(format!("cannot find notmuch message {:?}", id))? + .ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))? + .filename() + .to_owned(); + self.db + .remove_message(msg_filepath) + .context(format!("cannot delete notmuch message {:?}", id)) } fn add_flags(&mut self, _mbox: &str, _id: &str, _flags_str: &str) -> Result<()> { From 00e25246409b3eaff31933128a82f31c52d22b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 26 Feb 2022 00:13:14 +0100 Subject: [PATCH 12/26] implement notmuch get_mboxes --- src/backends/notmuch/notmuch_backend.rs | 11 +++- src/backends/notmuch/notmuch_mbox.rs | 80 +++++++++++++++++++++++++ src/lib.rs | 3 + 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 src/backends/notmuch/notmuch_mbox.rs diff --git a/src/backends/notmuch/notmuch_backend.rs b/src/backends/notmuch/notmuch_backend.rs index dc8306a..dbfd4c2 100644 --- a/src/backends/notmuch/notmuch_backend.rs +++ b/src/backends/notmuch/notmuch_backend.rs @@ -3,7 +3,7 @@ use std::{convert::TryInto, fs}; use anyhow::{anyhow, Context, Result}; use crate::{ - backends::{Backend, NotmuchEnvelopes}, + backends::{Backend, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes}, config::{AccountConfig, NotmuchBackendConfig}, mbox::Mboxes, msg::{Envelopes, Msg}, @@ -39,7 +39,14 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { } fn get_mboxes(&mut self) -> Result> { - unimplemented!(); + let mut mboxes: Vec<_> = self + .account_config + .mailboxes + .iter() + .map(|(k, v)| NotmuchMbox::new(k, v)) + .collect(); + mboxes.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap()); + Ok(Box::new(NotmuchMboxes(mboxes))) } fn del_mbox(&mut self, _mbox: &str) -> Result<()> { diff --git a/src/backends/notmuch/notmuch_mbox.rs b/src/backends/notmuch/notmuch_mbox.rs new file mode 100644 index 0000000..6cde8b5 --- /dev/null +++ b/src/backends/notmuch/notmuch_mbox.rs @@ -0,0 +1,80 @@ +//! Notmuch mailbox module. +//! +//! This module provides Notmuch types and conversion utilities +//! related to the mailbox + +use anyhow::Result; +use std::{ + fmt::{self, Display}, + ops::Deref, +}; + +use crate::{ + mbox::Mboxes, + output::{PrintTable, PrintTableOpts, WriteColor}, + ui::{Cell, Row, Table}, +}; + +/// Represents a list of Notmuch mailboxes. +#[derive(Debug, Default, serde::Serialize)] +pub struct NotmuchMboxes(pub Vec); + +impl Deref for NotmuchMboxes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PrintTable for NotmuchMboxes { + fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writter)?; + Table::print(writter, self, opts)?; + writeln!(writter)?; + Ok(()) + } +} + +impl Mboxes for NotmuchMboxes { + // +} + +/// Represents the notmuch virtual mailbox. +#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)] +pub struct NotmuchMbox { + /// Represents the virtual mailbox name. + pub name: String, + + /// Represents the query associated to the virtual mailbox name. + pub query: String, +} + +impl NotmuchMbox { + pub fn new(name: &str, query: &str) -> Self { + Self { + name: name.into(), + query: query.into(), + } + } +} + +impl Display for NotmuchMbox { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +impl Table for NotmuchMbox { + fn head() -> Row { + Row::new() + .cell(Cell::new("NAME").bold().underline().white()) + .cell(Cell::new("QUERY").bold().underline().white()) + } + + fn row(&self) -> Row { + Row::new() + .cell(Cell::new(&self.name).white()) + .cell(Cell::new(&self.query).green()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 898f536..5e9a09a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,6 +81,9 @@ pub mod backends { pub mod notmuch_backend; pub use notmuch_backend::*; + pub mod notmuch_mbox; + pub use notmuch_mbox::*; + pub mod notmuch_envelope; pub use notmuch_envelope::*; } From a2616fc1bdc5291bf101270945f0e75c923914db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 26 Feb 2022 09:56:26 +0100 Subject: [PATCH 13/26] make notmuch optional via cargo features (#303) --- Cargo.toml | 2 +- src/config/account_config.rs | 4 ++++ src/config/deserialized_account_config.rs | 3 +++ src/lib.rs | 2 ++ src/main.rs | 9 ++++++++- 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 240dd9a..54930aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ log = "0.4.14" maildir = "0.6.0" mailparse = "0.13.6" native-tls = "0.2.8" -notmuch = "0.7.1" +notmuch = { version = "0.7.1", optional = true } regex = "1.5.4" rfc2047-decoder = "0.1.2" serde = { version = "1.0.118", features = ["derive"] } diff --git a/src/config/account_config.rs b/src/config/account_config.rs index b222abd..20c51ca 100644 --- a/src/config/account_config.rs +++ b/src/config/account_config.rs @@ -70,6 +70,7 @@ impl<'a> AccountConfig { DeserializedAccountConfig::Maildir(account) => { account.default.unwrap_or_default() } + #[cfg(feature = "notmuch")] DeserializedAccountConfig::Notmuch(account) => { account.default.unwrap_or_default() } @@ -177,6 +178,7 @@ impl<'a> AccountConfig { maildir_dir: shellexpand::full(&config.maildir_dir)?.to_string().into(), }) } + #[cfg(feature = "notmuch")] DeserializedAccountConfig::Notmuch(config) => { BackendConfig::Notmuch(NotmuchBackendConfig { notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir)? @@ -311,6 +313,7 @@ impl<'a> AccountConfig { pub enum BackendConfig { Imap(ImapBackendConfig), Maildir(MaildirBackendConfig), + #[cfg(feature = "notmuch")] Notmuch(NotmuchBackendConfig), } @@ -350,6 +353,7 @@ pub struct MaildirBackendConfig { } /// Represents the Notmuch backend. +#[cfg(feature = "notmuch")] #[derive(Debug, Default, Clone)] pub struct NotmuchBackendConfig { /// Represents the Notmuch database path. diff --git a/src/config/deserialized_account_config.rs b/src/config/deserialized_account_config.rs index 4f5c440..80b59fb 100644 --- a/src/config/deserialized_account_config.rs +++ b/src/config/deserialized_account_config.rs @@ -11,6 +11,7 @@ pub trait ToDeserializedBaseAccountConfig { pub enum DeserializedAccountConfig { Imap(DeserializedImapAccountConfig), Maildir(DeserializedMaildirAccountConfig), + #[cfg(feature = "notmuch")] Notmuch(DeserializedNotmuchAccountConfig), } @@ -19,6 +20,7 @@ impl ToDeserializedBaseAccountConfig for DeserializedAccountConfig { match self { Self::Imap(config) => config.to_base(), Self::Maildir(config) => config.to_base(), + #[cfg(feature = "notmuch")] Self::Notmuch(config) => config.to_base(), } } @@ -122,6 +124,7 @@ make_account_config!( make_account_config!(DeserializedMaildirAccountConfig, maildir_dir: String); +#[cfg(feature = "notmuch")] make_account_config!( DeserializedNotmuchAccountConfig, notmuch_database_dir: String diff --git a/src/lib.rs b/src/lib.rs index 5e9a09a..48b162d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,7 +76,9 @@ pub mod backends { pub use maildir_flag::*; } + #[cfg(feature = "notmuch")] pub use self::notmuch::*; + #[cfg(feature = "notmuch")] pub mod notmuch { pub mod notmuch_backend; pub use notmuch_backend::*; diff --git a/src/main.rs b/src/main.rs index f4db6b7..c99d4cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use std::{convert::TryFrom, env}; use url::Url; use himalaya::{ - backends::{imap_arg, imap_handler, Backend, ImapBackend, MaildirBackend, NotmuchBackend}, + backends::{imap_arg, imap_handler, Backend, ImapBackend, MaildirBackend}, compl::{compl_arg, compl_handler}, config::{ account_args, config_args, AccountConfig, BackendConfig, DeserializedConfig, @@ -15,6 +15,9 @@ use himalaya::{ smtp::LettreService, }; +#[cfg(feature = "notmuch")] +use himalaya::backends::NotmuchBackend; + fn create_app<'a>() -> clap::App<'a, 'a> { clap::App::new(env!("CARGO_PKG_NAME")) .version(env!("CARGO_PKG_VERSION")) @@ -48,6 +51,7 @@ fn main() -> Result<()> { let mut imap; let mut maildir; + #[cfg(feature = "notmuch")] let mut notmuch; let backend: Box<&mut dyn Backend> = match backend_config { BackendConfig::Imap(ref imap_config) => { @@ -58,6 +62,7 @@ fn main() -> Result<()> { maildir = MaildirBackend::new(&account_config, maildir_config); Box::new(&mut maildir) } + #[cfg(feature = "notmuch")] BackendConfig::Notmuch(ref notmuch_config) => { notmuch = NotmuchBackend::new(&account_config, notmuch_config)?; Box::new(&mut notmuch) @@ -90,6 +95,7 @@ fn main() -> Result<()> { let mut printer = StdoutPrinter::try_from(m.value_of("output"))?; let mut imap; let mut maildir; + #[cfg(feature = "notmuch")] let mut notmuch; let backend: Box<&mut dyn Backend> = match backend_config { BackendConfig::Imap(ref imap_config) => { @@ -100,6 +106,7 @@ fn main() -> Result<()> { maildir = MaildirBackend::new(&account_config, maildir_config); Box::new(&mut maildir) } + #[cfg(feature = "notmuch")] BackendConfig::Notmuch(ref notmuch_config) => { notmuch = NotmuchBackend::new(&account_config, notmuch_config)?; Box::new(&mut notmuch) 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 14/26] 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)) } } From 5b002b1f30adbc77710a517f0420003f12ad4396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 27 Feb 2022 22:36:09 +0100 Subject: [PATCH 15/26] improve maildir id <-> hash mapping --- src/backends/backend.rs | 2 +- src/backends/imap/imap_backend.rs | 2 +- src/backends/maildir/maildir_backend.rs | 327 +++++++++++++++-------- src/backends/maildir/maildir_envelope.rs | 21 +- src/mbox/mbox_handler.rs | 5 +- src/msg/msg_handler.rs | 4 +- tests/test_imap_backend.rs | 20 +- tests/test_maildir_backend.rs | 32 +-- 8 files changed, 253 insertions(+), 160 deletions(-) diff --git a/src/backends/backend.rs b/src/backends/backend.rs index b7194f6..d00ad1f 100644 --- a/src/backends/backend.rs +++ b/src/backends/backend.rs @@ -24,7 +24,7 @@ pub trait Backend<'a> { page_size: usize, page: usize, ) -> Result>; - fn find_envelopes( + fn search_envelopes( &mut self, mbox: &str, query: &str, diff --git a/src/backends/imap/imap_backend.rs b/src/backends/imap/imap_backend.rs index 5d328bf..f6319e5 100644 --- a/src/backends/imap/imap_backend.rs +++ b/src/backends/imap/imap_backend.rs @@ -260,7 +260,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { Ok(Box::new(envelopes)) } - fn find_envelopes( + fn search_envelopes( &mut self, mbox: &str, query: &str, diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index cd365c7..94b0f26 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -1,11 +1,11 @@ use anyhow::{anyhow, Context, Result}; use std::{ - collections::HashSet, + collections::HashMap, convert::TryInto, env::temp_dir, fs::{self, OpenOptions}, io::{BufRead, BufReader, Write}, - iter::FromIterator, + ops::{Deref, DerefMut}, path::PathBuf, }; @@ -16,6 +16,134 @@ use crate::{ msg::{Envelopes, Msg}, }; +#[derive(Debug, Default)] +pub struct MaildirEnvelopesIdHashMapper { + path: PathBuf, + map: HashMap, + short_hash_len: usize, +} + +impl MaildirEnvelopesIdHashMapper { + fn get_cache_path(mdir: &maildir::Maildir) -> PathBuf { + let path_digest = md5::compute(format!("{:?}", mdir.path())); + let file_name = format!("himalaya-hash-map-{:x}", path_digest); + temp_dir().join(file_name) + } + + pub fn new(mdir: &maildir::Maildir) -> Result { + let mut mapper = Self::default(); + mapper.path = Self::get_cache_path(mdir); + + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&mapper.path) + .context("cannot open id hash map file")?; + let reader = BufReader::new(file); + + for line in reader.lines() { + let line = + line.context("cannot read line from maildir envelopes id mapper cache file")?; + if mapper.short_hash_len == 0 { + mapper.short_hash_len = 2.max(line.parse().unwrap_or(2)); + } else { + let (hash, id) = line.split_once(' ').ok_or_else(|| { + anyhow!( + "cannot parse line {:?} from maildir envelopes id mapper cache file", + line + ) + })?; + mapper.insert(hash.to_owned(), id.to_owned()); + } + } + + Ok(mapper) + } + + pub fn find(&self, short_hash: &str) -> Result { + let matching_hashes: Vec<_> = self + .keys() + .filter(|hash| hash.starts_with(short_hash)) + .collect(); + if matching_hashes.len() == 0 { + Err(anyhow!( + "cannot find maildir message id from short hash {:?}", + short_hash, + )) + } else if matching_hashes.len() > 1 { + Err(anyhow!( + "the short hash {:?} matches more than one hash: {}", + short_hash, + matching_hashes + .iter() + .map(|s| s.to_string()) + .collect::>() + .join(", ") + ) + .context(format!( + "cannot find maildir message id from short hash {:?}", + short_hash + ))) + } else { + Ok(self.get(matching_hashes[0]).unwrap().to_owned()) + } + } + + fn append(&mut self, lines: Vec<(String, String)>) -> Result { + let mut entries = String::new(); + + self.extend(lines.clone()); + + for (hash, id) in self.iter() { + entries.push_str(&format!("{} {}\n", hash, id)); + } + + for (hash, id) in lines { + loop { + let short_hash = &hash[0..self.short_hash_len]; + let conflict_found = self + .map + .keys() + .find(|cached_hash| { + cached_hash.starts_with(short_hash) && *cached_hash != &hash + }) + .is_some(); + if self.short_hash_len > 32 || !conflict_found { + break; + } + self.short_hash_len += 1; + } + entries.push_str(&format!("{} {}\n", hash, id)); + } + + OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&self.path) + .context("cannot open maildir id hash map cache")? + .write(format!("{}\n{}", self.short_hash_len, entries).as_bytes()) + .context("cannot write maildir id hash map cache")?; + + Ok(self.short_hash_len) + } +} + +impl Deref for MaildirEnvelopesIdHashMapper { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl DerefMut for MaildirEnvelopesIdHashMapper { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} + pub struct MaildirBackend<'a> { mdir: maildir::Maildir, account_config: &'a AccountConfig, @@ -63,48 +191,6 @@ 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> { @@ -125,54 +211,20 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { fn get_envelopes( &mut self, - mdir: &str, + mdir_str: &str, page_size: usize, page: usize, ) -> Result> { - let mdir = self.get_mdir_from_name(mdir)?; + let mdir = self.get_mdir_from_name(mdir_str)?; + // Reads envelopes from the "cur" folder of the selected + // maildir. 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()); - + // Calculates pagination boundaries. let page_begin = page * page_size; if page_begin > envelopes.len() { return Err(anyhow!(format!( @@ -181,12 +233,34 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { ))); } let page_end = envelopes.len().min(page_begin + page_size); + + // Sorts envelopes by most recent date. + envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); + + // Applies pagination boundaries. envelopes.0 = envelopes[page_begin..page_end].to_owned(); + // Writes envelope ids and their hashes to a cache file. The + // cache file name is based on the name of the given maildir: + // this way there is one cache per maildir. + let short_hash_len = { + let mut mapper = MaildirEnvelopesIdHashMapper::new(&mdir)?; + let entries = envelopes + .iter() + .map(|env| (env.hash.to_owned(), env.id.to_owned())) + .collect(); + mapper.append(entries)? + }; + + // Shorten envelopes hash. + envelopes + .iter_mut() + .for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned()); + Ok(Box::new(envelopes)) } - fn find_envelopes( + fn search_envelopes( &mut self, _mdir: &str, _query: &str, @@ -204,89 +278,112 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let flags: MaildirFlags = flags.try_into()?; let id = mdir .store_cur_with_flags(msg, &flags.to_string()) - .context(format!( - "cannot add message to the \"cur\" folder of maildir {:?}", - mdir.path() - ))?; - Ok(Box::new(id)) + .context(format!("cannot add maildir message at {:?}", mdir.path()))?; + let hash = format!("{:x}", md5::compute(&id)); + + // Appends hash line to the maildir cache file. + let mut mapper = MaildirEnvelopesIdHashMapper::new(&mdir)?; + mapper.append(vec![(hash.clone(), id)])?; + + Ok(Box::new(hash)) } - fn get_msg(&mut self, mdir: &str, hash: &str) -> Result { + fn get_msg(&mut self, mdir: &str, short_hash: &str) -> Result { let mdir = self.get_mdir_from_name(mdir)?; - 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 id = MaildirEnvelopesIdHashMapper::new(&mdir)? + .find(short_hash) + .context(format!( + "cannot get maildir message from short hash {:?}", + short_hash + ))?; + let mut mail_entry = mdir + .find(&id) + .ok_or_else(|| anyhow!("cannot find maildir message {:?} in {:?}", id, mdir.path()))?; let parsed_mail = mail_entry.parsed().context(format!( "cannot parse maildir message {:?} in {:?}", - hash, + id, mdir.path() ))?; Msg::from_parsed_mail(parsed_mail, self.account_config).context(format!( "cannot parse maildir message {:?} from {:?}", - hash, + id, mdir.path() )) } - fn copy_msg(&mut self, mdir_src: &str, mdir_dst: &str, id: &str) -> Result<()> { + fn copy_msg(&mut self, mdir_src: &str, mdir_dst: &str, short_hash: &str) -> Result<()> { let mdir_src = self.get_mdir_from_name(mdir_src)?; let mdir_dst = self.get_mdir_from_name(mdir_dst)?; - mdir_src.copy_to(id, &mdir_dst).context(format!( + let id = MaildirEnvelopesIdHashMapper::new(&mdir_src)?.find(short_hash)?; + + mdir_src.copy_to(&id, &mdir_dst).context(format!( "cannot copy message {:?} from maildir {:?} to maildir {:?}", id, mdir_src.path(), mdir_dst.path() - )) + ))?; + + // Appends hash line to the destination maildir cache file. + MaildirEnvelopesIdHashMapper::new(&mdir_dst)? + .append(vec![(format!("{:x}", md5::compute(&id)), id)])?; + + Ok(()) } - fn move_msg(&mut self, mdir_src: &str, mdir_dst: &str, id: &str) -> Result<()> { + fn move_msg(&mut self, mdir_src: &str, mdir_dst: &str, short_hash: &str) -> Result<()> { let mdir_src = self.get_mdir_from_name(mdir_src)?; let mdir_dst = self.get_mdir_from_name(mdir_dst)?; - mdir_src.move_to(id, &mdir_dst).context(format!( + let id = MaildirEnvelopesIdHashMapper::new(&mdir_src)?.find(short_hash)?; + + mdir_src.move_to(&id, &mdir_dst).context(format!( "cannot move message {:?} from maildir {:?} to maildir {:?}", id, mdir_src.path(), mdir_dst.path() - )) + ))?; + + // Appends hash line to the destination maildir cache file. + MaildirEnvelopesIdHashMapper::new(&mdir_dst)? + .append(vec![(format!("{:x}", md5::compute(&id)), id)])?; + + Ok(()) } - fn del_msg(&mut self, mdir: &str, id: &str) -> Result<()> { + fn del_msg(&mut self, mdir: &str, short_hash: &str) -> Result<()> { let mdir = self.get_mdir_from_name(mdir)?; - mdir.delete(id).context(format!( + let id = MaildirEnvelopesIdHashMapper::new(&mdir)?.find(short_hash)?; + mdir.delete(&id).context(format!( "cannot delete message {:?} from maildir {:?}", id, mdir.path() )) } - fn add_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> { + fn add_flags(&mut self, mdir: &str, short_hash: &str, flags_str: &str) -> Result<()> { let mdir = self.get_mdir_from_name(mdir)?; + let id = MaildirEnvelopesIdHashMapper::new(&mdir)?.find(short_hash)?; let flags: MaildirFlags = flags_str.try_into()?; - mdir.add_flags(id, &flags.to_string()).context(format!( + mdir.add_flags(&id, &flags.to_string()).context(format!( "cannot add flags {:?} to maildir message {:?}", flags_str, id )) } - fn set_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> { + fn set_flags(&mut self, mdir: &str, short_hash: &str, flags_str: &str) -> Result<()> { let mdir = self.get_mdir_from_name(mdir)?; + let id = MaildirEnvelopesIdHashMapper::new(&mdir)?.find(short_hash)?; let flags: MaildirFlags = flags_str.try_into()?; - mdir.set_flags(id, &flags.to_string()).context(format!( + mdir.set_flags(&id, &flags.to_string()).context(format!( "cannot set flags {:?} to maildir message {:?}", flags_str, id )) } - fn del_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> { + fn del_flags(&mut self, mdir: &str, short_hash: &str, flags_str: &str) -> Result<()> { let mdir = self.get_mdir_from_name(mdir)?; + let id = MaildirEnvelopesIdHashMapper::new(&mdir)?.find(short_hash)?; let flags: MaildirFlags = flags_str.try_into()?; - mdir.remove_flags(id, &flags.to_string()).context(format!( + mdir.remove_flags(&id, &flags.to_string()).context(format!( "cannot remove flags {:?} from maildir message {:?}", flags_str, id )) diff --git a/src/backends/maildir/maildir_envelope.rs b/src/backends/maildir/maildir_envelope.rs index 2f87825..137be42 100644 --- a/src/backends/maildir/maildir_envelope.rs +++ b/src/backends/maildir/maildir_envelope.rs @@ -56,6 +56,9 @@ pub struct MaildirEnvelope { /// Represents the id of the message. pub id: String, + /// Represents the MD5 hash of the message id. + pub hash: String, + /// Represents the flags of the message. pub flags: MaildirFlags, @@ -80,14 +83,14 @@ impl Table for MaildirEnvelope { } fn row(&self) -> Row { - let id = self.id.clone(); + let hash = self.hash.clone(); let unseen = !self.flags.contains(&MaildirFlag::Seen); let flags = self.flags.to_symbols_string(); let subject = &self.subject; let sender = &self.sender; let date = &self.date; Row::new() - .cell(Cell::new(id).bold_if(unseen).red()) + .cell(Cell::new(hash).bold_if(unseen).red()) .cell(Cell::new(flags).bold_if(unseen).white()) .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) .cell(Cell::new(sender).bold_if(unseen).blue()) @@ -124,13 +127,13 @@ impl<'a> TryFrom for MaildirEnvelope { fn try_from(mut mail_entry: RawMaildirEnvelope) -> Result { info!("begin: try building envelope from maildir parsed mail"); - let mut envelope = Self { - id: mail_entry.id().into(), - flags: (&mail_entry) - .try_into() - .context("cannot parse maildir flags")?, - ..Self::default() - }; + let mut envelope = Self::default(); + + envelope.id = mail_entry.id().into(); + envelope.hash = format!("{:x}", md5::compute(&envelope.id)); + envelope.flags = (&mail_entry) + .try_into() + .context("cannot parse maildir flags")?; let parsed_mail = mail_entry .parsed() diff --git a/src/mbox/mbox_handler.rs b/src/mbox/mbox_handler.rs index 7bb27e4..4ed3aa0 100644 --- a/src/mbox/mbox_handler.rs +++ b/src/mbox/mbox_handler.rs @@ -120,7 +120,10 @@ mod tests { fn del_mbox(&mut self, _: &str) -> Result<()> { unimplemented!(); } - fn get_envelopes( + fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> Result> { + unimplemented!() + } + fn search_envelopes( &mut self, _: &str, _: &str, diff --git a/src/msg/msg_handler.rs b/src/msg/msg_handler.rs index 300bcff..c9e10ce 100644 --- a/src/msg/msg_handler.rs +++ b/src/msg/msg_handler.rs @@ -273,7 +273,7 @@ pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>( ) -> Result<()> { let page_size = page_size.unwrap_or(config.default_page_size); debug!("page size: {}", page_size); - let msgs = backend.find_envelopes(mbox, &query, "", page_size, page)?; + let msgs = backend.search_envelopes(mbox, &query, "", page_size, page)?; trace!("messages: {:#?}", msgs); printer.print_table(msgs, PrintTableOpts { max_width }) } @@ -292,7 +292,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>( ) -> Result<()> { let page_size = page_size.unwrap_or(config.default_page_size); debug!("page size: {}", page_size); - let msgs = backend.find_envelopes(mbox, &query, &sort, page_size, page)?; + let msgs = backend.search_envelopes(mbox, &query, &sort, page_size, page)?; trace!("envelopes: {:#?}", msgs); printer.print_table(msgs, PrintTableOpts { max_width }) } diff --git a/tests/test_imap_backend.rs b/tests/test_imap_backend.rs index 37c03e8..7356a92 100644 --- a/tests/test_imap_backend.rs +++ b/tests/test_imap_backend.rs @@ -43,9 +43,7 @@ fn test_imap_backend() { assert_eq!("Ceci est un message.", msg.fold_text_plain_parts()); // check that the envelope of the added message exists - let envelopes = imap - .get_envelopes("Mailbox1", "arrival:desc", "ALL", 10, 0) - .unwrap(); + let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap(); let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(1, envelopes.len()); let envelope = envelopes.first().unwrap(); @@ -55,28 +53,20 @@ fn test_imap_backend() { // check that the message can be copied imap.copy_msg("Mailbox1", "Mailbox2", &envelope.id.to_string()) .unwrap(); - let envelopes = imap - .get_envelopes("Mailbox1", "arrival:desc", "ALL", 10, 0) - .unwrap(); + let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap(); let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(1, envelopes.len()); - let envelopes = imap - .get_envelopes("Mailbox2", "arrival:desc", "ALL", 10, 0) - .unwrap(); + let envelopes = imap.get_envelopes("Mailbox2", 10, 0).unwrap(); let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(1, envelopes.len()); // check that the message can be moved imap.move_msg("Mailbox1", "Mailbox2", &envelope.id.to_string()) .unwrap(); - let envelopes = imap - .get_envelopes("Mailbox1", "arrival:desc", "ALL", 10, 0) - .unwrap(); + let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap(); let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(0, envelopes.len()); - let envelopes = imap - .get_envelopes("Mailbox2", "arrival:desc", "ALL", 10, 0) - .unwrap(); + let envelopes = imap.get_envelopes("Mailbox2", 10, 0).unwrap(); let envelopes: &ImapEnvelopes = envelopes.as_any().downcast_ref().unwrap(); assert_eq!(2, envelopes.len()); let id = envelopes.first().unwrap().id.to_string(); diff --git a/tests/test_maildir_backend.rs b/tests/test_maildir_backend.rs index cfc37a5..932ea13 100644 --- a/tests/test_maildir_backend.rs +++ b/tests/test_maildir_backend.rs @@ -1,5 +1,5 @@ use maildir::Maildir; -use std::{env, fs}; +use std::{collections::HashMap, env, fs, iter::FromIterator}; use himalaya::{ backends::{Backend, MaildirBackend, MaildirEnvelopes}, @@ -19,7 +19,7 @@ fn test_maildir_backend() { // configure accounts let account_config = AccountConfig { - inbox_folder: "INBOX".into(), + mailboxes: HashMap::from_iter([("inbox".into(), "INBOX".into())]), ..AccountConfig::default() }; let mdir_config = MaildirBackendConfig { @@ -33,16 +33,16 @@ fn test_maildir_backend() { // check that a message can be added let msg = include_bytes!("./emails/alice-to-patrick.eml"); - let id = mdir.add_msg("INBOX", msg, "seen").unwrap().to_string(); + let hash = mdir.add_msg("INBOX", msg, "seen").unwrap().to_string(); // check that the added message exists - let msg = mdir.get_msg("INBOX", &id).unwrap(); + let msg = mdir.get_msg("INBOX", &hash).unwrap(); assert_eq!("alice@localhost", msg.from.clone().unwrap().to_string()); assert_eq!("patrick@localhost", msg.to.clone().unwrap().to_string()); assert_eq!("Ceci est un message.", msg.fold_text_plain_parts()); // check that the envelope of the added message exists - let envelopes = mdir.get_envelopes("INBOX", "", "cur", 10, 0).unwrap(); + let envelopes = mdir.get_envelopes("INBOX", 10, 0).unwrap(); let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); assert_eq!(1, envelopes.len()); @@ -50,19 +50,19 @@ fn test_maildir_backend() { assert_eq!("Plain message", envelope.subject); // check that the message can be copied - mdir.copy_msg("INBOX", "Subdir", &envelope.id).unwrap(); - assert!(mdir.get_msg("INBOX", &id).is_ok()); - assert!(mdir.get_msg("Subdir", &id).is_ok()); - assert!(mdir_subdir.get_msg("INBOX", &id).is_ok()); + mdir.copy_msg("INBOX", "Subdir", &envelope.hash).unwrap(); + assert!(mdir.get_msg("INBOX", &hash).is_ok()); + assert!(mdir.get_msg("Subdir", &hash).is_ok()); + assert!(mdir_subdir.get_msg("INBOX", &hash).is_ok()); // check that the message can be moved - mdir.move_msg("INBOX", "Subdir", &envelope.id).unwrap(); - assert!(mdir.get_msg("INBOX", &id).is_err()); - assert!(mdir.get_msg("Subdir", &id).is_ok()); - assert!(mdir_subdir.get_msg("INBOX", &id).is_ok()); + mdir.move_msg("INBOX", "Subdir", &envelope.hash).unwrap(); + assert!(mdir.get_msg("INBOX", &hash).is_err()); + assert!(mdir.get_msg("Subdir", &hash).is_ok()); + assert!(mdir_subdir.get_msg("INBOX", &hash).is_ok()); // check that the message can be deleted - mdir.del_msg("Subdir", &id).unwrap(); - assert!(mdir.get_msg("Subdir", &id).is_err()); - assert!(mdir_subdir.get_msg("INBOX", &id).is_err()); + mdir.del_msg("Subdir", &hash).unwrap(); + assert!(mdir.get_msg("Subdir", &hash).is_err()); + assert!(mdir_subdir.get_msg("INBOX", &hash).is_err()); } From ad1f97faed38ef7119897722e8217ea70120b10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 27 Feb 2022 23:40:19 +0100 Subject: [PATCH 16/26] move id mapper to its dedicated module --- src/backends/id_mapper.rs | 130 +++++++++++++++++ src/backends/maildir/maildir_backend.rs | 178 +++--------------------- src/lib.rs | 3 + 3 files changed, 153 insertions(+), 158 deletions(-) create mode 100644 src/backends/id_mapper.rs diff --git a/src/backends/id_mapper.rs b/src/backends/id_mapper.rs new file mode 100644 index 0000000..7ca77a1 --- /dev/null +++ b/src/backends/id_mapper.rs @@ -0,0 +1,130 @@ +use anyhow::{anyhow, Context, Result}; +use std::{ + collections::HashMap, + fs::OpenOptions, + io::{BufRead, BufReader, Write}, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, +}; + +#[derive(Debug, Default)] +pub struct IdMapper { + path: PathBuf, + map: HashMap, + short_hash_len: usize, +} + +impl IdMapper { + pub fn new(dir: &Path) -> Result { + let mut mapper = Self::default(); + mapper.path = dir.join(".himalaya-id-map"); + + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&mapper.path) + .context("cannot open id hash map file")?; + let reader = BufReader::new(file); + + for line in reader.lines() { + let line = + line.context("cannot read line from maildir envelopes id mapper cache file")?; + if mapper.short_hash_len == 0 { + mapper.short_hash_len = 2.max(line.parse().unwrap_or(2)); + } else { + let (hash, id) = line.split_once(' ').ok_or_else(|| { + anyhow!( + "cannot parse line {:?} from maildir envelopes id mapper cache file", + line + ) + })?; + mapper.insert(hash.to_owned(), id.to_owned()); + } + } + + Ok(mapper) + } + + pub fn find(&self, short_hash: &str) -> Result { + let matching_hashes: Vec<_> = self + .keys() + .filter(|hash| hash.starts_with(short_hash)) + .collect(); + if matching_hashes.len() == 0 { + Err(anyhow!( + "cannot find maildir message id from short hash {:?}", + short_hash, + )) + } else if matching_hashes.len() > 1 { + Err(anyhow!( + "the short hash {:?} matches more than one hash: {}", + short_hash, + matching_hashes + .iter() + .map(|s| s.to_string()) + .collect::>() + .join(", ") + ) + .context(format!( + "cannot find maildir message id from short hash {:?}", + short_hash + ))) + } else { + Ok(self.get(matching_hashes[0]).unwrap().to_owned()) + } + } + + pub fn append(&mut self, lines: Vec<(String, String)>) -> Result { + let mut entries = String::new(); + + self.extend(lines.clone()); + + for (hash, id) in self.iter() { + entries.push_str(&format!("{} {}\n", hash, id)); + } + + for (hash, id) in lines { + loop { + let short_hash = &hash[0..self.short_hash_len]; + let conflict_found = self + .map + .keys() + .find(|cached_hash| { + cached_hash.starts_with(short_hash) && *cached_hash != &hash + }) + .is_some(); + if self.short_hash_len > 32 || !conflict_found { + break; + } + self.short_hash_len += 1; + } + entries.push_str(&format!("{} {}\n", hash, id)); + } + + OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&self.path) + .context("cannot open maildir id hash map cache")? + .write(format!("{}\n{}", self.short_hash_len, entries).as_bytes()) + .context("cannot write maildir id hash map cache")?; + + Ok(self.short_hash_len) + } +} + +impl Deref for IdMapper { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl DerefMut for IdMapper { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index 94b0f26..28ac626 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -1,149 +1,13 @@ use anyhow::{anyhow, Context, Result}; -use std::{ - collections::HashMap, - convert::TryInto, - env::temp_dir, - fs::{self, OpenOptions}, - io::{BufRead, BufReader, Write}, - ops::{Deref, DerefMut}, - path::PathBuf, -}; +use std::{convert::TryInto, fs, path::PathBuf}; use crate::{ - backends::{Backend, MaildirEnvelopes, MaildirFlags, MaildirMboxes}, + backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes}, config::{AccountConfig, MaildirBackendConfig, DEFAULT_INBOX_FOLDER}, mbox::Mboxes, msg::{Envelopes, Msg}, }; -#[derive(Debug, Default)] -pub struct MaildirEnvelopesIdHashMapper { - path: PathBuf, - map: HashMap, - short_hash_len: usize, -} - -impl MaildirEnvelopesIdHashMapper { - fn get_cache_path(mdir: &maildir::Maildir) -> PathBuf { - let path_digest = md5::compute(format!("{:?}", mdir.path())); - let file_name = format!("himalaya-hash-map-{:x}", path_digest); - temp_dir().join(file_name) - } - - pub fn new(mdir: &maildir::Maildir) -> Result { - let mut mapper = Self::default(); - mapper.path = Self::get_cache_path(mdir); - - let file = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .open(&mapper.path) - .context("cannot open id hash map file")?; - let reader = BufReader::new(file); - - for line in reader.lines() { - let line = - line.context("cannot read line from maildir envelopes id mapper cache file")?; - if mapper.short_hash_len == 0 { - mapper.short_hash_len = 2.max(line.parse().unwrap_or(2)); - } else { - let (hash, id) = line.split_once(' ').ok_or_else(|| { - anyhow!( - "cannot parse line {:?} from maildir envelopes id mapper cache file", - line - ) - })?; - mapper.insert(hash.to_owned(), id.to_owned()); - } - } - - Ok(mapper) - } - - pub fn find(&self, short_hash: &str) -> Result { - let matching_hashes: Vec<_> = self - .keys() - .filter(|hash| hash.starts_with(short_hash)) - .collect(); - if matching_hashes.len() == 0 { - Err(anyhow!( - "cannot find maildir message id from short hash {:?}", - short_hash, - )) - } else if matching_hashes.len() > 1 { - Err(anyhow!( - "the short hash {:?} matches more than one hash: {}", - short_hash, - matching_hashes - .iter() - .map(|s| s.to_string()) - .collect::>() - .join(", ") - ) - .context(format!( - "cannot find maildir message id from short hash {:?}", - short_hash - ))) - } else { - Ok(self.get(matching_hashes[0]).unwrap().to_owned()) - } - } - - fn append(&mut self, lines: Vec<(String, String)>) -> Result { - let mut entries = String::new(); - - self.extend(lines.clone()); - - for (hash, id) in self.iter() { - entries.push_str(&format!("{} {}\n", hash, id)); - } - - for (hash, id) in lines { - loop { - let short_hash = &hash[0..self.short_hash_len]; - let conflict_found = self - .map - .keys() - .find(|cached_hash| { - cached_hash.starts_with(short_hash) && *cached_hash != &hash - }) - .is_some(); - if self.short_hash_len > 32 || !conflict_found { - break; - } - self.short_hash_len += 1; - } - entries.push_str(&format!("{} {}\n", hash, id)); - } - - OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(&self.path) - .context("cannot open maildir id hash map cache")? - .write(format!("{}\n{}", self.short_hash_len, entries).as_bytes()) - .context("cannot write maildir id hash map cache")?; - - Ok(self.short_hash_len) - } -} - -impl Deref for MaildirEnvelopesIdHashMapper { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.map - } -} - -impl DerefMut for MaildirEnvelopesIdHashMapper { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.map - } -} - pub struct MaildirBackend<'a> { mdir: maildir::Maildir, account_config: &'a AccountConfig, @@ -211,18 +75,18 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { fn get_envelopes( &mut self, - mdir_str: &str, + mdir: &str, page_size: usize, page: usize, ) -> Result> { - let mdir = self.get_mdir_from_name(mdir_str)?; + let mdir = self.get_mdir_from_name(mdir)?; // Reads envelopes from the "cur" folder of the selected // maildir. - let mut envelopes: MaildirEnvelopes = mdir - .list_cur() - .try_into() - .context("cannot parse maildir envelopes from {:?}")?; + let mut envelopes: MaildirEnvelopes = mdir.list_cur().try_into().context(format!( + "cannot parse maildir envelopes from {:?}", + self.mdir.path() + ))?; // Calculates pagination boundaries. let page_begin = page * page_size; @@ -244,7 +108,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { // cache file name is based on the name of the given maildir: // this way there is one cache per maildir. let short_hash_len = { - let mut mapper = MaildirEnvelopesIdHashMapper::new(&mdir)?; + let mut mapper = IdMapper::new(mdir.path())?; let entries = envelopes .iter() .map(|env| (env.hash.to_owned(), env.id.to_owned())) @@ -278,11 +142,11 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let flags: MaildirFlags = flags.try_into()?; let id = mdir .store_cur_with_flags(msg, &flags.to_string()) - .context(format!("cannot add maildir message at {:?}", mdir.path()))?; + .context(format!("cannot add maildir message to {:?}", mdir.path()))?; let hash = format!("{:x}", md5::compute(&id)); // Appends hash line to the maildir cache file. - let mut mapper = MaildirEnvelopesIdHashMapper::new(&mdir)?; + let mut mapper = IdMapper::new(mdir.path())?; mapper.append(vec![(hash.clone(), id)])?; Ok(Box::new(hash)) @@ -290,7 +154,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { fn get_msg(&mut self, mdir: &str, short_hash: &str) -> Result { let mdir = self.get_mdir_from_name(mdir)?; - let id = MaildirEnvelopesIdHashMapper::new(&mdir)? + let id = IdMapper::new(mdir.path())? .find(short_hash) .context(format!( "cannot get maildir message from short hash {:?}", @@ -314,7 +178,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { fn copy_msg(&mut self, mdir_src: &str, mdir_dst: &str, short_hash: &str) -> Result<()> { let mdir_src = self.get_mdir_from_name(mdir_src)?; let mdir_dst = self.get_mdir_from_name(mdir_dst)?; - let id = MaildirEnvelopesIdHashMapper::new(&mdir_src)?.find(short_hash)?; + let id = IdMapper::new(mdir_src.path())?.find(short_hash)?; mdir_src.copy_to(&id, &mdir_dst).context(format!( "cannot copy message {:?} from maildir {:?} to maildir {:?}", @@ -324,8 +188,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { ))?; // Appends hash line to the destination maildir cache file. - MaildirEnvelopesIdHashMapper::new(&mdir_dst)? - .append(vec![(format!("{:x}", md5::compute(&id)), id)])?; + IdMapper::new(mdir_dst.path())?.append(vec![(format!("{:x}", md5::compute(&id)), id)])?; Ok(()) } @@ -333,7 +196,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { fn move_msg(&mut self, mdir_src: &str, mdir_dst: &str, short_hash: &str) -> Result<()> { let mdir_src = self.get_mdir_from_name(mdir_src)?; let mdir_dst = self.get_mdir_from_name(mdir_dst)?; - let id = MaildirEnvelopesIdHashMapper::new(&mdir_src)?.find(short_hash)?; + let id = IdMapper::new(mdir_src.path())?.find(short_hash)?; mdir_src.move_to(&id, &mdir_dst).context(format!( "cannot move message {:?} from maildir {:?} to maildir {:?}", @@ -343,15 +206,14 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { ))?; // Appends hash line to the destination maildir cache file. - MaildirEnvelopesIdHashMapper::new(&mdir_dst)? - .append(vec![(format!("{:x}", md5::compute(&id)), id)])?; + IdMapper::new(mdir_dst.path())?.append(vec![(format!("{:x}", md5::compute(&id)), id)])?; Ok(()) } fn del_msg(&mut self, mdir: &str, short_hash: &str) -> Result<()> { let mdir = self.get_mdir_from_name(mdir)?; - let id = MaildirEnvelopesIdHashMapper::new(&mdir)?.find(short_hash)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; mdir.delete(&id).context(format!( "cannot delete message {:?} from maildir {:?}", id, @@ -361,7 +223,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { fn add_flags(&mut self, mdir: &str, short_hash: &str, flags_str: &str) -> Result<()> { let mdir = self.get_mdir_from_name(mdir)?; - let id = MaildirEnvelopesIdHashMapper::new(&mdir)?.find(short_hash)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; let flags: MaildirFlags = flags_str.try_into()?; mdir.add_flags(&id, &flags.to_string()).context(format!( "cannot add flags {:?} to maildir message {:?}", @@ -371,7 +233,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { fn set_flags(&mut self, mdir: &str, short_hash: &str, flags_str: &str) -> Result<()> { let mdir = self.get_mdir_from_name(mdir)?; - let id = MaildirEnvelopesIdHashMapper::new(&mdir)?.find(short_hash)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; let flags: MaildirFlags = flags_str.try_into()?; mdir.set_flags(&id, &flags.to_string()).context(format!( "cannot set flags {:?} to maildir message {:?}", @@ -381,7 +243,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { fn del_flags(&mut self, mdir: &str, short_hash: &str, flags_str: &str) -> Result<()> { let mdir = self.get_mdir_from_name(mdir)?; - let id = MaildirEnvelopesIdHashMapper::new(&mdir)?.find(short_hash)?; + let id = IdMapper::new(mdir.path())?.find(short_hash)?; let flags: MaildirFlags = flags_str.try_into()?; mdir.remove_flags(&id, &flags.to_string()).context(format!( "cannot remove flags {:?} from maildir message {:?}", diff --git a/src/lib.rs b/src/lib.rs index 48b162d..2665914 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,6 +37,9 @@ pub mod backends { pub use backend::*; pub mod backend; + pub use id_mapper::*; + pub mod id_mapper; + pub use self::imap::*; pub mod imap { pub mod imap_arg; From 6606bd9f160d5d6f6f4acbfc2bed15e989545743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Mon, 28 Feb 2022 12:59:46 +0100 Subject: [PATCH 17/26] add id mapper to notmuch backend --- src/backends/notmuch/notmuch_backend.rs | 137 ++++++++++++++--------- src/backends/notmuch/notmuch_envelope.rs | 13 ++- 2 files changed, 92 insertions(+), 58 deletions(-) diff --git a/src/backends/notmuch/notmuch_backend.rs b/src/backends/notmuch/notmuch_backend.rs index dbfd4c2..50a53b6 100644 --- a/src/backends/notmuch/notmuch_backend.rs +++ b/src/backends/notmuch/notmuch_backend.rs @@ -3,7 +3,7 @@ use std::{convert::TryInto, fs}; use anyhow::{anyhow, Context, Result}; use crate::{ - backends::{Backend, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes}, + backends::{Backend, IdMapper, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes}, config::{AccountConfig, NotmuchBackendConfig}, mbox::Mboxes, msg::{Envelopes, Msg}, @@ -11,6 +11,7 @@ use crate::{ pub struct NotmuchBackend<'a> { account_config: &'a AccountConfig, + notmuch_config: &'a NotmuchBackendConfig, db: notmuch::Database, } @@ -21,6 +22,7 @@ impl<'a> NotmuchBackend<'a> { ) -> Result { Ok(Self { account_config, + notmuch_config, db: notmuch::Database::open( notmuch_config.notmuch_database_dir.clone(), notmuch::DatabaseMode::ReadWrite, @@ -31,6 +33,64 @@ impl<'a> NotmuchBackend<'a> { ))?, }) } + + fn _search_envelopes( + &mut self, + query: &str, + virt_mbox: &str, + page_size: usize, + page: usize, + ) -> Result> { + // Gets envelopes matching the given Notmuch query. + let query_builder = self + .db + .create_query(query) + .context(format!("cannot create notmuch query from {:?}", query))?; + let mut envelopes: NotmuchEnvelopes = query_builder + .search_messages() + .context(format!( + "cannot find notmuch envelopes from query {:?}", + query + ))? + .try_into() + .context(format!( + "cannot parse notmuch envelopes from query {:?}", + query + ))?; + + // Calculates pagination boundaries. + let page_begin = page * page_size; + if page_begin > envelopes.len() { + return Err(anyhow!(format!( + "cannot find notmuch envelopes at page {:?} (out of bounds)", + page_begin + 1, + ))); + } + let page_end = envelopes.len().min(page_begin + page_size); + + // Sorts envelopes by most recent date. + envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); + + // Applies pagination boundaries. + envelopes.0 = envelopes[page_begin..page_end].to_owned(); + + // Appends id <=> hash entries to the id mapper cache file. + let short_hash_len = { + let mut mapper = IdMapper::new(&self.notmuch_config.notmuch_database_dir)?; + let entries = envelopes + .iter() + .map(|env| (env.hash.to_owned(), env.id.to_owned())) + .collect(); + mapper.append(entries)? + }; + + // Shorten envelopes hash. + envelopes + .iter_mut() + .for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned()); + + Ok(Box::new(envelopes)) + } } impl<'a> Backend<'a> for NotmuchBackend<'a> { @@ -55,81 +115,44 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { fn get_envelopes( &mut self, - mbox: &str, + virt_mbox: &str, page_size: usize, page: usize, ) -> Result> { let query = self .account_config .mailboxes - .get(mbox) + .get(virt_mbox) .map(|s| s.as_str()) .unwrap_or("all"); - let query_builder = self - .db - .create_query(query) - .context("cannot create notmuch query")?; - let mut envelopes: NotmuchEnvelopes = query_builder - .search_messages() - .context(format!( - "cannot find notmuch envelopes with query {:?}", - query - ))? - .try_into()?; - envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); - let page_begin = page * page_size; - if page_begin > envelopes.len() { - return Err(anyhow!(format!( - "cannot find notmuch envelopes at page {:?} (out of bounds)", - page_begin + 1, - ))); - } - let page_end = envelopes.len().min(page_begin + page_size); - envelopes.0 = envelopes[page_begin..page_end].to_owned(); - Ok(Box::new(envelopes)) + self._search_envelopes(query, virt_mbox, page_size, page) } - fn find_envelopes( + fn search_envelopes( &mut self, - _mbox: &str, + virt_mbox: &str, query: &str, _sort: &str, page_size: usize, page: usize, ) -> Result> { - let query_builder = self - .db - .create_query(query) - .context("cannot create notmuch query")?; - let mut envelopes: NotmuchEnvelopes = query_builder - .search_messages() - .context(format!( - "cannot find notmuch envelopes with query {:?}", - query - ))? - .try_into()?; - // TODO: use sort from parameters instead - envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); - let page_begin = page * page_size; - if page_begin > envelopes.len() { - return Err(anyhow!(format!( - "cannot find notmuch envelopes at page {:?} (out of bounds)", - page_begin + 1, - ))); - } - let page_end = envelopes.len().min(page_begin + page_size); - envelopes.0 = envelopes[page_begin..page_end].to_owned(); - Ok(Box::new(envelopes)) + self._search_envelopes(query, virt_mbox, page_size, page) } fn add_msg(&mut self, _mbox: &str, _msg: &[u8], _flags: &str) -> Result> { unimplemented!(); } - fn get_msg(&mut self, _mbox: &str, id: &str) -> Result { + fn get_msg(&mut self, _mbox: &str, short_hash: &str) -> Result { + let id = IdMapper::new(&self.notmuch_config.notmuch_database_dir)? + .find(short_hash) + .context(format!( + "cannot get notmuch message from short hash {:?}", + short_hash + ))?; let msg_filepath = self .db - .find_message(id) + .find_message(&id) .context(format!("cannot find notmuch message {:?}", id))? .ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))? .filename() @@ -148,10 +171,16 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { unimplemented!(); } - fn del_msg(&mut self, _mbox: &str, id: &str) -> Result<()> { + fn del_msg(&mut self, _mbox: &str, short_hash: &str) -> Result<()> { + let id = IdMapper::new(&self.notmuch_config.notmuch_database_dir)? + .find(short_hash) + .context(format!( + "cannot get notmuch message from short hash {:?}", + short_hash + ))?; let msg_filepath = self .db - .find_message(id) + .find_message(&id) .context(format!("cannot find notmuch message {:?}", id))? .ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))? .filename() diff --git a/src/backends/notmuch/notmuch_envelope.rs b/src/backends/notmuch/notmuch_envelope.rs index 559191c..297535f 100644 --- a/src/backends/notmuch/notmuch_envelope.rs +++ b/src/backends/notmuch/notmuch_envelope.rs @@ -51,6 +51,9 @@ pub struct NotmuchEnvelope { /// Represents the id of the message. pub id: String, + /// Represents the MD5 hash of the message id. + pub hash: String, + /// Represents the tags of the message. pub flags: Vec, @@ -67,7 +70,7 @@ pub struct NotmuchEnvelope { impl Table for NotmuchEnvelope { fn head() -> Row { Row::new() - .cell(Cell::new("ID").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()) @@ -75,14 +78,14 @@ impl Table for NotmuchEnvelope { } fn row(&self) -> Row { - let id = self.id.to_string(); + let hash = self.hash.to_string(); let unseen = !self.flags.contains(&String::from("unread")); let flags = String::new(); let subject = &self.subject; let sender = &self.sender; let date = &self.date; Row::new() - .cell(Cell::new(id).bold_if(unseen).red()) + .cell(Cell::new(hash).bold_if(unseen).red()) .cell(Cell::new(flags).bold_if(unseen).white()) .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) .cell(Cell::new(sender).bold_if(unseen).blue()) @@ -117,7 +120,8 @@ impl<'a> TryFrom for NotmuchEnvelope { fn try_from(raw_envelope: RawNotmuchEnvelope) -> Result { info!("begin: try building envelope from notmuch parsed mail"); - let id = raw_envelope.id().trim().to_string(); + let id = raw_envelope.id().to_string(); + let hash = format!("{:x}", md5::compute(&id)); let subject = raw_envelope .header("subject") .context("cannot get header \"Subject\" from notmuch message")? @@ -159,6 +163,7 @@ impl<'a> TryFrom for NotmuchEnvelope { let envelope = Self { id, + hash, flags: raw_envelope.tags().collect(), subject, sender, From 526f344c7c00b856d670d9cd8d209b447ad75f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Mon, 28 Feb 2022 21:20:36 +0100 Subject: [PATCH 18/26] improve maildir backend logs and comments --- src/backends/maildir/maildir_backend.rs | 403 +++++++++++++++++++----- 1 file changed, 327 insertions(+), 76 deletions(-) diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index 28ac626..7198ce5 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, Context, Result}; +use log::{debug, info, trace}; use std::{convert::TryInto, fs, path::PathBuf}; use crate::{ @@ -8,9 +9,10 @@ use crate::{ msg::{Envelopes, Msg}, }; +/// Represents the maildir backend. pub struct MaildirBackend<'a> { - mdir: maildir::Maildir, account_config: &'a AccountConfig, + mdir: maildir::Maildir, } impl<'a> MaildirBackend<'a> { @@ -28,14 +30,12 @@ impl<'a> MaildirBackend<'a> { if mdir_path.is_dir() { Ok(mdir_path) } else { - Err(anyhow!( - "cannot read maildir from directory {:?}", - mdir_path - )) + Err(anyhow!("cannot read maildir directory {:?}", mdir_path)) } } - fn get_mdir_from_name(&self, mdir: &str) -> Result { + /// Creates a maildir instance from a string slice. + fn get_mdir_from_dir(&self, dir: &str) -> Result { let inbox_folder = self .account_config .mailboxes @@ -43,13 +43,20 @@ impl<'a> MaildirBackend<'a> { .map(|s| s.as_str()) .unwrap_or(DEFAULT_INBOX_FOLDER); - if mdir == inbox_folder { + // If the dir points to the inbox folder, creates a maildir + // instance from the root folder. + if dir == inbox_folder { self.validate_mdir_path(self.mdir.path().to_owned()) .map(maildir::Maildir::from) } else { - self.validate_mdir_path(mdir.into()) + // If the dir is a valid maildir path, creates a maildir instance from it. + self.validate_mdir_path(dir.into()) .or_else(|_| { - let path = self.mdir.path().join(format!(".{}", mdir)); + // Otherwise creates a maildir instance from a + // maildir subdirectory by adding a "." in front + // of the name as described in the spec: + // https://cr.yp.to/proto/maildir.html + let path = self.mdir.path().join(format!(".{}", dir)); self.validate_mdir_path(path) }) .map(maildir::Maildir::from) @@ -58,28 +65,66 @@ impl<'a> MaildirBackend<'a> { } impl<'a> Backend<'a> for MaildirBackend<'a> { - fn add_mbox(&mut self, mdir: &str) -> Result<()> { - fs::create_dir(self.mdir.path().join(format!(".{}", mdir))) - .context(format!("cannot create maildir subfolder {:?}", mdir)) + fn add_mbox(&mut self, subdir: &str) -> Result<()> { + info!(">> add maildir subdir"); + debug!("subdir: {:?}", subdir); + + let path = self.mdir.path().join(format!(".{}", subdir)); + trace!("subdir path: {:?}", path); + + fs::create_dir(&path).context(format!( + "cannot create maildir subdir {:?} at {:?}", + subdir, path + ))?; + + info!("<< add maildir subdir"); + Ok(()) } fn get_mboxes(&mut self) -> Result> { - let mboxes: MaildirMboxes = self.mdir.list_subdirs().try_into()?; - Ok(Box::new(mboxes)) + info!(">> get maildir subdirs"); + + let subdirs: MaildirMboxes = self.mdir.list_subdirs().try_into().context(format!( + "cannot parse maildir subdirs from {:?}", + self.mdir.path() + ))?; + trace!("subdirs: {:?}", subdirs); + + info!("<< get maildir subdirs"); + Ok(Box::new(subdirs)) } - fn del_mbox(&mut self, mdir: &str) -> Result<()> { - fs::remove_dir_all(self.mdir.path().join(format!(".{}", mdir))) - .context(format!("cannot delete maildir subfolder {:?}", mdir)) + fn del_mbox(&mut self, subdir: &str) -> Result<()> { + info!(">> delete maildir subdir"); + debug!("subdir: {:?}", subdir); + + let path = self.mdir.path().join(format!(".{}", subdir)); + trace!("subdir path: {:?}", path); + + fs::remove_dir_all(&path).context(format!( + "cannot delete maildir subdir {:?} from {:?}", + subdir, path + ))?; + + info!("<< delete maildir subdir"); + Ok(()) } fn get_envelopes( &mut self, - mdir: &str, + subdir: &str, page_size: usize, page: usize, ) -> Result> { - let mdir = self.get_mdir_from_name(mdir)?; + info!(">> get maildir envelopes"); + debug!("maildir subdir: {:?}", subdir); + debug!("page size: {:?}", page_size); + debug!("page: {:?}", page); + + let mdir = self.get_mdir_from_dir(subdir).context(format!( + "cannot get maildir instance from subdir {:?}", + subdir + ))?; // Reads envelopes from the "cur" folder of the selected // maildir. @@ -87,16 +132,20 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { "cannot parse maildir envelopes from {:?}", self.mdir.path() ))?; + debug!("envelopes len: {:?}", envelopes.len()); + trace!("envelopes: {:?}", envelopes); // Calculates pagination boundaries. let page_begin = page * page_size; + debug!("page begin: {:?}", page_begin); if page_begin > envelopes.len() { return Err(anyhow!(format!( - "cannot list maildir envelopes at page {:?} (out of bounds)", + "cannot get maildir envelopes at page {:?} (out of bounds)", page_begin + 1, ))); } let page_end = envelopes.len().min(page_begin + page_size); + debug!("page end: {:?}", page_end); // Sorts envelopes by most recent date. envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); @@ -104,9 +153,10 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { // Applies pagination boundaries. envelopes.0 = envelopes[page_begin..page_end].to_owned(); - // Writes envelope ids and their hashes to a cache file. The - // cache file name is based on the name of the given maildir: - // this way there is one cache per maildir. + // Appends envelopes hash to the id mapper cache file and + // calculates the new short hash length. The short hash length + // represents the minimum hash length possible to avoid + // conflicts. let short_hash_len = { let mut mapper = IdMapper::new(mdir.path())?; let entries = envelopes @@ -115,70 +165,132 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { .collect(); mapper.append(entries)? }; + debug!("short hash length: {:?}", short_hash_len); // Shorten envelopes hash. envelopes .iter_mut() .for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned()); + info!("<< get maildir envelopes"); Ok(Box::new(envelopes)) } fn search_envelopes( &mut self, - _mdir: &str, + _subdir: &str, _query: &str, _sort: &str, _page_size: usize, _page: usize, ) -> Result> { + info!(">> search maildir envelopes"); + info!("<< search maildir envelopes"); Err(anyhow!( "cannot find maildir envelopes: feature not implemented" )) } - fn add_msg(&mut self, mdir: &str, msg: &[u8], flags: &str) -> Result> { - let mdir = self.get_mdir_from_name(mdir)?; - let flags: MaildirFlags = flags.try_into()?; + fn add_msg(&mut self, subdir: &str, msg: &[u8], flags: &str) -> Result> { + info!(">> add maildir message"); + debug!("subdir: {:?}", subdir); + debug!("flags: {:?}", flags); + + let mdir = self.get_mdir_from_dir(subdir).context(format!( + "cannot get maildir instance from subdir {:?}", + subdir + ))?; + let flags: MaildirFlags = flags + .try_into() + .context(format!("cannot parse flags {:?}", flags))?; let id = mdir .store_cur_with_flags(msg, &flags.to_string()) .context(format!("cannot add maildir message to {:?}", mdir.path()))?; + debug!("id: {:?}", id); let hash = format!("{:x}", md5::compute(&id)); + debug!("hash: {:?}", hash); - // Appends hash line to the maildir cache file. - let mut mapper = IdMapper::new(mdir.path())?; - mapper.append(vec![(hash.clone(), id)])?; + // Appends hash entry to the id mapper cache file. + let mut mapper = IdMapper::new(mdir.path()).context(format!( + "cannot create id mapper instance for {:?}", + mdir.path() + ))?; + mapper + .append(vec![(hash.clone(), id.clone())]) + .context(format!( + "cannot append hash {:?} with id {:?} to id mapper", + hash, id + ))?; + info!("<< add maildir message"); Ok(Box::new(hash)) } - fn get_msg(&mut self, mdir: &str, short_hash: &str) -> Result { - let mdir = self.get_mdir_from_name(mdir)?; + fn get_msg(&mut self, subdir: &str, short_hash: &str) -> Result { + info!(">> get maildir message"); + debug!("subdir: {:?}", subdir); + debug!("short hash: {:?}", short_hash); + + let mdir = self.get_mdir_from_dir(subdir).context(format!( + "cannot get maildir instance from subdir {:?}", + subdir + ))?; let id = IdMapper::new(mdir.path())? .find(short_hash) .context(format!( - "cannot get maildir message from short hash {:?}", - short_hash + "cannot find maildir message by short hash {:?} at {:?}", + short_hash, + mdir.path() ))?; - let mut mail_entry = mdir - .find(&id) - .ok_or_else(|| anyhow!("cannot find maildir message {:?} in {:?}", id, mdir.path()))?; + debug!("id: {:?}", id); + let mut mail_entry = mdir.find(&id).ok_or_else(|| { + anyhow!( + "cannot find maildir message by id {:?} at {:?}", + id, + mdir.path() + ) + })?; let parsed_mail = mail_entry.parsed().context(format!( - "cannot parse maildir message {:?} in {:?}", + "cannot parse maildir message {:?} at {:?}", id, mdir.path() ))?; - Msg::from_parsed_mail(parsed_mail, self.account_config).context(format!( - "cannot parse maildir message {:?} from {:?}", + let msg = Msg::from_parsed_mail(parsed_mail, self.account_config).context(format!( + "cannot parse maildir message {:?} at {:?}", id, mdir.path() - )) + ))?; + trace!("message: {:?}", msg); + + info!("<< get maildir message"); + Ok(msg) } - fn copy_msg(&mut self, mdir_src: &str, mdir_dst: &str, short_hash: &str) -> Result<()> { - let mdir_src = self.get_mdir_from_name(mdir_src)?; - let mdir_dst = self.get_mdir_from_name(mdir_dst)?; - let id = IdMapper::new(mdir_src.path())?.find(short_hash)?; + fn copy_msg(&mut self, subdir_src: &str, subdir_dst: &str, short_hash: &str) -> Result<()> { + info!(">> copy maildir message"); + debug!("source subdir: {:?}", subdir_src); + debug!("destination subdir: {:?}", subdir_dst); + + let mdir_src = self.get_mdir_from_dir(subdir_src).context(format!( + "cannot get source maildir instance from subdir {:?}", + subdir_src + ))?; + let mdir_dst = self.get_mdir_from_dir(subdir_dst).context(format!( + "cannot get destination maildir instance from subdir {:?}", + subdir_dst + ))?; + let id = IdMapper::new(mdir_src.path()) + .context(format!( + "cannot create id mapper instance for {:?}", + mdir_src.path() + ))? + .find(short_hash) + .context(format!( + "cannot find maildir message by short hash {:?} at {:?}", + short_hash, + mdir_src.path() + ))?; + debug!("id: {:?}", id); mdir_src.copy_to(&id, &mdir_dst).context(format!( "cannot copy message {:?} from maildir {:?} to maildir {:?}", @@ -187,16 +299,48 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { mdir_dst.path() ))?; - // Appends hash line to the destination maildir cache file. - IdMapper::new(mdir_dst.path())?.append(vec![(format!("{:x}", md5::compute(&id)), id)])?; + // Appends hash entry to the id mapper cache file. + let mut mapper = IdMapper::new(mdir_dst.path()).context(format!( + "cannot create id mapper instance for {:?}", + mdir_dst.path() + ))?; + let hash = format!("{:x}", md5::compute(&id)); + mapper + .append(vec![(hash.clone(), id.clone())]) + .context(format!( + "cannot append hash {:?} with id {:?} to id mapper", + hash, id + ))?; + info!("<< copy maildir message"); Ok(()) } - fn move_msg(&mut self, mdir_src: &str, mdir_dst: &str, short_hash: &str) -> Result<()> { - let mdir_src = self.get_mdir_from_name(mdir_src)?; - let mdir_dst = self.get_mdir_from_name(mdir_dst)?; - let id = IdMapper::new(mdir_src.path())?.find(short_hash)?; + fn move_msg(&mut self, subdir_src: &str, subdir_dst: &str, short_hash: &str) -> Result<()> { + info!(">> move maildir message"); + debug!("source subdir: {:?}", subdir_src); + debug!("destination subdir: {:?}", subdir_dst); + + let mdir_src = self.get_mdir_from_dir(subdir_src).context(format!( + "cannot get source maildir instance from subdir {:?}", + subdir_src + ))?; + let mdir_dst = self.get_mdir_from_dir(subdir_dst).context(format!( + "cannot get destination maildir instance from subdir {:?}", + subdir_dst + ))?; + let id = IdMapper::new(mdir_src.path()) + .context(format!( + "cannot create id mapper instance for {:?}", + mdir_src.path() + ))? + .find(short_hash) + .context(format!( + "cannot find maildir message by short hash {:?} at {:?}", + short_hash, + mdir_src.path() + ))?; + debug!("id: {:?}", id); mdir_src.move_to(&id, &mdir_dst).context(format!( "cannot move message {:?} from maildir {:?} to maildir {:?}", @@ -205,49 +349,156 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { mdir_dst.path() ))?; - // Appends hash line to the destination maildir cache file. - IdMapper::new(mdir_dst.path())?.append(vec![(format!("{:x}", md5::compute(&id)), id)])?; + // Appends hash entry to the id mapper cache file. + let mut mapper = IdMapper::new(mdir_dst.path()).context(format!( + "cannot create id mapper instance for {:?}", + mdir_dst.path() + ))?; + let hash = format!("{:x}", md5::compute(&id)); + mapper + .append(vec![(hash.clone(), id.clone())]) + .context(format!( + "cannot append hash {:?} with id {:?} to id mapper", + hash, id + ))?; + info!("<< move maildir message"); Ok(()) } - fn del_msg(&mut self, mdir: &str, short_hash: &str) -> Result<()> { - let mdir = self.get_mdir_from_name(mdir)?; - let id = IdMapper::new(mdir.path())?.find(short_hash)?; + fn del_msg(&mut self, subdir: &str, short_hash: &str) -> Result<()> { + info!(">> delete maildir message"); + debug!("subdir: {:?}", subdir); + debug!("short hash: {:?}", short_hash); + + let mdir = self.get_mdir_from_dir(subdir).context(format!( + "cannot get maildir instance from subdir {:?}", + subdir + ))?; + let id = IdMapper::new(mdir.path()) + .context(format!( + "cannot create id mapper instance for {:?}", + mdir.path() + ))? + .find(short_hash) + .context(format!( + "cannot find maildir message by short hash {:?} at {:?}", + short_hash, + mdir.path() + ))?; + debug!("id: {:?}", id); mdir.delete(&id).context(format!( "cannot delete message {:?} from maildir {:?}", id, mdir.path() - )) + ))?; + + info!("<< delete maildir message"); + Ok(()) } - fn add_flags(&mut self, mdir: &str, short_hash: &str, flags_str: &str) -> Result<()> { - let mdir = self.get_mdir_from_name(mdir)?; - let id = IdMapper::new(mdir.path())?.find(short_hash)?; - let flags: MaildirFlags = flags_str.try_into()?; + fn add_flags(&mut self, subdir: &str, short_hash: &str, flags: &str) -> Result<()> { + info!(">> add maildir message flags"); + debug!("subdir: {:?}", subdir); + debug!("short hash: {:?}", short_hash); + debug!("flags: {:?}", flags); + + let mdir = self.get_mdir_from_dir(subdir).context(format!( + "cannot get maildir instance from subdir {:?}", + subdir + ))?; + let flags: MaildirFlags = flags + .try_into() + .context(format!("cannot parse maildir flags {:?}", flags))?; + debug!("flags: {:?}", flags); + let id = IdMapper::new(mdir.path()) + .context(format!( + "cannot create id mapper instance for {:?}", + mdir.path() + ))? + .find(short_hash) + .context(format!( + "cannot find maildir message by short hash {:?} at {:?}", + short_hash, + mdir.path() + ))?; + debug!("id: {:?}", id); mdir.add_flags(&id, &flags.to_string()).context(format!( "cannot add flags {:?} to maildir message {:?}", - flags_str, id - )) + flags, id + ))?; + + info!("<< add maildir message flags"); + Ok(()) } - fn set_flags(&mut self, mdir: &str, short_hash: &str, flags_str: &str) -> Result<()> { - let mdir = self.get_mdir_from_name(mdir)?; - let id = IdMapper::new(mdir.path())?.find(short_hash)?; - let flags: MaildirFlags = flags_str.try_into()?; + fn set_flags(&mut self, subdir: &str, short_hash: &str, flags: &str) -> Result<()> { + info!(">> set maildir message flags"); + debug!("subdir: {:?}", subdir); + debug!("short hash: {:?}", short_hash); + debug!("flags: {:?}", flags); + + let mdir = self.get_mdir_from_dir(subdir).context(format!( + "cannot get maildir instance from subdir {:?}", + subdir + ))?; + let flags: MaildirFlags = flags + .try_into() + .context(format!("cannot parse maildir flags {:?}", flags))?; + debug!("flags: {:?}", flags); + let id = IdMapper::new(mdir.path()) + .context(format!( + "cannot create id mapper instance for {:?}", + mdir.path() + ))? + .find(short_hash) + .context(format!( + "cannot find maildir message by short hash {:?} at {:?}", + short_hash, + mdir.path() + ))?; + debug!("id: {:?}", id); mdir.set_flags(&id, &flags.to_string()).context(format!( "cannot set flags {:?} to maildir message {:?}", - flags_str, id - )) + flags, id + ))?; + + info!("<< set maildir message flags"); + Ok(()) } - fn del_flags(&mut self, mdir: &str, short_hash: &str, flags_str: &str) -> Result<()> { - let mdir = self.get_mdir_from_name(mdir)?; - let id = IdMapper::new(mdir.path())?.find(short_hash)?; - let flags: MaildirFlags = flags_str.try_into()?; + fn del_flags(&mut self, subdir: &str, short_hash: &str, flags: &str) -> Result<()> { + info!(">> delete maildir message flags"); + debug!("subdir: {:?}", subdir); + debug!("short hash: {:?}", short_hash); + debug!("flags: {:?}", flags); + + let mdir = self.get_mdir_from_dir(subdir).context(format!( + "cannot get maildir instance from subdir {:?}", + subdir + ))?; + let flags: MaildirFlags = flags + .try_into() + .context(format!("cannot parse maildir flags {:?}", flags))?; + debug!("flags: {:?}", flags); + let id = IdMapper::new(mdir.path()) + .context(format!( + "cannot create id mapper instance for {:?}", + mdir.path() + ))? + .find(short_hash) + .context(format!( + "cannot find maildir message by short hash {:?} at {:?}", + short_hash, + mdir.path() + ))?; + debug!("id: {:?}", id); mdir.remove_flags(&id, &flags.to_string()).context(format!( - "cannot remove flags {:?} from maildir message {:?}", - flags_str, id - )) + "cannot delete flags {:?} to maildir message {:?}", + flags, id + ))?; + + info!("<< delete maildir message flags"); + Ok(()) } } From 328da34f8d3cfb67b6df69b7dff0234cf18ba558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Mon, 28 Feb 2022 23:20:09 +0100 Subject: [PATCH 19/26] fix comment typos in maildir backend --- src/backends/maildir/maildir_backend.rs | 152 ++++++++++++------------ 1 file changed, 74 insertions(+), 78 deletions(-) diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index 7198ce5..e0e9153 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -1,3 +1,8 @@ +//! Maildir backend module. +//! +//! This module contains the definition of the maildir backend and its +//! traits implementation. + use anyhow::{anyhow, Context, Result}; use log::{debug, info, trace}; use std::{convert::TryInto, fs, path::PathBuf}; @@ -82,49 +87,46 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { } fn get_mboxes(&mut self) -> Result> { - info!(">> get maildir subdirs"); + info!(">> get maildir dirs"); - let subdirs: MaildirMboxes = self.mdir.list_subdirs().try_into().context(format!( - "cannot parse maildir subdirs from {:?}", + let dirs: MaildirMboxes = self.mdir.list_subdirs().try_into().context(format!( + "cannot parse maildir dirs from {:?}", self.mdir.path() ))?; - trace!("subdirs: {:?}", subdirs); + trace!("dirs: {:?}", dirs); - info!("<< get maildir subdirs"); - Ok(Box::new(subdirs)) + info!("<< get maildir dirs"); + Ok(Box::new(dirs)) } - fn del_mbox(&mut self, subdir: &str) -> Result<()> { - info!(">> delete maildir subdir"); - debug!("subdir: {:?}", subdir); + fn del_mbox(&mut self, dir: &str) -> Result<()> { + info!(">> delete maildir dir"); + debug!("dir: {:?}", dir); - let path = self.mdir.path().join(format!(".{}", subdir)); - trace!("subdir path: {:?}", path); + let path = self.mdir.path().join(format!(".{}", dir)); + trace!("dir path: {:?}", path); - fs::remove_dir_all(&path).context(format!( - "cannot delete maildir subdir {:?} from {:?}", - subdir, path - ))?; + fs::remove_dir_all(&path) + .context(format!("cannot delete maildir {:?} from {:?}", dir, path))?; - info!("<< delete maildir subdir"); + info!("<< delete maildir dir"); Ok(()) } fn get_envelopes( &mut self, - subdir: &str, + dir: &str, page_size: usize, page: usize, ) -> Result> { info!(">> get maildir envelopes"); - debug!("maildir subdir: {:?}", subdir); + debug!("dir: {:?}", dir); debug!("page size: {:?}", page_size); debug!("page: {:?}", page); - let mdir = self.get_mdir_from_dir(subdir).context(format!( - "cannot get maildir instance from subdir {:?}", - subdir - ))?; + let mdir = self + .get_mdir_from_dir(dir) + .context(format!("cannot get maildir instance from {:?}", dir))?; // Reads envelopes from the "cur" folder of the selected // maildir. @@ -178,7 +180,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { fn search_envelopes( &mut self, - _subdir: &str, + _dir: &str, _query: &str, _sort: &str, _page_size: usize, @@ -191,18 +193,17 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { )) } - fn add_msg(&mut self, subdir: &str, msg: &[u8], flags: &str) -> Result> { + fn add_msg(&mut self, dir: &str, msg: &[u8], flags: &str) -> Result> { info!(">> add maildir message"); - debug!("subdir: {:?}", subdir); + debug!("dir: {:?}", dir); debug!("flags: {:?}", flags); - let mdir = self.get_mdir_from_dir(subdir).context(format!( - "cannot get maildir instance from subdir {:?}", - subdir - ))?; + let mdir = self + .get_mdir_from_dir(dir) + .context(format!("cannot get maildir instance from {:?}", dir))?; let flags: MaildirFlags = flags .try_into() - .context(format!("cannot parse flags {:?}", flags))?; + .context(format!("cannot parse maildir flags {:?}", flags))?; let id = mdir .store_cur_with_flags(msg, &flags.to_string()) .context(format!("cannot add maildir message to {:?}", mdir.path()))?; @@ -226,15 +227,14 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { Ok(Box::new(hash)) } - fn get_msg(&mut self, subdir: &str, short_hash: &str) -> Result { + fn get_msg(&mut self, dir: &str, short_hash: &str) -> Result { info!(">> get maildir message"); - debug!("subdir: {:?}", subdir); + debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); - let mdir = self.get_mdir_from_dir(subdir).context(format!( - "cannot get maildir instance from subdir {:?}", - subdir - ))?; + let mdir = self + .get_mdir_from_dir(dir) + .context(format!("cannot get maildir instance from {:?}", dir))?; let id = IdMapper::new(mdir.path())? .find(short_hash) .context(format!( @@ -266,18 +266,18 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { Ok(msg) } - fn copy_msg(&mut self, subdir_src: &str, subdir_dst: &str, short_hash: &str) -> Result<()> { + fn copy_msg(&mut self, dir_src: &str, dir_dst: &str, short_hash: &str) -> Result<()> { info!(">> copy maildir message"); - debug!("source subdir: {:?}", subdir_src); - debug!("destination subdir: {:?}", subdir_dst); + debug!("source dir: {:?}", dir_src); + debug!("destination dir: {:?}", dir_dst); - let mdir_src = self.get_mdir_from_dir(subdir_src).context(format!( - "cannot get source maildir instance from subdir {:?}", - subdir_src + let mdir_src = self.get_mdir_from_dir(dir_src).context(format!( + "cannot get source maildir instance from {:?}", + dir_src ))?; - let mdir_dst = self.get_mdir_from_dir(subdir_dst).context(format!( - "cannot get destination maildir instance from subdir {:?}", - subdir_dst + let mdir_dst = self.get_mdir_from_dir(dir_dst).context(format!( + "cannot get destination maildir instance from {:?}", + dir_dst ))?; let id = IdMapper::new(mdir_src.path()) .context(format!( @@ -316,18 +316,18 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { Ok(()) } - fn move_msg(&mut self, subdir_src: &str, subdir_dst: &str, short_hash: &str) -> Result<()> { + fn move_msg(&mut self, dir_src: &str, dir_dst: &str, short_hash: &str) -> Result<()> { info!(">> move maildir message"); - debug!("source subdir: {:?}", subdir_src); - debug!("destination subdir: {:?}", subdir_dst); + debug!("source dir: {:?}", dir_src); + debug!("destination dir: {:?}", dir_dst); - let mdir_src = self.get_mdir_from_dir(subdir_src).context(format!( - "cannot get source maildir instance from subdir {:?}", - subdir_src + let mdir_src = self.get_mdir_from_dir(dir_src).context(format!( + "cannot get source maildir instance from {:?}", + dir_src ))?; - let mdir_dst = self.get_mdir_from_dir(subdir_dst).context(format!( - "cannot get destination maildir instance from subdir {:?}", - subdir_dst + let mdir_dst = self.get_mdir_from_dir(dir_dst).context(format!( + "cannot get destination maildir instance from {:?}", + dir_dst ))?; let id = IdMapper::new(mdir_src.path()) .context(format!( @@ -366,15 +366,14 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { Ok(()) } - fn del_msg(&mut self, subdir: &str, short_hash: &str) -> Result<()> { + fn del_msg(&mut self, dir: &str, short_hash: &str) -> Result<()> { info!(">> delete maildir message"); - debug!("subdir: {:?}", subdir); + debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); - let mdir = self.get_mdir_from_dir(subdir).context(format!( - "cannot get maildir instance from subdir {:?}", - subdir - ))?; + let mdir = self + .get_mdir_from_dir(dir) + .context(format!("cannot get maildir instance from {:?}", dir))?; let id = IdMapper::new(mdir.path()) .context(format!( "cannot create id mapper instance for {:?}", @@ -397,16 +396,15 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { Ok(()) } - fn add_flags(&mut self, subdir: &str, short_hash: &str, flags: &str) -> Result<()> { + fn add_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> { info!(">> add maildir message flags"); - debug!("subdir: {:?}", subdir); + debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); debug!("flags: {:?}", flags); - let mdir = self.get_mdir_from_dir(subdir).context(format!( - "cannot get maildir instance from subdir {:?}", - subdir - ))?; + let mdir = self + .get_mdir_from_dir(dir) + .context(format!("cannot get maildir instance from {:?}", dir))?; let flags: MaildirFlags = flags .try_into() .context(format!("cannot parse maildir flags {:?}", flags))?; @@ -432,16 +430,15 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { Ok(()) } - fn set_flags(&mut self, subdir: &str, short_hash: &str, flags: &str) -> Result<()> { + fn set_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> { info!(">> set maildir message flags"); - debug!("subdir: {:?}", subdir); + debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); debug!("flags: {:?}", flags); - let mdir = self.get_mdir_from_dir(subdir).context(format!( - "cannot get maildir instance from subdir {:?}", - subdir - ))?; + let mdir = self + .get_mdir_from_dir(dir) + .context(format!("cannot get maildir instance from {:?}", dir))?; let flags: MaildirFlags = flags .try_into() .context(format!("cannot parse maildir flags {:?}", flags))?; @@ -467,16 +464,15 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { Ok(()) } - fn del_flags(&mut self, subdir: &str, short_hash: &str, flags: &str) -> Result<()> { + fn del_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> { info!(">> delete maildir message flags"); - debug!("subdir: {:?}", subdir); + debug!("dir: {:?}", dir); debug!("short hash: {:?}", short_hash); debug!("flags: {:?}", flags); - let mdir = self.get_mdir_from_dir(subdir).context(format!( - "cannot get maildir instance from subdir {:?}", - subdir - ))?; + let mdir = self + .get_mdir_from_dir(dir) + .context(format!("cannot get maildir instance from {:?}", dir))?; let flags: MaildirFlags = flags .try_into() .context(format!("cannot parse maildir flags {:?}", flags))?; From f631f637997452502a23f0c07bbbea1e399a1c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 1 Mar 2022 12:28:20 +0100 Subject: [PATCH 20/26] improve notmuch backend logs and error msg --- src/backends/maildir/maildir_backend.rs | 4 +- src/backends/notmuch/notmuch_backend.rs | 206 +++++++++++++++++------- 2 files changed, 154 insertions(+), 56 deletions(-) diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index e0e9153..b9c842a 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -141,10 +141,10 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let page_begin = page * page_size; debug!("page begin: {:?}", page_begin); if page_begin > envelopes.len() { - return Err(anyhow!(format!( + return Err(anyhow!( "cannot get maildir envelopes at page {:?} (out of bounds)", page_begin + 1, - ))); + )); } let page_end = envelopes.len().min(page_begin + page_size); debug!("page end: {:?}", page_end); diff --git a/src/backends/notmuch/notmuch_backend.rs b/src/backends/notmuch/notmuch_backend.rs index 50a53b6..8db629b 100644 --- a/src/backends/notmuch/notmuch_backend.rs +++ b/src/backends/notmuch/notmuch_backend.rs @@ -1,6 +1,7 @@ use std::{convert::TryInto, fs}; use anyhow::{anyhow, Context, Result}; +use log::{debug, info, trace}; use crate::{ backends::{Backend, IdMapper, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes}, @@ -9,6 +10,8 @@ use crate::{ msg::{Envelopes, Msg}, }; +/// Represents the Notmuch backend. +#[derive(Debug)] pub struct NotmuchBackend<'a> { account_config: &'a AccountConfig, notmuch_config: &'a NotmuchBackendConfig, @@ -20,24 +23,31 @@ impl<'a> NotmuchBackend<'a> { account_config: &'a AccountConfig, notmuch_config: &'a NotmuchBackendConfig, ) -> Result { - Ok(Self { + info!(">> create new notmuch backend"); + + let backend = Self { account_config, notmuch_config, db: notmuch::Database::open( notmuch_config.notmuch_database_dir.clone(), notmuch::DatabaseMode::ReadWrite, ) - .context(format!( - "cannot open notmuch database at {:?}", - notmuch_config.notmuch_database_dir - ))?, - }) + .with_context(|| { + format!( + "cannot open notmuch database at {:?}", + notmuch_config.notmuch_database_dir + ) + })?, + }; + trace!("backend: {:?}", backend); + + info!("<< create new notmuch backend"); + Ok(backend) } fn _search_envelopes( &mut self, query: &str, - virt_mbox: &str, page_size: usize, page: usize, ) -> Result> { @@ -45,28 +55,26 @@ impl<'a> NotmuchBackend<'a> { let query_builder = self .db .create_query(query) - .context(format!("cannot create notmuch query from {:?}", query))?; + .with_context(|| format!("cannot create notmuch query from {:?}", query))?; let mut envelopes: NotmuchEnvelopes = query_builder .search_messages() - .context(format!( - "cannot find notmuch envelopes from query {:?}", - query - ))? + .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))? .try_into() - .context(format!( - "cannot parse notmuch envelopes from query {:?}", - query - ))?; + .with_context(|| format!("cannot parse notmuch envelopes from query {:?}", query))?; + debug!("envelopes len: {:?}", envelopes.len()); + trace!("envelopes: {:?}", envelopes); // Calculates pagination boundaries. let page_begin = page * page_size; + debug!("page begin: {:?}", page_begin); if page_begin > envelopes.len() { return Err(anyhow!(format!( - "cannot find notmuch envelopes at page {:?} (out of bounds)", + "cannot get notmuch envelopes at page {:?} (out of bounds)", page_begin + 1, ))); } let page_end = envelopes.len().min(page_begin + page_size); + debug!("page end: {:?}", page_end); // Sorts envelopes by most recent date. envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); @@ -74,7 +82,10 @@ impl<'a> NotmuchBackend<'a> { // Applies pagination boundaries. envelopes.0 = envelopes[page_begin..page_end].to_owned(); - // Appends id <=> hash entries to the id mapper cache file. + // Appends envelopes hash to the id mapper cache file and + // calculates the new short hash length. The short hash length + // represents the minimum hash length possible to avoid + // conflicts. let short_hash_len = { let mut mapper = IdMapper::new(&self.notmuch_config.notmuch_database_dir)?; let entries = envelopes @@ -83,6 +94,7 @@ impl<'a> NotmuchBackend<'a> { .collect(); mapper.append(entries)? }; + debug!("short hash length: {:?}", short_hash_len); // Shorten envelopes hash. envelopes @@ -95,22 +107,35 @@ impl<'a> NotmuchBackend<'a> { impl<'a> Backend<'a> for NotmuchBackend<'a> { fn add_mbox(&mut self, _mbox: &str) -> Result<()> { - unimplemented!(); + info!(">> add notmuch mailbox"); + info!("<< add notmuch mailbox"); + Err(anyhow!( + "cannot add notmuch mailbox: feature not implemented" + )) } fn get_mboxes(&mut self) -> Result> { - let mut mboxes: Vec<_> = self + info!(">> get notmuch virtual mailboxes"); + + let mut virt_mboxes: Vec<_> = self .account_config .mailboxes .iter() .map(|(k, v)| NotmuchMbox::new(k, v)) .collect(); - mboxes.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap()); - Ok(Box::new(NotmuchMboxes(mboxes))) + trace!("virtual mailboxes: {:?}", virt_mboxes); + virt_mboxes.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap()); + + info!("<< get notmuch virtual mailboxes"); + Ok(Box::new(NotmuchMboxes(virt_mboxes))) } fn del_mbox(&mut self, _mbox: &str) -> Result<()> { - unimplemented!(); + info!(">> delete notmuch mailbox"); + info!("<< delete notmuch mailbox"); + Err(anyhow!( + "cannot delete notmuch mailbox: feature not implemented" + )) } fn get_envelopes( @@ -119,13 +144,22 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { page_size: usize, page: usize, ) -> Result> { + info!(">> get notmuch envelopes"); + debug!("virtual mailbox: {:?}", virt_mbox); + debug!("page size: {:?}", page_size); + debug!("page: {:?}", page); + let query = self .account_config .mailboxes .get(virt_mbox) .map(|s| s.as_str()) .unwrap_or("all"); - self._search_envelopes(query, virt_mbox, page_size, page) + debug!("query: {:?}", query); + let envelopes = self._search_envelopes(query, page_size, page)?; + + info!("<< get notmuch envelopes"); + Ok(envelopes) } fn search_envelopes( @@ -136,69 +170,133 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { page_size: usize, page: usize, ) -> Result> { - self._search_envelopes(query, virt_mbox, page_size, page) + info!(">> search notmuch envelopes"); + debug!("virtual mailbox: {:?}", virt_mbox); + debug!("query: {:?}", query); + debug!("page size: {:?}", page_size); + debug!("page: {:?}", page); + + let query = if query.is_empty() { + self.account_config + .mailboxes + .get(virt_mbox) + .map(|s| s.as_str()) + .unwrap_or("all") + } else { + query + }; + debug!("final query: {:?}", query); + let envelopes = self._search_envelopes(query, page_size, page)?; + + info!("<< search notmuch envelopes"); + Ok(envelopes) } - fn add_msg(&mut self, _mbox: &str, _msg: &[u8], _flags: &str) -> Result> { - unimplemented!(); + fn add_msg( + &mut self, + _virt_mbox: &str, + _msg: &[u8], + _flags: &str, + ) -> Result> { + info!(">> add notmuch envelopes"); + info!("<< add notmuch envelopes"); + Err(anyhow!( + "cannot add notmuch envelopes: feature not implemented" + )) } - fn get_msg(&mut self, _mbox: &str, short_hash: &str) -> Result { - let id = IdMapper::new(&self.notmuch_config.notmuch_database_dir)? + fn get_msg(&mut self, _virt_mbox: &str, short_hash: &str) -> Result { + info!(">> add notmuch envelopes"); + debug!("short hash: {:?}", short_hash); + + let dir = &self.notmuch_config.notmuch_database_dir; + let id = IdMapper::new(dir) + .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? .find(short_hash) - .context(format!( - "cannot get notmuch message from short hash {:?}", - short_hash - ))?; - let msg_filepath = self + .with_context(|| { + format!( + "cannot find notmuch message from short hash {:?}", + short_hash + ) + })?; + debug!("id: {:?}", id); + let msg_file_path = self .db .find_message(&id) - .context(format!("cannot find notmuch message {:?}", id))? + .with_context(|| format!("cannot find notmuch message {:?}", id))? .ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))? .filename() .to_owned(); - let raw_msg = fs::read(&msg_filepath) - .context(format!("cannot read message from file {:?}", msg_filepath))?; - let msg = Msg::from_parsed_mail(mailparse::parse_mail(&raw_msg)?, &self.account_config)?; + debug!("message file path: {:?}", msg_file_path); + let raw_msg = fs::read(&msg_file_path).with_context(|| { + format!("cannot read notmuch message from file {:?}", msg_file_path) + })?; + let msg = mailparse::parse_mail(&raw_msg) + .with_context(|| format!("cannot parse raw notmuch message {:?}", id))?; + let msg = Msg::from_parsed_mail(msg, &self.account_config) + .with_context(|| format!("cannot parse notmuch message {:?}", id))?; + trace!("message: {:?}", msg); + + info!("<< get notmuch message"); Ok(msg) } fn copy_msg(&mut self, _mbox_src: &str, _mbox_dst: &str, _id: &str) -> Result<()> { - unimplemented!(); + info!(">> copy notmuch message"); + info!("<< copy notmuch message"); + Err(anyhow!( + "cannot copy notmuch message: feature not implemented" + )) } - fn move_msg(&mut self, _mbox_src: &str, _mbox_dst: &str, _id: &str) -> Result<()> { - unimplemented!(); + fn move_msg(&mut self, _src: &str, _dst: &str, _id: &str) -> Result<()> { + info!(">> move notmuch message"); + info!("<< move notmuch message"); + Err(anyhow!( + "cannot move notmuch message: feature not implemented" + )) } - fn del_msg(&mut self, _mbox: &str, short_hash: &str) -> Result<()> { - let id = IdMapper::new(&self.notmuch_config.notmuch_database_dir)? + fn del_msg(&mut self, _virt_mbox: &str, short_hash: &str) -> Result<()> { + info!(">> delete notmuch message"); + debug!("short hash: {:?}", short_hash); + + let dir = &self.notmuch_config.notmuch_database_dir; + let id = IdMapper::new(dir) + .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? .find(short_hash) - .context(format!( - "cannot get notmuch message from short hash {:?}", - short_hash - ))?; - let msg_filepath = self + .with_context(|| { + format!( + "cannot find notmuch message from short hash {:?}", + short_hash + ) + })?; + debug!("id: {:?}", id); + let msg_file_path = self .db .find_message(&id) - .context(format!("cannot find notmuch message {:?}", id))? + .with_context(|| format!("cannot find notmuch message {:?}", id))? .ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))? .filename() .to_owned(); + debug!("message file path: {:?}", msg_file_path); self.db - .remove_message(msg_filepath) - .context(format!("cannot delete notmuch message {:?}", id)) + .remove_message(msg_file_path) + .with_context(|| format!("cannot delete notmuch message {:?}", id))?; + + info!("<< delete notmuch message"); + Ok(()) } - fn add_flags(&mut self, _mbox: &str, _id: &str, _flags_str: &str) -> Result<()> { + fn add_flags(&mut self, _virt_mbox: &str, _id: &str, _flags: &str) -> Result<()> { unimplemented!(); } - fn set_flags(&mut self, _mbox: &str, _id: &str, _flags_str: &str) -> Result<()> { + fn set_flags(&mut self, _virt_mbox: &str, _id: &str, _flags: &str) -> Result<()> { unimplemented!(); } - fn del_flags(&mut self, _mbox: &str, _id: &str, _flags_str: &str) -> Result<()> { + fn del_flags(&mut self, _virt_mbox: &str, _id: &str, _flags: &str) -> Result<()> { unimplemented!(); } } From 4093d137659d498596a79ace391331166f1b72f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 1 Mar 2022 13:34:24 +0100 Subject: [PATCH 21/26] add maildir backend flag tests --- src/backends/maildir/maildir_backend.rs | 318 ++++++++++++------------ tests/test_maildir_backend.rs | 30 ++- 2 files changed, 184 insertions(+), 164 deletions(-) diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index b9c842a..5aa974f 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -77,10 +77,8 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let path = self.mdir.path().join(format!(".{}", subdir)); trace!("subdir path: {:?}", path); - fs::create_dir(&path).context(format!( - "cannot create maildir subdir {:?} at {:?}", - subdir, path - ))?; + fs::create_dir(&path) + .with_context(|| format!("cannot create maildir subdir {:?} at {:?}", subdir, path))?; info!("<< add maildir subdir"); Ok(()) @@ -89,10 +87,10 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { fn get_mboxes(&mut self) -> Result> { info!(">> get maildir dirs"); - let dirs: MaildirMboxes = self.mdir.list_subdirs().try_into().context(format!( - "cannot parse maildir dirs from {:?}", - self.mdir.path() - ))?; + let dirs: MaildirMboxes = + self.mdir.list_subdirs().try_into().with_context(|| { + format!("cannot parse maildir dirs from {:?}", self.mdir.path()) + })?; trace!("dirs: {:?}", dirs); info!("<< get maildir dirs"); @@ -107,7 +105,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { trace!("dir path: {:?}", path); fs::remove_dir_all(&path) - .context(format!("cannot delete maildir {:?} from {:?}", dir, path))?; + .with_context(|| format!("cannot delete maildir {:?} from {:?}", dir, path))?; info!("<< delete maildir dir"); Ok(()) @@ -126,14 +124,13 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) - .context(format!("cannot get maildir instance from {:?}", dir))?; + .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; // Reads envelopes from the "cur" folder of the selected // maildir. - let mut envelopes: MaildirEnvelopes = mdir.list_cur().try_into().context(format!( - "cannot parse maildir envelopes from {:?}", - self.mdir.path() - ))?; + let mut envelopes: MaildirEnvelopes = mdir.list_cur().try_into().with_context(|| { + format!("cannot parse maildir envelopes from {:?}", self.mdir.path()) + })?; debug!("envelopes len: {:?}", envelopes.len()); trace!("envelopes: {:?}", envelopes); @@ -200,28 +197,28 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) - .context(format!("cannot get maildir instance from {:?}", dir))?; + .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; let flags: MaildirFlags = flags .try_into() - .context(format!("cannot parse maildir flags {:?}", flags))?; + .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; let id = mdir .store_cur_with_flags(msg, &flags.to_string()) - .context(format!("cannot add maildir message to {:?}", mdir.path()))?; + .with_context(|| format!("cannot add maildir message to {:?}", mdir.path()))?; debug!("id: {:?}", id); let hash = format!("{:x}", md5::compute(&id)); debug!("hash: {:?}", hash); // Appends hash entry to the id mapper cache file. - let mut mapper = IdMapper::new(mdir.path()).context(format!( - "cannot create id mapper instance for {:?}", - mdir.path() - ))?; + let mut mapper = IdMapper::new(mdir.path()) + .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?; mapper .append(vec![(hash.clone(), id.clone())]) - .context(format!( - "cannot append hash {:?} with id {:?} to id mapper", - hash, id - ))?; + .with_context(|| { + format!( + "cannot append hash {:?} with id {:?} to id mapper", + hash, id + ) + })?; info!("<< add maildir message"); Ok(Box::new(hash)) @@ -234,14 +231,16 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) - .context(format!("cannot get maildir instance from {:?}", dir))?; + .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; let id = IdMapper::new(mdir.path())? .find(short_hash) - .context(format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ))?; + .with_context(|| { + format!( + "cannot find maildir message by short hash {:?} at {:?}", + short_hash, + mdir.path() + ) + })?; debug!("id: {:?}", id); let mut mail_entry = mdir.find(&id).ok_or_else(|| { anyhow!( @@ -250,16 +249,12 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { mdir.path() ) })?; - let parsed_mail = mail_entry.parsed().context(format!( - "cannot parse maildir message {:?} at {:?}", - id, - mdir.path() - ))?; - let msg = Msg::from_parsed_mail(parsed_mail, self.account_config).context(format!( - "cannot parse maildir message {:?} at {:?}", - id, - mdir.path() - ))?; + let parsed_mail = mail_entry.parsed().with_context(|| { + format!("cannot parse maildir message {:?} at {:?}", id, mdir.path()) + })?; + let msg = Msg::from_parsed_mail(parsed_mail, self.account_config).with_context(|| { + format!("cannot parse maildir message {:?} at {:?}", id, mdir.path()) + })?; trace!("message: {:?}", msg); info!("<< get maildir message"); @@ -271,46 +266,46 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { debug!("source dir: {:?}", dir_src); debug!("destination dir: {:?}", dir_dst); - let mdir_src = self.get_mdir_from_dir(dir_src).context(format!( - "cannot get source maildir instance from {:?}", - dir_src - ))?; - let mdir_dst = self.get_mdir_from_dir(dir_dst).context(format!( - "cannot get destination maildir instance from {:?}", - dir_dst - ))?; + let mdir_src = self + .get_mdir_from_dir(dir_src) + .with_context(|| format!("cannot get source maildir instance from {:?}", dir_src))?; + let mdir_dst = self.get_mdir_from_dir(dir_dst).with_context(|| { + format!("cannot get destination maildir instance from {:?}", dir_dst) + })?; let id = IdMapper::new(mdir_src.path()) - .context(format!( - "cannot create id mapper instance for {:?}", - mdir_src.path() - ))? + .with_context(|| format!("cannot create id mapper instance for {:?}", mdir_src.path()))? .find(short_hash) - .context(format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir_src.path() - ))?; + .with_context(|| { + format!( + "cannot find maildir message by short hash {:?} at {:?}", + short_hash, + mdir_src.path() + ) + })?; debug!("id: {:?}", id); - mdir_src.copy_to(&id, &mdir_dst).context(format!( - "cannot copy message {:?} from maildir {:?} to maildir {:?}", - id, - mdir_src.path(), - mdir_dst.path() - ))?; + mdir_src.copy_to(&id, &mdir_dst).with_context(|| { + format!( + "cannot copy message {:?} from maildir {:?} to maildir {:?}", + id, + mdir_src.path(), + mdir_dst.path() + ) + })?; // Appends hash entry to the id mapper cache file. - let mut mapper = IdMapper::new(mdir_dst.path()).context(format!( - "cannot create id mapper instance for {:?}", - mdir_dst.path() - ))?; + let mut mapper = IdMapper::new(mdir_dst.path()).with_context(|| { + format!("cannot create id mapper instance for {:?}", mdir_dst.path()) + })?; let hash = format!("{:x}", md5::compute(&id)); mapper .append(vec![(hash.clone(), id.clone())]) - .context(format!( - "cannot append hash {:?} with id {:?} to id mapper", - hash, id - ))?; + .with_context(|| { + format!( + "cannot append hash {:?} with id {:?} to id mapper", + hash, id + ) + })?; info!("<< copy maildir message"); Ok(()) @@ -321,46 +316,46 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { debug!("source dir: {:?}", dir_src); debug!("destination dir: {:?}", dir_dst); - let mdir_src = self.get_mdir_from_dir(dir_src).context(format!( - "cannot get source maildir instance from {:?}", - dir_src - ))?; - let mdir_dst = self.get_mdir_from_dir(dir_dst).context(format!( - "cannot get destination maildir instance from {:?}", - dir_dst - ))?; + let mdir_src = self + .get_mdir_from_dir(dir_src) + .with_context(|| format!("cannot get source maildir instance from {:?}", dir_src))?; + let mdir_dst = self.get_mdir_from_dir(dir_dst).with_context(|| { + format!("cannot get destination maildir instance from {:?}", dir_dst) + })?; let id = IdMapper::new(mdir_src.path()) - .context(format!( - "cannot create id mapper instance for {:?}", - mdir_src.path() - ))? + .with_context(|| format!("cannot create id mapper instance for {:?}", mdir_src.path()))? .find(short_hash) - .context(format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir_src.path() - ))?; + .with_context(|| { + format!( + "cannot find maildir message by short hash {:?} at {:?}", + short_hash, + mdir_src.path() + ) + })?; debug!("id: {:?}", id); - mdir_src.move_to(&id, &mdir_dst).context(format!( - "cannot move message {:?} from maildir {:?} to maildir {:?}", - id, - mdir_src.path(), - mdir_dst.path() - ))?; + mdir_src.move_to(&id, &mdir_dst).with_context(|| { + format!( + "cannot move message {:?} from maildir {:?} to maildir {:?}", + id, + mdir_src.path(), + mdir_dst.path() + ) + })?; // Appends hash entry to the id mapper cache file. - let mut mapper = IdMapper::new(mdir_dst.path()).context(format!( - "cannot create id mapper instance for {:?}", - mdir_dst.path() - ))?; + let mut mapper = IdMapper::new(mdir_dst.path()).with_context(|| { + format!("cannot create id mapper instance for {:?}", mdir_dst.path()) + })?; let hash = format!("{:x}", md5::compute(&id)); mapper .append(vec![(hash.clone(), id.clone())]) - .context(format!( - "cannot append hash {:?} with id {:?} to id mapper", - hash, id - ))?; + .with_context(|| { + format!( + "cannot append hash {:?} with id {:?} to id mapper", + hash, id + ) + })?; info!("<< move maildir message"); Ok(()) @@ -373,24 +368,25 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) - .context(format!("cannot get maildir instance from {:?}", dir))?; + .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; let id = IdMapper::new(mdir.path()) - .context(format!( - "cannot create id mapper instance for {:?}", - mdir.path() - ))? + .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? .find(short_hash) - .context(format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ))?; + .with_context(|| { + format!( + "cannot find maildir message by short hash {:?} at {:?}", + short_hash, + mdir.path() + ) + })?; debug!("id: {:?}", id); - mdir.delete(&id).context(format!( - "cannot delete message {:?} from maildir {:?}", - id, - mdir.path() - ))?; + mdir.delete(&id).with_context(|| { + format!( + "cannot delete message {:?} from maildir {:?}", + id, + mdir.path() + ) + })?; info!("<< delete maildir message"); Ok(()) @@ -404,27 +400,24 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) - .context(format!("cannot get maildir instance from {:?}", dir))?; + .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; let flags: MaildirFlags = flags .try_into() - .context(format!("cannot parse maildir flags {:?}", flags))?; + .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; debug!("flags: {:?}", flags); let id = IdMapper::new(mdir.path()) - .context(format!( - "cannot create id mapper instance for {:?}", - mdir.path() - ))? + .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? .find(short_hash) - .context(format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ))?; + .with_context(|| { + format!( + "cannot find maildir message by short hash {:?} at {:?}", + short_hash, + mdir.path() + ) + })?; debug!("id: {:?}", id); - mdir.add_flags(&id, &flags.to_string()).context(format!( - "cannot add flags {:?} to maildir message {:?}", - flags, id - ))?; + mdir.add_flags(&id, &flags.to_string()) + .with_context(|| format!("cannot add flags {:?} to maildir message {:?}", flags, id))?; info!("<< add maildir message flags"); Ok(()) @@ -438,27 +431,24 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) - .context(format!("cannot get maildir instance from {:?}", dir))?; + .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; let flags: MaildirFlags = flags .try_into() - .context(format!("cannot parse maildir flags {:?}", flags))?; + .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; debug!("flags: {:?}", flags); let id = IdMapper::new(mdir.path()) - .context(format!( - "cannot create id mapper instance for {:?}", - mdir.path() - ))? + .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? .find(short_hash) - .context(format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ))?; + .with_context(|| { + format!( + "cannot find maildir message by short hash {:?} at {:?}", + short_hash, + mdir.path() + ) + })?; debug!("id: {:?}", id); - mdir.set_flags(&id, &flags.to_string()).context(format!( - "cannot set flags {:?} to maildir message {:?}", - flags, id - ))?; + mdir.set_flags(&id, &flags.to_string()) + .with_context(|| format!("cannot set flags {:?} to maildir message {:?}", flags, id))?; info!("<< set maildir message flags"); Ok(()) @@ -472,27 +462,29 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) - .context(format!("cannot get maildir instance from {:?}", dir))?; + .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; let flags: MaildirFlags = flags .try_into() - .context(format!("cannot parse maildir flags {:?}", flags))?; + .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; debug!("flags: {:?}", flags); let id = IdMapper::new(mdir.path()) - .context(format!( - "cannot create id mapper instance for {:?}", - mdir.path() - ))? + .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? .find(short_hash) - .context(format!( - "cannot find maildir message by short hash {:?} at {:?}", - short_hash, - mdir.path() - ))?; + .with_context(|| { + format!( + "cannot find maildir message by short hash {:?} at {:?}", + short_hash, + mdir.path() + ) + })?; debug!("id: {:?}", id); - mdir.remove_flags(&id, &flags.to_string()).context(format!( - "cannot delete flags {:?} to maildir message {:?}", - flags, id - ))?; + mdir.remove_flags(&id, &flags.to_string()) + .with_context(|| { + format!( + "cannot delete flags {:?} to maildir message {:?}", + flags, id + ) + })?; info!("<< delete maildir message flags"); Ok(()) diff --git a/tests/test_maildir_backend.rs b/tests/test_maildir_backend.rs index 932ea13..adcc2e2 100644 --- a/tests/test_maildir_backend.rs +++ b/tests/test_maildir_backend.rs @@ -2,7 +2,7 @@ use maildir::Maildir; use std::{collections::HashMap, env, fs, iter::FromIterator}; use himalaya::{ - backends::{Backend, MaildirBackend, MaildirEnvelopes}, + backends::{Backend, MaildirBackend, MaildirEnvelopes, MaildirFlag}, config::{AccountConfig, MaildirBackendConfig}, }; @@ -49,6 +49,34 @@ fn test_maildir_backend() { assert_eq!("alice@localhost", envelope.sender); assert_eq!("Plain message", envelope.subject); + // check that a flag can be added to the message + mdir.add_flags("INBOX", &envelope.hash, "flagged passed") + .unwrap(); + let envelopes = mdir.get_envelopes("INBOX", 1, 0).unwrap(); + let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); + let envelope = envelopes.first().unwrap(); + assert!(envelope.flags.contains(&MaildirFlag::Seen)); + assert!(envelope.flags.contains(&MaildirFlag::Flagged)); + assert!(envelope.flags.contains(&MaildirFlag::Passed)); + + // check that the message flags can be changed + mdir.set_flags("INBOX", &envelope.hash, "passed").unwrap(); + let envelopes = mdir.get_envelopes("INBOX", 1, 0).unwrap(); + let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); + let envelope = envelopes.first().unwrap(); + assert!(!envelope.flags.contains(&MaildirFlag::Seen)); + assert!(!envelope.flags.contains(&MaildirFlag::Flagged)); + assert!(envelope.flags.contains(&MaildirFlag::Passed)); + + // check that a flag can be removed from the message + mdir.del_flags("INBOX", &envelope.hash, "passed").unwrap(); + let envelopes = mdir.get_envelopes("INBOX", 1, 0).unwrap(); + let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); + let envelope = envelopes.first().unwrap(); + assert!(!envelope.flags.contains(&MaildirFlag::Seen)); + assert!(!envelope.flags.contains(&MaildirFlag::Flagged)); + assert!(!envelope.flags.contains(&MaildirFlag::Passed)); + // check that the message can be copied mdir.copy_msg("INBOX", "Subdir", &envelope.hash).unwrap(); assert!(mdir.get_msg("INBOX", &hash).is_ok()); From e544536e01e3bcc5f8d6dc2a6c44539d55438fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 1 Mar 2022 14:15:15 +0100 Subject: [PATCH 22/26] implement notmuch backend flags methods --- src/backends/notmuch/notmuch_backend.rs | 114 ++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 6 deletions(-) diff --git a/src/backends/notmuch/notmuch_backend.rs b/src/backends/notmuch/notmuch_backend.rs index 8db629b..fc85175 100644 --- a/src/backends/notmuch/notmuch_backend.rs +++ b/src/backends/notmuch/notmuch_backend.rs @@ -288,15 +288,117 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { Ok(()) } - fn add_flags(&mut self, _virt_mbox: &str, _id: &str, _flags: &str) -> Result<()> { - unimplemented!(); + fn add_flags(&mut self, virt_mbox: &str, query: &str, tags: &str) -> Result<()> { + info!(">> add notmuch message flags"); + debug!("tags: {:?}", tags); + debug!("query: {:?}", query); + + let query = self + .account_config + .mailboxes + .get(virt_mbox) + .map(|s| s.as_str()) + .unwrap_or(query); + debug!("final query: {:?}", query); + let tags: Vec<_> = tags.split_whitespace().collect(); + let query_builder = self + .db + .create_query(query) + .with_context(|| format!("cannot create notmuch query from {:?}", query))?; + let envelopes = query_builder + .search_messages() + .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; + for envelope in envelopes { + for tag in tags.iter() { + envelope.add_tag(*tag).with_context(|| { + format!( + "cannot add tag {:?} to notmuch message {:?}", + tag, + envelope.id() + ) + })? + } + } + + info!("<< add notmuch message flags"); + Ok(()) } - fn set_flags(&mut self, _virt_mbox: &str, _id: &str, _flags: &str) -> Result<()> { - unimplemented!(); + fn set_flags(&mut self, virt_mbox: &str, query: &str, tags: &str) -> Result<()> { + info!(">> set notmuch message flags"); + debug!("tags: {:?}", tags); + debug!("query: {:?}", query); + + let query = self + .account_config + .mailboxes + .get(virt_mbox) + .map(|s| s.as_str()) + .unwrap_or(query); + debug!("final query: {:?}", query); + let tags: Vec<_> = tags.split_whitespace().collect(); + let query_builder = self + .db + .create_query(query) + .with_context(|| format!("cannot create notmuch query from {:?}", query))?; + let envelopes = query_builder + .search_messages() + .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; + for envelope in envelopes { + envelope.remove_all_tags().with_context(|| { + format!( + "cannot remove all tags from notmuch message {:?}", + envelope.id() + ) + })?; + for tag in tags.iter() { + envelope.add_tag(*tag).with_context(|| { + format!( + "cannot add tag {:?} to notmuch message {:?}", + tag, + envelope.id() + ) + })? + } + } + + info!("<< set notmuch message flags"); + Ok(()) } - fn del_flags(&mut self, _virt_mbox: &str, _id: &str, _flags: &str) -> Result<()> { - unimplemented!(); + fn del_flags(&mut self, virt_mbox: &str, query: &str, tags: &str) -> Result<()> { + info!(">> delete notmuch message flags"); + debug!("tags: {:?}", tags); + debug!("query: {:?}", query); + + let query = self + .account_config + .mailboxes + .get(virt_mbox) + .map(|s| s.as_str()) + .unwrap_or(query); + debug!("final query: {:?}", query); + let tags: Vec<_> = tags.split_whitespace().collect(); + let query_builder = self + .db + .create_query(query) + .with_context(|| format!("cannot create notmuch query from {:?}", query))?; + let envelopes = query_builder + .search_messages() + .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; + for envelope in envelopes { + for tag in tags.iter() { + envelope.remove_tag(*tag).with_context(|| { + format!( + "cannot delete tag {:?} from notmuch message {:?}", + tag, + envelope.id() + ) + })? + } + } + + info!("<< delete notmuch message flags"); + Ok(()) } } From 886b66a017499a0237343fb90f068000d5d4551b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 1 Mar 2022 18:17:44 +0100 Subject: [PATCH 23/26] init notmuch backend e2e tests --- src/backends/maildir/maildir_backend.rs | 13 +- src/backends/notmuch/notmuch_backend.rs | 203 ++++++++++++++++-------- src/main.rs | 16 +- tests/emails/alice-to-patrick.eml | 1 + tests/test_notmuch_backend.rs | 81 ++++++++++ 5 files changed, 231 insertions(+), 83 deletions(-) create mode 100644 tests/test_notmuch_backend.rs diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index 5aa974f..c3e64b4 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -9,7 +9,7 @@ use std::{convert::TryInto, fs, path::PathBuf}; use crate::{ backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes}, - config::{AccountConfig, MaildirBackendConfig, DEFAULT_INBOX_FOLDER}, + config::{AccountConfig, MaildirBackendConfig}, mbox::Mboxes, msg::{Envelopes, Msg}, }; @@ -40,17 +40,10 @@ impl<'a> MaildirBackend<'a> { } /// Creates a maildir instance from a string slice. - fn get_mdir_from_dir(&self, dir: &str) -> Result { - let inbox_folder = self - .account_config - .mailboxes - .get("inbox") - .map(|s| s.as_str()) - .unwrap_or(DEFAULT_INBOX_FOLDER); - + pub fn get_mdir_from_dir(&self, dir: &str) -> Result { // If the dir points to the inbox folder, creates a maildir // instance from the root folder. - if dir == inbox_folder { + if dir == "inbox" { self.validate_mdir_path(self.mdir.path().to_owned()) .map(maildir::Maildir::from) } else { diff --git a/src/backends/notmuch/notmuch_backend.rs b/src/backends/notmuch/notmuch_backend.rs index fc85175..6393d68 100644 --- a/src/backends/notmuch/notmuch_backend.rs +++ b/src/backends/notmuch/notmuch_backend.rs @@ -4,17 +4,17 @@ use anyhow::{anyhow, Context, Result}; use log::{debug, info, trace}; use crate::{ - backends::{Backend, IdMapper, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes}, + backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes}, config::{AccountConfig, NotmuchBackendConfig}, mbox::Mboxes, msg::{Envelopes, Msg}, }; /// Represents the Notmuch backend. -#[derive(Debug)] pub struct NotmuchBackend<'a> { account_config: &'a AccountConfig, notmuch_config: &'a NotmuchBackendConfig, + pub mdir: &'a mut MaildirBackend<'a>, db: notmuch::Database, } @@ -22,12 +22,14 @@ impl<'a> NotmuchBackend<'a> { pub fn new( account_config: &'a AccountConfig, notmuch_config: &'a NotmuchBackendConfig, - ) -> Result { + mdir: &'a mut MaildirBackend<'a>, + ) -> Result> { info!(">> create new notmuch backend"); let backend = Self { account_config, notmuch_config, + mdir, db: notmuch::Database::open( notmuch_config.notmuch_database_dir.clone(), notmuch::DatabaseMode::ReadWrite, @@ -39,7 +41,6 @@ impl<'a> NotmuchBackend<'a> { ) })?, }; - trace!("backend: {:?}", backend); info!("<< create new notmuch backend"); Ok(backend) @@ -68,10 +69,10 @@ impl<'a> NotmuchBackend<'a> { let page_begin = page * page_size; debug!("page begin: {:?}", page_begin); if page_begin > envelopes.len() { - return Err(anyhow!(format!( + return Err(anyhow!( "cannot get notmuch envelopes at page {:?} (out of bounds)", page_begin + 1, - ))); + )); } let page_end = envelopes.len().min(page_begin + page_size); debug!("page end: {:?}", page_end); @@ -192,17 +193,75 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { Ok(envelopes) } - fn add_msg( - &mut self, - _virt_mbox: &str, - _msg: &[u8], - _flags: &str, - ) -> Result> { + fn add_msg(&mut self, dir: &str, msg: &[u8], tags: &str) -> Result> { info!(">> add notmuch envelopes"); + debug!("dir: {:?}", dir); + debug!("tags: {:?}", tags); + + let mdir = self + .mdir + .get_mdir_from_dir(dir) + .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; + let mdir_path_str = mdir + .path() + .to_str() + .ok_or_else(|| anyhow!("cannot parse maildir path to string"))?; + + // Adds the message to the maildir folder and gets its hash. + let hash = self + .mdir + .add_msg(mdir_path_str, msg, "seen") + .with_context(|| { + format!( + "cannot add notmuch message to maildir {:?}", + self.notmuch_config.notmuch_database_dir + ) + })? + .to_string(); + debug!("hash: {:?}", hash); + + // Retrieves the file path of the added message by its maildir + // identifier. + let id = IdMapper::new(mdir.path()) + .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? + .find(&hash) + .with_context(|| format!("cannot find notmuch message from short hash {:?}", hash))?; + debug!("id: {:?}", id); + let file_path = mdir.path().join("cur").join(format!("{}:2,S", id)); + debug!("file path: {:?}", file_path); + + // Adds the message to the notmuch database by indexing it. + let id = self + .db + .index_file(&file_path, None) + .with_context(|| format!("cannot index notmuch message from file {:?}", file_path))? + .id() + .to_string(); + let hash = format!("{:x}", md5::compute(&id)); + + // Appends hash entry to the id mapper cache file. + let mut mapper = + IdMapper::new(&self.notmuch_config.notmuch_database_dir).with_context(|| { + format!( + "cannot create id mapper instance for {:?}", + self.notmuch_config.notmuch_database_dir + ) + })?; + mapper + .append(vec![(hash.clone(), id.clone())]) + .with_context(|| { + format!( + "cannot append hash {:?} with id {:?} to id mapper", + hash, id + ) + })?; + + // Attaches tags to the notmuch message. + self.add_flags("", &hash, tags) + .with_context(|| format!("cannot add flags to notmuch message {:?}", id))?; + info!("<< add notmuch envelopes"); - Err(anyhow!( - "cannot add notmuch envelopes: feature not implemented" - )) + Ok(Box::new(hash)) } fn get_msg(&mut self, _virt_mbox: &str, short_hash: &str) -> Result { @@ -288,34 +347,35 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { Ok(()) } - fn add_flags(&mut self, virt_mbox: &str, query: &str, tags: &str) -> Result<()> { + fn add_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> { info!(">> add notmuch message flags"); debug!("tags: {:?}", tags); - debug!("query: {:?}", query); - let query = self - .account_config - .mailboxes - .get(virt_mbox) - .map(|s| s.as_str()) - .unwrap_or(query); - debug!("final query: {:?}", query); + let dir = &self.notmuch_config.notmuch_database_dir; + let id = IdMapper::new(dir) + .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? + .find(short_hash) + .with_context(|| { + format!( + "cannot find notmuch message from short hash {:?}", + short_hash + ) + })?; + debug!("id: {:?}", id); + let query = format!("id:{}", id); + debug!("query: {:?}", query); let tags: Vec<_> = tags.split_whitespace().collect(); let query_builder = self .db - .create_query(query) + .create_query(&query) .with_context(|| format!("cannot create notmuch query from {:?}", query))?; - let envelopes = query_builder + let msgs = query_builder .search_messages() .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; - for envelope in envelopes { + for msg in msgs { for tag in tags.iter() { - envelope.add_tag(*tag).with_context(|| { - format!( - "cannot add tag {:?} to notmuch message {:?}", - tag, - envelope.id() - ) + msg.add_tag(*tag).with_context(|| { + format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id()) })? } } @@ -324,40 +384,38 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { Ok(()) } - fn set_flags(&mut self, virt_mbox: &str, query: &str, tags: &str) -> Result<()> { + fn set_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> { info!(">> set notmuch message flags"); debug!("tags: {:?}", tags); - debug!("query: {:?}", query); - let query = self - .account_config - .mailboxes - .get(virt_mbox) - .map(|s| s.as_str()) - .unwrap_or(query); - debug!("final query: {:?}", query); + let dir = &self.notmuch_config.notmuch_database_dir; + let id = IdMapper::new(dir) + .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? + .find(short_hash) + .with_context(|| { + format!( + "cannot find notmuch message from short hash {:?}", + short_hash + ) + })?; + debug!("id: {:?}", id); + let query = format!("id:{}", id); + debug!("query: {:?}", query); let tags: Vec<_> = tags.split_whitespace().collect(); let query_builder = self .db - .create_query(query) + .create_query(&query) .with_context(|| format!("cannot create notmuch query from {:?}", query))?; - let envelopes = query_builder + let msgs = query_builder .search_messages() .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; - for envelope in envelopes { - envelope.remove_all_tags().with_context(|| { - format!( - "cannot remove all tags from notmuch message {:?}", - envelope.id() - ) + for msg in msgs { + msg.remove_all_tags().with_context(|| { + format!("cannot remove all tags from notmuch message {:?}", msg.id()) })?; for tag in tags.iter() { - envelope.add_tag(*tag).with_context(|| { - format!( - "cannot add tag {:?} to notmuch message {:?}", - tag, - envelope.id() - ) + msg.add_tag(*tag).with_context(|| { + format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id()) })? } } @@ -366,33 +424,38 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { Ok(()) } - fn del_flags(&mut self, virt_mbox: &str, query: &str, tags: &str) -> Result<()> { + fn del_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> { info!(">> delete notmuch message flags"); debug!("tags: {:?}", tags); - debug!("query: {:?}", query); - let query = self - .account_config - .mailboxes - .get(virt_mbox) - .map(|s| s.as_str()) - .unwrap_or(query); - debug!("final query: {:?}", query); + let dir = &self.notmuch_config.notmuch_database_dir; + let id = IdMapper::new(dir) + .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? + .find(short_hash) + .with_context(|| { + format!( + "cannot find notmuch message from short hash {:?}", + short_hash + ) + })?; + debug!("id: {:?}", id); + let query = format!("id:{}", id); + debug!("query: {:?}", query); let tags: Vec<_> = tags.split_whitespace().collect(); let query_builder = self .db - .create_query(query) + .create_query(&query) .with_context(|| format!("cannot create notmuch query from {:?}", query))?; - let envelopes = query_builder + let msgs = query_builder .search_messages() .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; - for envelope in envelopes { + for msg in msgs { for tag in tags.iter() { - envelope.remove_tag(*tag).with_context(|| { + msg.remove_tag(*tag).with_context(|| { format!( "cannot delete tag {:?} from notmuch message {:?}", tag, - envelope.id() + msg.id() ) })? } diff --git a/src/main.rs b/src/main.rs index c99d4cc..7165bf3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use himalaya::{ compl::{compl_arg, compl_handler}, config::{ account_args, config_args, AccountConfig, BackendConfig, DeserializedConfig, - DEFAULT_INBOX_FOLDER, + MaildirBackendConfig, DEFAULT_INBOX_FOLDER, }, mbox::{mbox_arg, mbox_handler}, msg::{flag_arg, flag_handler, msg_arg, msg_handler, tpl_arg, tpl_handler}, @@ -51,6 +51,7 @@ fn main() -> Result<()> { let mut imap; let mut maildir; + let maildir_config; #[cfg(feature = "notmuch")] let mut notmuch; let backend: Box<&mut dyn Backend> = match backend_config { @@ -64,7 +65,11 @@ fn main() -> Result<()> { } #[cfg(feature = "notmuch")] BackendConfig::Notmuch(ref notmuch_config) => { - notmuch = NotmuchBackend::new(&account_config, notmuch_config)?; + maildir_config = MaildirBackendConfig { + maildir_dir: notmuch_config.notmuch_database_dir.clone(), + }; + maildir = MaildirBackend::new(&account_config, &maildir_config); + notmuch = NotmuchBackend::new(&account_config, notmuch_config, &mut maildir)?; Box::new(&mut notmuch) } }; @@ -95,6 +100,7 @@ fn main() -> Result<()> { let mut printer = StdoutPrinter::try_from(m.value_of("output"))?; let mut imap; let mut maildir; + let maildir_config; #[cfg(feature = "notmuch")] let mut notmuch; let backend: Box<&mut dyn Backend> = match backend_config { @@ -108,7 +114,11 @@ fn main() -> Result<()> { } #[cfg(feature = "notmuch")] BackendConfig::Notmuch(ref notmuch_config) => { - notmuch = NotmuchBackend::new(&account_config, notmuch_config)?; + maildir_config = MaildirBackendConfig { + maildir_dir: notmuch_config.notmuch_database_dir.clone(), + }; + maildir = MaildirBackend::new(&account_config, &maildir_config); + notmuch = NotmuchBackend::new(&account_config, notmuch_config, &mut maildir)?; Box::new(&mut notmuch) } }; diff --git a/tests/emails/alice-to-patrick.eml b/tests/emails/alice-to-patrick.eml index 1fd4651..2cef116 100644 --- a/tests/emails/alice-to-patrick.eml +++ b/tests/emails/alice-to-patrick.eml @@ -2,5 +2,6 @@ From: alice@localhost To: patrick@localhost Subject: Plain message Content-Type: text/plain; charset=utf-8 +Date: Tue, 1 Mar 2022 12:00:00 +0000 Ceci est un message. \ No newline at end of file diff --git a/tests/test_notmuch_backend.rs b/tests/test_notmuch_backend.rs new file mode 100644 index 0000000..d015073 --- /dev/null +++ b/tests/test_notmuch_backend.rs @@ -0,0 +1,81 @@ +use std::{collections::HashMap, env, fs, iter::FromIterator}; + +use himalaya::{ + backends::{Backend, MaildirBackend, NotmuchBackend, NotmuchEnvelopes}, + config::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig}, +}; + +#[test] +fn test_notmuch_backend() { + // set up maildir folders and notmuch database + let mdir: maildir::Maildir = env::temp_dir().join("himalaya-test-notmuch").into(); + if let Err(_) = fs::remove_dir_all(mdir.path()) {} + mdir.create_dirs().unwrap(); + notmuch::Database::create(mdir.path()).unwrap(); + + // configure accounts + let account_config = AccountConfig { + mailboxes: HashMap::from_iter([("inbox".into(), "*".into())]), + ..AccountConfig::default() + }; + let mdir_config = MaildirBackendConfig { + maildir_dir: mdir.path().to_owned(), + }; + let notmuch_config = NotmuchBackendConfig { + notmuch_database_dir: mdir.path().to_owned(), + }; + let mut mdir = MaildirBackend::new(&account_config, &mdir_config); + let mut notmuch = NotmuchBackend::new(&account_config, ¬much_config, &mut mdir).unwrap(); + + // check that a message can be added + let msg = include_bytes!("./emails/alice-to-patrick.eml"); + let hash = notmuch.add_msg("", msg, "inbox seen").unwrap().to_string(); + + // check that the added message exists + let msg = notmuch.get_msg("", &hash).unwrap(); + assert_eq!("alice@localhost", msg.from.clone().unwrap().to_string()); + assert_eq!("patrick@localhost", msg.to.clone().unwrap().to_string()); + assert_eq!("Ceci est un message.", msg.fold_text_plain_parts()); + + // check that the envelope of the added message exists + let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); + let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); + let envelope = envelopes.first().unwrap(); + assert_eq!(1, envelopes.len()); + assert_eq!("alice@localhost", envelope.sender); + assert_eq!("Plain message", envelope.subject); + + // check that a flag can be added to the message + notmuch + .add_flags("", &envelope.hash, "flagged passed") + .unwrap(); + let envelopes = notmuch.get_envelopes("inbox", 1, 0).unwrap(); + let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); + let envelope = envelopes.first().unwrap(); + assert!(envelope.flags.contains(&"inbox".into())); + assert!(envelope.flags.contains(&"seen".into())); + assert!(envelope.flags.contains(&"flagged".into())); + assert!(envelope.flags.contains(&"passed".into())); + + // check that the message flags can be changed + notmuch + .set_flags("", &envelope.hash, "inbox passed") + .unwrap(); + let envelopes = notmuch.get_envelopes("inbox", 1, 0).unwrap(); + let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); + let envelope = envelopes.first().unwrap(); + assert!(envelope.flags.contains(&"inbox".into())); + assert!(!envelope.flags.contains(&"seen".into())); + assert!(!envelope.flags.contains(&"flagged".into())); + assert!(envelope.flags.contains(&"passed".into())); + + // check that a flag can be removed from the message + notmuch.del_flags("", &envelope.hash, "passed").unwrap(); + let envelopes = notmuch.get_envelopes("inbox", 1, 0).unwrap(); + let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); + let envelope = envelopes.first().unwrap(); + assert!(envelope.flags.contains(&"inbox".into())); + assert!(!envelope.flags.contains(&"seen".into())); + assert!(!envelope.flags.contains(&"flagged".into())); + assert!(!envelope.flags.contains(&"passed".into())); +} From 5f13489e83563cc14ae2672334f76e9980678e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 1 Mar 2022 22:44:40 +0100 Subject: [PATCH 24/26] improve maildir and notmuch tests --- src/backends/id_mapper.rs | 21 +++++------- src/backends/maildir/maildir_backend.rs | 6 ++++ src/backends/notmuch/notmuch_backend.rs | 34 ++++++------------- tests/test_maildir_backend.rs | 45 +++++++++++++------------ tests/test_notmuch_backend.rs | 10 ++++-- 5 files changed, 56 insertions(+), 60 deletions(-) diff --git a/src/backends/id_mapper.rs b/src/backends/id_mapper.rs index 7ca77a1..953a729 100644 --- a/src/backends/id_mapper.rs +++ b/src/backends/id_mapper.rs @@ -76,42 +76,39 @@ impl IdMapper { } pub fn append(&mut self, lines: Vec<(String, String)>) -> Result { - let mut entries = String::new(); + self.extend(lines); - self.extend(lines.clone()); + let mut entries = String::new(); + let mut short_hash_len = self.short_hash_len; for (hash, id) in self.iter() { - entries.push_str(&format!("{} {}\n", hash, id)); - } - - for (hash, id) in lines { loop { let short_hash = &hash[0..self.short_hash_len]; let conflict_found = self .map .keys() - .find(|cached_hash| { - cached_hash.starts_with(short_hash) && *cached_hash != &hash - }) + .find(|cached_hash| cached_hash.starts_with(short_hash) && cached_hash != &hash) .is_some(); if self.short_hash_len > 32 || !conflict_found { break; } - self.short_hash_len += 1; + short_hash_len += 1; } entries.push_str(&format!("{} {}\n", hash, id)); } + self.short_hash_len = short_hash_len; + OpenOptions::new() .write(true) .create(true) .truncate(true) .open(&self.path) .context("cannot open maildir id hash map cache")? - .write(format!("{}\n{}", self.short_hash_len, entries).as_bytes()) + .write(format!("{}\n{}", short_hash_len, entries).as_bytes()) .context("cannot write maildir id hash map cache")?; - Ok(self.short_hash_len) + Ok(short_hash_len) } } diff --git a/src/backends/maildir/maildir_backend.rs b/src/backends/maildir/maildir_backend.rs index c3e64b4..2bf3a9c 100644 --- a/src/backends/maildir/maildir_backend.rs +++ b/src/backends/maildir/maildir_backend.rs @@ -54,6 +54,12 @@ impl<'a> MaildirBackend<'a> { // maildir subdirectory by adding a "." in front // of the name as described in the spec: // https://cr.yp.to/proto/maildir.html + let dir = self + .account_config + .mailboxes + .get(dir) + .map(|s| s.as_str()) + .unwrap_or(dir); let path = self.mdir.path().join(format!(".{}", dir)); self.validate_mdir_path(path) }) diff --git a/src/backends/notmuch/notmuch_backend.rs b/src/backends/notmuch/notmuch_backend.rs index 6393d68..416e1c0 100644 --- a/src/backends/notmuch/notmuch_backend.rs +++ b/src/backends/notmuch/notmuch_backend.rs @@ -193,24 +193,16 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { Ok(envelopes) } - fn add_msg(&mut self, dir: &str, msg: &[u8], tags: &str) -> Result> { + fn add_msg(&mut self, _: &str, msg: &[u8], tags: &str) -> Result> { info!(">> add notmuch envelopes"); - debug!("dir: {:?}", dir); debug!("tags: {:?}", tags); - let mdir = self - .mdir - .get_mdir_from_dir(dir) - .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let mdir_path_str = mdir - .path() - .to_str() - .ok_or_else(|| anyhow!("cannot parse maildir path to string"))?; + let dir = &self.notmuch_config.notmuch_database_dir; // Adds the message to the maildir folder and gets its hash. let hash = self .mdir - .add_msg(mdir_path_str, msg, "seen") + .add_msg("inbox", msg, "seen") .with_context(|| { format!( "cannot add notmuch message to maildir {:?}", @@ -222,12 +214,13 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { // Retrieves the file path of the added message by its maildir // identifier. - let id = IdMapper::new(mdir.path()) - .with_context(|| format!("cannot create id mapper instance for {:?}", dir))? + let mut mapper = IdMapper::new(dir) + .with_context(|| format!("cannot create id mapper instance for {:?}", dir))?; + let id = mapper .find(&hash) .with_context(|| format!("cannot find notmuch message from short hash {:?}", hash))?; debug!("id: {:?}", id); - let file_path = mdir.path().join("cur").join(format!("{}:2,S", id)); + let file_path = dir.join("cur").join(format!("{}:2,S", id)); debug!("file path: {:?}", file_path); // Adds the message to the notmuch database by indexing it. @@ -240,13 +233,6 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { let hash = format!("{:x}", md5::compute(&id)); // Appends hash entry to the id mapper cache file. - let mut mapper = - IdMapper::new(&self.notmuch_config.notmuch_database_dir).with_context(|| { - format!( - "cannot create id mapper instance for {:?}", - self.notmuch_config.notmuch_database_dir - ) - })?; mapper .append(vec![(hash.clone(), id.clone())]) .with_context(|| { @@ -264,7 +250,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { Ok(Box::new(hash)) } - fn get_msg(&mut self, _virt_mbox: &str, short_hash: &str) -> Result { + fn get_msg(&mut self, _: &str, short_hash: &str) -> Result { info!(">> add notmuch envelopes"); debug!("short hash: {:?}", short_hash); @@ -300,7 +286,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { Ok(msg) } - fn copy_msg(&mut self, _mbox_src: &str, _mbox_dst: &str, _id: &str) -> Result<()> { + fn copy_msg(&mut self, _dir_src: &str, _dir_dst: &str, _short_hash: &str) -> Result<()> { info!(">> copy notmuch message"); info!("<< copy notmuch message"); Err(anyhow!( @@ -308,7 +294,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { )) } - fn move_msg(&mut self, _src: &str, _dst: &str, _id: &str) -> Result<()> { + fn move_msg(&mut self, _dir_src: &str, _dir_dst: &str, _short_hash: &str) -> Result<()> { info!(">> move notmuch message"); info!("<< move notmuch message"); Err(anyhow!( diff --git a/tests/test_maildir_backend.rs b/tests/test_maildir_backend.rs index adcc2e2..d998789 100644 --- a/tests/test_maildir_backend.rs +++ b/tests/test_maildir_backend.rs @@ -19,7 +19,10 @@ fn test_maildir_backend() { // configure accounts let account_config = AccountConfig { - mailboxes: HashMap::from_iter([("inbox".into(), "INBOX".into())]), + mailboxes: HashMap::from_iter([ + ("inbox".into(), "INBOX".into()), + ("subdir".into(), "Subdir".into()), + ]), ..AccountConfig::default() }; let mdir_config = MaildirBackendConfig { @@ -33,16 +36,16 @@ fn test_maildir_backend() { // check that a message can be added let msg = include_bytes!("./emails/alice-to-patrick.eml"); - let hash = mdir.add_msg("INBOX", msg, "seen").unwrap().to_string(); + let hash = mdir.add_msg("inbox", msg, "seen").unwrap().to_string(); // check that the added message exists - let msg = mdir.get_msg("INBOX", &hash).unwrap(); + let msg = mdir.get_msg("inbox", &hash).unwrap(); assert_eq!("alice@localhost", msg.from.clone().unwrap().to_string()); assert_eq!("patrick@localhost", msg.to.clone().unwrap().to_string()); assert_eq!("Ceci est un message.", msg.fold_text_plain_parts()); // check that the envelope of the added message exists - let envelopes = mdir.get_envelopes("INBOX", 10, 0).unwrap(); + let envelopes = mdir.get_envelopes("inbox", 10, 0).unwrap(); let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); assert_eq!(1, envelopes.len()); @@ -50,9 +53,9 @@ fn test_maildir_backend() { assert_eq!("Plain message", envelope.subject); // check that a flag can be added to the message - mdir.add_flags("INBOX", &envelope.hash, "flagged passed") + mdir.add_flags("inbox", &envelope.hash, "flagged passed") .unwrap(); - let envelopes = mdir.get_envelopes("INBOX", 1, 0).unwrap(); + let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap(); let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); assert!(envelope.flags.contains(&MaildirFlag::Seen)); @@ -60,8 +63,8 @@ fn test_maildir_backend() { assert!(envelope.flags.contains(&MaildirFlag::Passed)); // check that the message flags can be changed - mdir.set_flags("INBOX", &envelope.hash, "passed").unwrap(); - let envelopes = mdir.get_envelopes("INBOX", 1, 0).unwrap(); + mdir.set_flags("inbox", &envelope.hash, "passed").unwrap(); + let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap(); let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); assert!(!envelope.flags.contains(&MaildirFlag::Seen)); @@ -69,8 +72,8 @@ fn test_maildir_backend() { assert!(envelope.flags.contains(&MaildirFlag::Passed)); // check that a flag can be removed from the message - mdir.del_flags("INBOX", &envelope.hash, "passed").unwrap(); - let envelopes = mdir.get_envelopes("INBOX", 1, 0).unwrap(); + mdir.del_flags("inbox", &envelope.hash, "passed").unwrap(); + let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap(); let envelopes: &MaildirEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); assert!(!envelope.flags.contains(&MaildirFlag::Seen)); @@ -78,19 +81,19 @@ fn test_maildir_backend() { assert!(!envelope.flags.contains(&MaildirFlag::Passed)); // check that the message can be copied - mdir.copy_msg("INBOX", "Subdir", &envelope.hash).unwrap(); - assert!(mdir.get_msg("INBOX", &hash).is_ok()); - assert!(mdir.get_msg("Subdir", &hash).is_ok()); - assert!(mdir_subdir.get_msg("INBOX", &hash).is_ok()); + mdir.copy_msg("inbox", "subdir", &envelope.hash).unwrap(); + assert!(mdir.get_msg("inbox", &hash).is_ok()); + assert!(mdir.get_msg("subdir", &hash).is_ok()); + assert!(mdir_subdir.get_msg("inbox", &hash).is_ok()); // check that the message can be moved - mdir.move_msg("INBOX", "Subdir", &envelope.hash).unwrap(); - assert!(mdir.get_msg("INBOX", &hash).is_err()); - assert!(mdir.get_msg("Subdir", &hash).is_ok()); - assert!(mdir_subdir.get_msg("INBOX", &hash).is_ok()); + mdir.move_msg("inbox", "subdir", &envelope.hash).unwrap(); + assert!(mdir.get_msg("inbox", &hash).is_err()); + assert!(mdir.get_msg("subdir", &hash).is_ok()); + assert!(mdir_subdir.get_msg("inbox", &hash).is_ok()); // check that the message can be deleted - mdir.del_msg("Subdir", &hash).unwrap(); - assert!(mdir.get_msg("Subdir", &hash).is_err()); - assert!(mdir_subdir.get_msg("INBOX", &hash).is_err()); + mdir.del_msg("subdir", &hash).unwrap(); + assert!(mdir.get_msg("subdir", &hash).is_err()); + assert!(mdir_subdir.get_msg("inbox", &hash).is_err()); } diff --git a/tests/test_notmuch_backend.rs b/tests/test_notmuch_backend.rs index d015073..93e707b 100644 --- a/tests/test_notmuch_backend.rs +++ b/tests/test_notmuch_backend.rs @@ -49,7 +49,7 @@ fn test_notmuch_backend() { notmuch .add_flags("", &envelope.hash, "flagged passed") .unwrap(); - let envelopes = notmuch.get_envelopes("inbox", 1, 0).unwrap(); + let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); assert!(envelope.flags.contains(&"inbox".into())); @@ -61,7 +61,7 @@ fn test_notmuch_backend() { notmuch .set_flags("", &envelope.hash, "inbox passed") .unwrap(); - let envelopes = notmuch.get_envelopes("inbox", 1, 0).unwrap(); + let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); assert!(envelope.flags.contains(&"inbox".into())); @@ -71,11 +71,15 @@ fn test_notmuch_backend() { // check that a flag can be removed from the message notmuch.del_flags("", &envelope.hash, "passed").unwrap(); - let envelopes = notmuch.get_envelopes("inbox", 1, 0).unwrap(); + let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); let envelopes: &NotmuchEnvelopes = envelopes.as_any().downcast_ref().unwrap(); let envelope = envelopes.first().unwrap(); assert!(envelope.flags.contains(&"inbox".into())); assert!(!envelope.flags.contains(&"seen".into())); assert!(!envelope.flags.contains(&"flagged".into())); assert!(!envelope.flags.contains(&"passed".into())); + + // check that the message can be deleted + notmuch.del_msg("", &hash).unwrap(); + assert!(notmuch.get_msg("inbox", &hash).is_err()); } From 21e5658e438b7872efebec54a01ee2ee95fb478f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 1 Mar 2022 22:50:24 +0100 Subject: [PATCH 25/26] fix errors and warns notmuch cargo feature --- src/main.rs | 10 ++++++---- tests/test_notmuch_backend.rs | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7165bf3..2d543d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use himalaya::{ compl::{compl_arg, compl_handler}, config::{ account_args, config_args, AccountConfig, BackendConfig, DeserializedConfig, - MaildirBackendConfig, DEFAULT_INBOX_FOLDER, + DEFAULT_INBOX_FOLDER, }, mbox::{mbox_arg, mbox_handler}, msg::{flag_arg, flag_handler, msg_arg, msg_handler, tpl_arg, tpl_handler}, @@ -16,7 +16,7 @@ use himalaya::{ }; #[cfg(feature = "notmuch")] -use himalaya::backends::NotmuchBackend; +use himalaya::{backends::NotmuchBackend, config::MaildirBackendConfig}; fn create_app<'a>() -> clap::App<'a, 'a> { clap::App::new(env!("CARGO_PKG_NAME")) @@ -51,7 +51,8 @@ fn main() -> Result<()> { let mut imap; let mut maildir; - let maildir_config; + #[cfg(feature = "notmuch")] + let maildir_config: MaildirBackendConfig; #[cfg(feature = "notmuch")] let mut notmuch; let backend: Box<&mut dyn Backend> = match backend_config { @@ -100,7 +101,8 @@ fn main() -> Result<()> { let mut printer = StdoutPrinter::try_from(m.value_of("output"))?; let mut imap; let mut maildir; - let maildir_config; + #[cfg(feature = "notmuch")] + let maildir_config: MaildirBackendConfig; #[cfg(feature = "notmuch")] let mut notmuch; let backend: Box<&mut dyn Backend> = match backend_config { diff --git a/tests/test_notmuch_backend.rs b/tests/test_notmuch_backend.rs index 93e707b..183fab9 100644 --- a/tests/test_notmuch_backend.rs +++ b/tests/test_notmuch_backend.rs @@ -1,10 +1,13 @@ +#[cfg(feature = "notmuch")] use std::{collections::HashMap, env, fs, iter::FromIterator}; +#[cfg(feature = "notmuch")] use himalaya::{ backends::{Backend, MaildirBackend, NotmuchBackend, NotmuchEnvelopes}, config::{AccountConfig, MaildirBackendConfig, NotmuchBackendConfig}, }; +#[cfg(feature = "notmuch")] #[test] fn test_notmuch_backend() { // set up maildir folders and notmuch database From 4e24d04faf2c2508fe7f1646baf1b33d94f3c87c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 1 Mar 2022 23:12:21 +0100 Subject: [PATCH 26/26] update changelog, prepare v0.5.7 --- CHANGELOG.md | 12 +++++++++++- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ec8fda..1aef8cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.5.7] - 2022-03-01 + +### Added + +- Notmuch support [#57] + ### Fixed - Build failure due to `imap` version [#303] @@ -16,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - [**BREAKING**] Replace `inbox-folder`, `sent-folder` and `draft-folder` by a generic hashmap `mailboxes` +- Display short envelopes id for `maildir` and `notmuch` backends [#309] ## [0.5.6] - 2022-02-22 @@ -314,7 +321,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Password from command [#22] - Set up README [#20] -[unreleased]: https://github.com/soywod/himalaya/compare/v0.5.6...HEAD +[unreleased]: https://github.com/soywod/himalaya/compare/v0.5.7...HEAD +[0.5.7]: https://github.com/soywod/himalaya/compare/v0.5.6...v0.5.7 [0.5.6]: https://github.com/soywod/himalaya/compare/v0.5.5...v0.5.6 [0.5.5]: https://github.com/soywod/himalaya/compare/v0.5.4...v0.5.5 [0.5.4]: https://github.com/soywod/himalaya/compare/v0.5.3...v0.5.4 @@ -374,6 +382,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#48]: https://github.com/soywod/himalaya/issues/48 [#50]: https://github.com/soywod/himalaya/issues/50 [#54]: https://github.com/soywod/himalaya/issues/54 +[#57]: https://github.com/soywod/himalaya/issues/57 [#58]: https://github.com/soywod/himalaya/issues/58 [#59]: https://github.com/soywod/himalaya/issues/59 [#61]: https://github.com/soywod/himalaya/issues/61 @@ -446,3 +455,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#303]: https://github.com/soywod/himalaya/issues/303 [#305]: https://github.com/soywod/himalaya/issues/305 [#308]: https://github.com/soywod/himalaya/issues/308 +[#309]: https://github.com/soywod/himalaya/issues/309 diff --git a/Cargo.lock b/Cargo.lock index 665ee9c..c51aa6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,7 +436,7 @@ dependencies = [ [[package]] name = "himalaya" -version = "0.5.6" +version = "0.5.7" dependencies = [ "ammonia", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 51d8451..ee3f0a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "himalaya" description = "Command-line interface for email management" -version = "0.5.6" +version = "0.5.7" authors = ["soywod "] edition = "2018" license-file = "LICENSE"