From e9b87b2e40303649f838ad6b5f56d5093332b233 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Mon, 5 Aug 2024 09:07:02 +0300 Subject: [PATCH] melib/maildr: add rename_regex config option Add optional rename_regex configuration option to allow stripping patterns from pathnames when renaming them. This is useful when other programs depend on specific substrings being unique like mbsync which erroneously assumes UIDs are unique instead of UID+UIDVALIDITY+mailbox name like the IMAP standard specifies. Closes #463 Signed-off-by: Manos Pitsidianakis --- Cargo.lock | 1 + meli/src/conf.rs | 7 +- melib/Cargo.toml | 1 + melib/src/conf.rs | 5 +- melib/src/error.rs | 7 + melib/src/maildir/backend.rs | 143 ++++++++++-------- melib/src/maildir/mod.rs | 71 ++++++++- melib/src/maildir/stream.rs | 3 +- melib/src/maildir/tests.rs | 231 +++++++++++++++++++++++++++++ melib/src/maildir/watch.rs | 6 +- melib/tests/integration/configs.rs | 115 ++++++++++++++ melib/tests/integration/main.rs | 1 + 12 files changed, 524 insertions(+), 67 deletions(-) create mode 100644 melib/src/maildir/tests.rs create mode 100644 melib/tests/integration/configs.rs diff --git a/Cargo.lock b/Cargo.lock index 288416cf..1f78aa5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1398,6 +1398,7 @@ dependencies = [ "socket2", "stderrlog", "tempfile", + "toml", "unicode-segmentation", "url", "uuid", diff --git a/meli/src/conf.rs b/meli/src/conf.rs index e2240d05..34a733a6 100644 --- a/meli/src/conf.rs +++ b/meli/src/conf.rs @@ -674,8 +674,13 @@ mod deserializers { D: Deserializer<'de>, { let v: Value = Deserialize::deserialize(deserializer)?; + if let Some(s) = v.as_str() { + return Ok(s.to_string()); + } let mut ret = v.to_string(); - if ret.starts_with('"') && ret.ends_with('"') { + if (ret.starts_with('"') && ret.ends_with('"')) + || (ret.starts_with('\"') && ret.ends_with('\'')) + { ret.drain(0..1).count(); ret.drain(ret.len() - 1..).count(); } diff --git a/melib/Cargo.toml b/melib/Cargo.toml index f726a0ae..5d996e1b 100644 --- a/melib/Cargo.toml +++ b/melib/Cargo.toml @@ -84,3 +84,4 @@ mailin-embedded = { version = "0.8", features = ["rtls"] } sealed_test = { version = "1.1.0" } stderrlog = { version = "^0.5" } tempfile = { version = "3.3" } +toml = { version = "0.8", default-features = false, features = ["display","preserve_order","parse"] } diff --git a/melib/src/conf.rs b/melib/src/conf.rs index 1a9cc7e6..93eaaf9f 100644 --- a/melib/src/conf.rs +++ b/melib/src/conf.rs @@ -34,7 +34,7 @@ use crate::{ }; pub use crate::{SortField, SortOrder}; -#[derive(Clone, Debug, Default, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct AccountSettings { pub name: String, /// Name of mailbox that is the root of the mailbox hierarchy. @@ -44,8 +44,11 @@ pub struct AccountSettings { pub root_mailbox: String, pub format: String, pub identity: String, + #[serde(default)] pub extra_identities: Vec, + #[serde(default = "false_val")] pub read_only: bool, + #[serde(default)] pub display_name: Option, #[serde(default)] pub order: (SortField, SortOrder), diff --git a/melib/src/error.rs b/melib/src/error.rs index 29814645..202ac2d9 100644 --- a/melib/src/error.rs +++ b/melib/src/error.rs @@ -147,6 +147,13 @@ pub struct Error { pub kind: ErrorKind, } +#[cfg(test)] +impl PartialEq for Error { + fn eq(&self, other: &Self) -> bool { + self.to_string().eq(&other.to_string()) + } +} + pub trait IntoError { fn set_err_summary(self, msg: M) -> Error where diff --git a/melib/src/maildir/backend.rs b/melib/src/maildir/backend.rs index ceb7880c..b2e2d786 100644 --- a/melib/src/maildir/backend.rs +++ b/melib/src/maildir/backend.rs @@ -36,6 +36,7 @@ use std::{ }; use notify::{RecommendedWatcher, RecursiveMode, Watcher}; +use regex::Regex; use super::{watch, MaildirMailbox, MaildirOp, MaildirPathTrait}; use crate::{ @@ -105,6 +106,34 @@ impl DerefMut for HashIndex { pub type HashIndexes = Arc>>; +#[derive(Debug)] +pub struct Configuration { + pub rename_regex: Option, +} + +impl Configuration { + pub fn new(settings: &AccountSettings) -> Result { + let rename_regex = if let Some(v) = settings.extra.get("rename_regex").map(|v| { + Regex::new(v).map_err(|e| { + Error::new(format!( + "Configuration error ({}): Invalid value for field `{}`: {}", + settings.name.as_str(), + "rename_regex", + v, + )) + .set_source(Some(crate::src_err_arc_wrap!(e))) + .set_kind(ErrorKind::ValueError) + }) + }) { + Some(v?) + } else { + None + }; + + Ok(Self { rename_regex }) + } +} + /// The maildir backend instance type. #[derive(Debug)] pub struct MaildirType { @@ -115,23 +144,22 @@ pub struct MaildirType { pub event_consumer: BackendEventConsumer, pub collection: Collection, pub path: PathBuf, + pub config: Arc, } -pub fn move_to_cur(p: &Path) -> Result { - let mut new = p.to_path_buf(); - let file_name = p.to_string_lossy(); - let slash_pos = file_name.bytes().rposition(|c| c == b'/').unwrap() + 1; - new.pop(); - new.pop(); - - new.push("cur"); - new.push(&file_name[slash_pos..]); - if !file_name.ends_with(":2,") { - new.set_extension(":2,"); - } - log::trace!("moved to cur: {}", new.display()); - fs::rename(p, &new)?; - Ok(new) +pub fn move_to_cur(config: &Configuration, p: &Path) -> Result { + let cur = { + let mut cur = p.to_path_buf(); + cur.pop(); + cur.pop(); + cur.push("cur"); + cur + }; + let dest_path = p.place_in_dir(&cur, config)?; + log::trace!("moved to cur: {}", dest_path.display()); + #[cfg(not(test))] + fs::rename(p, &dest_path)?; + Ok(dest_path) } impl MailBackend for MaildirType { @@ -169,7 +197,15 @@ impl MailBackend for MaildirType { let path: PathBuf = mailbox.fs_path().into(); let map = self.hash_indexes.clone(); let mailbox_index = self.mailbox_index.clone(); - super::stream::MaildirStream::new(mailbox_hash, unseen, total, path, map, mailbox_index) + super::stream::MaildirStream::new( + mailbox_hash, + unseen, + total, + path, + map, + mailbox_index, + self.config.clone(), + ) } fn refresh(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()> { @@ -180,12 +216,13 @@ impl MailBackend for MaildirType { let path: PathBuf = mailbox.fs_path().into(); let map = self.hash_indexes.clone(); let mailbox_index = self.mailbox_index.clone(); + let config = self.config.clone(); Ok(Box::pin(async move { let thunk = move |sender: &BackendEventConsumer| { log::trace!("refreshing"); let mut buf = Vec::with_capacity(4096); - let files = Self::list_mail_in_maildir_fs(path.clone(), false)?; + let files = Self::list_mail_in_maildir_fs(&config, path.clone(), false)?; let mut current_hashes = { let mut map = map.lock().unwrap(); let map = map.entry(mailbox_hash).or_default(); @@ -287,6 +324,7 @@ impl MailBackend for MaildirType { mailbox_index: self.mailbox_index.clone(), root_mailbox_hash, mailbox_counts, + config: self.config.clone(), }; Ok(Box::pin(async move { watch_state.watch().await })) } @@ -315,19 +353,20 @@ impl MailBackend for MaildirType { &mut self, env_hashes: EnvelopeHashBatch, mailbox_hash: MailboxHash, - flags: SmallVec<[FlagOp; 8]>, + flag_ops: SmallVec<[FlagOp; 8]>, ) -> ResultFuture<()> { - let hash_index = self.hash_indexes.clone(); - if flags.iter().any(|op| op.is_tag()) { + if flag_ops.iter().any(|op| op.is_tag()) { return Err(Error::new("Maildir doesn't support tags.")); } + let hash_index = self.hash_indexes.clone(); + let config = self.config.clone(); Ok(Box::pin(async move { let mut hash_indexes_lck = hash_index.lock().unwrap(); let hash_index = hash_indexes_lck.entry(mailbox_hash).or_default(); for env_hash in env_hashes.iter() { - let _path = { + let path = { if !hash_index.contains_key(&env_hash) { continue; } @@ -340,38 +379,14 @@ impl MailBackend for MaildirType { hash_index[&env_hash].to_path_buf() } }; - let mut env_flags = _path.flags(); - let path = _path.to_str().unwrap(); // Assume UTF-8 validity - let idx: usize = path - .rfind(":2,") - .ok_or_else(|| Error::new(format!("Invalid email filename: {:?}", path)))? - + 3; - let mut new_name: String = path[..idx].to_string(); - for op in flags.iter() { + let mut new_flags = path.flags(); + for op in flag_ops.iter() { if let FlagOp::Set(f) | FlagOp::UnSet(f) = op { - env_flags.set(*f, op.as_bool()); + new_flags.set(*f, op.as_bool()); } } - if !(env_flags & Flag::DRAFT).is_empty() { - new_name.push('D'); - } - if !(env_flags & Flag::FLAGGED).is_empty() { - new_name.push('F'); - } - if !(env_flags & Flag::PASSED).is_empty() { - new_name.push('P'); - } - if !(env_flags & Flag::REPLIED).is_empty() { - new_name.push('R'); - } - if !(env_flags & Flag::SEEN).is_empty() { - new_name.push('S'); - } - if !(env_flags & Flag::TRASHED).is_empty() { - new_name.push('T'); - } - let new_name: PathBuf = new_name.into(); + let new_name: PathBuf = path.set_flags(new_flags, &config)?; hash_index.entry(env_hash).or_default().modified = Some(PathMod::Path(new_name.clone())); @@ -394,7 +409,7 @@ impl MailBackend for MaildirType { let hash_index = hash_indexes_lck.entry(mailbox_hash).or_default(); for env_hash in env_hashes.iter() { - let _path = { + let path = { if !hash_index.contains_key(&env_hash) { continue; } @@ -408,7 +423,7 @@ impl MailBackend for MaildirType { } }; - fs::remove_file(&_path)?; + fs::remove_file(&path)?; } Ok(()) })) @@ -421,14 +436,15 @@ impl MailBackend for MaildirType { destination_mailbox_hash: MailboxHash, move_: bool, ) -> ResultFuture<()> { - let hash_index = self.hash_indexes.clone(); if !self.mailboxes.contains_key(&source_mailbox_hash) { return Err(Error::new("Invalid source mailbox hash").set_kind(ErrorKind::Bug)); } else if !self.mailboxes.contains_key(&destination_mailbox_hash) { return Err(Error::new("Invalid destination mailbox hash").set_kind(ErrorKind::Bug)); } - let mut dest_path: PathBuf = self.mailboxes[&destination_mailbox_hash].fs_path().into(); - dest_path.push("cur"); + let hash_index = self.hash_indexes.clone(); + let config = self.config.clone(); + let mut dest_dir: PathBuf = self.mailboxes[&destination_mailbox_hash].fs_path().into(); + dest_dir.push("cur"); Ok(Box::pin(async move { let mut hash_indexes_lck = hash_index.lock().unwrap(); let hash_index = hash_indexes_lck.entry(source_mailbox_hash).or_default(); @@ -447,10 +463,7 @@ impl MailBackend for MaildirType { hash_index[&env_hash].to_path_buf() } }; - let filename = path_src.file_name().ok_or_else(|| { - format!("Could not get filename of `{}`", path_src.display(),) - })?; - dest_path.push(filename); + let dest_path = path_src.place_in_dir(&dest_dir, &config)?; hash_index.entry(env_hash).or_default().modified = Some(PathMod::Path(dest_path.clone())); if move_ { @@ -462,7 +475,6 @@ impl MailBackend for MaildirType { fs::copy(&path_src, &dest_path)?; log::trace!("success in copy"); } - dest_path.pop(); } Ok(()) })) @@ -588,6 +600,8 @@ impl MaildirType { is_subscribed: Box bool>, event_consumer: BackendEventConsumer, ) -> Result> { + let config = Arc::new(Configuration::new(settings)?); + let mut mailboxes: HashMap = Default::default(); fn recurse_mailboxes>( mailboxes: &mut HashMap, @@ -737,6 +751,7 @@ impl MaildirType { event_consumer, collection: Default::default(), path: root_mailbox, + config, })) } @@ -823,16 +838,22 @@ impl MaildirType { s.root_mailbox.as_str() ))); } + _ = Configuration::new(s)?; + _ = s.extra.swap_remove("rename_regex"); Ok(()) } - pub fn list_mail_in_maildir_fs(mut path: PathBuf, read_only: bool) -> Result> { + pub fn list_mail_in_maildir_fs( + config: &Configuration, + mut path: PathBuf, + read_only: bool, + ) -> Result> { let mut files: Vec = vec![]; path.push("new"); for p in path.read_dir()?.flatten() { if !read_only { - move_to_cur(&p.path()).ok().take(); + move_to_cur(config, &p.path()).ok().take(); } else { files.push(p.path()); } diff --git a/melib/src/maildir/mod.rs b/melib/src/maildir/mod.rs index 47de22b1..038e890c 100644 --- a/melib/src/maildir/mod.rs +++ b/melib/src/maildir/mod.rs @@ -22,9 +22,12 @@ #[macro_use] mod backend; pub use self::backend::*; +mod stream; pub mod watch; -mod stream; +#[cfg(test)] +mod tests; + use std::{ collections::{hash_map::DefaultHasher, HashMap}, fs, @@ -268,6 +271,8 @@ impl BackendMailbox for MaildirMailbox { pub trait MaildirPathTrait { fn flags(&self) -> Flag; + fn set_flags(&self, flags: Flag, config: &Configuration) -> Result; + fn place_in_dir(&self, dest_dir: &Path, config: &Configuration) -> Result; fn to_mailbox_hash(&self) -> MailboxHash; fn to_envelope_hash(&self) -> EnvelopeHash; fn is_in_new(&self) -> bool; @@ -303,6 +308,70 @@ impl MaildirPathTrait for Path { flag } + fn set_flags(&self, flags: Flag, config: &Configuration) -> Result { + let filename = self + .file_name() + .ok_or_else(|| format!("Could not get filename of `{}`", self.display(),))? + .to_string_lossy() + .to_string(); + let (idx, append_2): (usize, bool) = if let Some(idx) = filename.rfind(":2,") { + (idx + 3, false) + } else { + log::trace!( + "Invalid maildir filename: {:?}\nBacktrace:\n{}", + self, + std::backtrace::Backtrace::capture() + ); + (filename.len(), true) + }; + let mut new_name: String = if let Some(ref rename_regex) = config.rename_regex { + rename_regex.replace_all(&filename[..idx], "").to_string() + } else { + filename[..idx].to_string() + }; + if append_2 { + new_name.push_str(":2,"); + } + if !(flags & Flag::DRAFT).is_empty() { + new_name.push('D'); + } + if !(flags & Flag::FLAGGED).is_empty() { + new_name.push('F'); + } + if !(flags & Flag::PASSED).is_empty() { + new_name.push('P'); + } + if !(flags & Flag::REPLIED).is_empty() { + new_name.push('R'); + } + if !(flags & Flag::SEEN).is_empty() { + new_name.push('S'); + } + if !(flags & Flag::TRASHED).is_empty() { + new_name.push('T'); + } + let mut new_path: PathBuf = self.into(); + new_path.set_file_name(new_name); + Ok(new_path) + } + + fn place_in_dir(&self, dest_dir: &Path, config: &Configuration) -> Result { + let mut filename = self + .file_name() + .ok_or_else(|| format!("Could not get filename of `{}`", self.display()))? + .to_string_lossy(); + if !filename.contains(":2,") { + filename = Cow::Owned(format!("{}:2,", filename)); + } + let mut new_path = dest_dir.to_path_buf(); + if let Some(ref rename_regex) = config.rename_regex { + new_path.push(rename_regex.replace_all(&filename, "").as_ref()); + } else { + new_path.push(filename.as_ref()); + }; + Ok(new_path) + } + fn to_mailbox_hash(&self) -> MailboxHash { let mut path = self.to_path_buf(); if path.is_file() { diff --git a/melib/src/maildir/stream.rs b/melib/src/maildir/stream.rs index 0e6517cb..de789c06 100644 --- a/melib/src/maildir/stream.rs +++ b/melib/src/maildir/stream.rs @@ -50,11 +50,12 @@ impl MaildirStream { mut path: PathBuf, map: HashIndexes, mailbox_index: Arc>>, + config: Arc, ) -> ResultStream> { let chunk_size = 2048; path.push("new"); for p in path.read_dir()?.flatten() { - move_to_cur(&p.path()).ok().take(); + move_to_cur(&config, &p.path()).ok().take(); } path.pop(); path.push("cur"); diff --git a/melib/src/maildir/tests.rs b/melib/src/maildir/tests.rs new file mode 100644 index 00000000..2e0f2f29 --- /dev/null +++ b/melib/src/maildir/tests.rs @@ -0,0 +1,231 @@ +// +// melib +// +// Copyright 2024 Emmanouil Pitsidianakis +// +// This file is part of melib. +// +// melib is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// melib is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with melib. If not, see . +// +// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later + +use std::path::{Path, PathBuf}; + +use regex::Regex; + +use crate::{ + backends::FlagOp, + email::Flag, + error::Result, + maildir::{move_to_cur, Configuration, MaildirPathTrait}, +}; + +fn set_flags(config: &Configuration, path: &Path, flag_ops: &[FlagOp]) -> Result { + let mut new_flags = path.flags(); + for op in flag_ops.iter() { + if let FlagOp::Set(f) | FlagOp::UnSet(f) = op { + new_flags.set(*f, op.as_bool()); + } + } + + path.set_flags(new_flags, config) +} + +#[test] +fn test_maildir_move_to_cur_rename() { + let config = Configuration { rename_regex: None }; + assert_eq!( + move_to_cur(&config, Path::new("/path/to/new/1423819205.29514_1:2,FRS")).unwrap(), + Path::new("/path/to/cur/1423819205.29514_1:2,FRS") + ); + assert_eq!( + move_to_cur(&config, Path::new("/path/to/new/1423819205.29514_1:2,")).unwrap(), + Path::new("/path/to/cur/1423819205.29514_1:2,") + ); + assert_eq!( + move_to_cur(&config, Path::new("/path/to/new/1423819205.29514_1:1,")).unwrap(), + Path::new("/path/to/cur/1423819205.29514_1:1,:2,") + ); + assert_eq!( + move_to_cur(&config, Path::new("/path/to/new/1423819205.29514_1")).unwrap(), + Path::new("/path/to/cur/1423819205.29514_1:2,") + ); +} + +#[test] +fn test_maildir_move_to_cur_rename_regexp() { + let config = Configuration { + rename_regex: Some(Regex::new(r",U=\d\d*").unwrap()), + }; + assert_eq!( + move_to_cur( + &config, + Path::new("/path/to/new/1423819205.29514_1.foo,U=123:2,S") + ) + .unwrap(), + Path::new("/path/to/cur/1423819205.29514_1.foo:2,S") + ); + assert_eq!( + move_to_cur( + &config, + Path::new("/path/to/new/1423819205.29514_1.foo,U=1:2,S") + ) + .unwrap(), + Path::new("/path/to/cur/1423819205.29514_1.foo:2,S") + ); + assert_eq!( + move_to_cur( + &config, + Path::new("/path/to/new/1423819205.29514_1.foo,U=:2,S") + ) + .unwrap(), + Path::new("/path/to/cur/1423819205.29514_1.foo,U=:2,S") + ); + assert_eq!( + move_to_cur( + &config, + Path::new("/path/to/new/1423819205.29514_1.foo:2,S") + ) + .unwrap(), + Path::new("/path/to/cur/1423819205.29514_1.foo:2,S") + ); +} + +#[test] +fn test_maildir_set_flags() { + let config = Configuration { rename_regex: None }; + + assert_eq!( + set_flags( + &config, + Path::new("/path/to/new/1423819205.29514_1:2,FRS"), + &[FlagOp::Set(Flag::FLAGGED | Flag::SEEN | Flag::REPLIED)] + ), + Ok(Path::new("/path/to/new/1423819205.29514_1:2,FRS").to_path_buf()), + "Setting the same flags should not change the path" + ); + assert_eq!( + set_flags( + &config, + Path::new("/path/to/new/1423819205.29514_1:2,FRS"), + &[FlagOp::UnSet(Flag::FLAGGED | Flag::SEEN | Flag::REPLIED)] + ), + Ok(Path::new("/path/to/new/1423819205.29514_1:2,").to_path_buf()), + "UnSetting all the set flags should change the path" + ); + assert_eq!( + set_flags( + &config, + Path::new("/path/to/new/1423819205.29514_1:2,FRS"), + &[FlagOp::Set(Flag::FLAGGED | Flag::TRASHED)] + ), + Ok(Path::new("/path/to/new/1423819205.29514_1:2,FRST").to_path_buf()), + "Setting new flags should change the path to include them" + ); +} + +#[test] +fn test_maildir_set_flags_regexp() { + let config = Configuration { + rename_regex: Some(Regex::new(r",U=\d\d*").unwrap()), + }; + + assert_eq!( + set_flags( + &config, + Path::new("/path/to/new/1423819205.29514_1.foo,U=123:2,S"), + &[FlagOp::Set(Flag::FLAGGED | Flag::SEEN | Flag::REPLIED)] + ), + Ok(Path::new("/path/to/new/1423819205.29514_1.foo:2,FRS").to_path_buf()), + "Setting the same flags should not change the path" + ); + assert_eq!( + set_flags( + &config, + Path::new("/path/to/new/1423819205.29514_1.foo,U=123:2,FRS"), + &[FlagOp::UnSet(Flag::FLAGGED | Flag::SEEN | Flag::REPLIED)] + ), + Ok(Path::new("/path/to/new/1423819205.29514_1.foo:2,").to_path_buf()), + "UnSetting all the set flags should change the path" + ); + assert_eq!( + set_flags( + &config, + Path::new("/path/to/new/1423819205.29514_1.foo,U=123:2,FRS"), + &[FlagOp::Set(Flag::FLAGGED | Flag::TRASHED)] + ), + Ok(Path::new("/path/to/new/1423819205.29514_1.foo:2,FRST").to_path_buf()), + "Setting new flags should change the path to include them" + ); +} + +#[test] +fn test_maildir_place_in_dir() { + let config = Configuration { rename_regex: None }; + + assert_eq!( + Path::new("/path/to/new/1423819205.29514_1:2,") + .place_in_dir(Path::new("/path/to/new/"), &config), + Ok(Path::new("/path/to/new/1423819205.29514_1:2,").to_path_buf()), + "place_in_dir() where dest_dir is the same should not change the parent dir", + ); + assert_eq!( + Path::new("/path/to/new/1423819205.29514_1:2,") + .place_in_dir(Path::new("/path/to2/new/"), &config), + Ok(Path::new("/path/to2/new/1423819205.29514_1:2,").to_path_buf()), + ); + assert_eq!( + Path::new("/path/to/new/1423819205.29514_1:2,FRS") + .place_in_dir(Path::new("/path/to2/new/"), &config), + Ok(Path::new("/path/to2/new/1423819205.29514_1:2,FRS").to_path_buf()), + "place_in_dir() where dest_dir is the same should not change flags", + ); + assert_eq!( + Path::new("/path/to/new/1423819205.29514_1") + .place_in_dir(Path::new("/path/to2/new/"), &config), + Ok(Path::new("/path/to2/new/1423819205.29514_1:2,").to_path_buf()), + "place_in_dir() should add missing `:2,` substring" + ); +} + +#[test] +fn test_maildir_place_in_dir_regexp() { + let config = Configuration { + rename_regex: Some(Regex::new(r",U=\d\d*").unwrap()), + }; + + assert_eq!( + Path::new("/path/to/new/1423819205.29514_1.foo,U=123:2,") + .place_in_dir(Path::new("/path/to/new/"), &config), + Ok(Path::new("/path/to/new/1423819205.29514_1.foo:2,").to_path_buf()), + "place_in_dir() where dest_dir is the same should not change the parent dir", + ); + assert_eq!( + Path::new("/path/to/new/1423819205.29514_1.foo,U=123:2,") + .place_in_dir(Path::new("/path/to2/new/"), &config), + Ok(Path::new("/path/to2/new/1423819205.29514_1.foo:2,").to_path_buf()), + ); + assert_eq!( + Path::new("/path/to/new/1423819205.29514_1.foo,U=123:2,FRS") + .place_in_dir(Path::new("/path/to2/new/"), &config), + Ok(Path::new("/path/to2/new/1423819205.29514_1.foo:2,FRS").to_path_buf()), + "place_in_dir() where dest_dir is the same should not change flags", + ); + assert_eq!( + Path::new("/path/to/new/1423819205.29514_1.foo,U=123") + .place_in_dir(Path::new("/path/to2/new/"), &config), + Ok(Path::new("/path/to2/new/1423819205.29514_1.foo:2,").to_path_buf()), + "place_in_dir() should add missing `:2,` substring" + ); +} diff --git a/melib/src/maildir/watch.rs b/melib/src/maildir/watch.rs index 0d604530..d04d29e7 100644 --- a/melib/src/maildir/watch.rs +++ b/melib/src/maildir/watch.rs @@ -33,7 +33,7 @@ use notify::{self, event::EventKind as NotifyEvent}; use crate::{ backends::{prelude::*, RefreshEventKind::*}, error, - maildir::{move_to_cur, HashIndexes, MaildirPathTrait, PathMod}, + maildir::{move_to_cur, Configuration, HashIndexes, MaildirPathTrait, PathMod}, }; pub struct MaildirWatch { @@ -45,6 +45,7 @@ pub struct MaildirWatch { pub hash_indexes: HashIndexes, pub mailbox_index: Arc>>, pub root_mailbox_hash: MailboxHash, + pub config: Arc, #[allow(clippy::type_complexity)] pub mailbox_counts: HashMap>, Arc>)>, } @@ -61,6 +62,7 @@ impl MaildirWatch { mailbox_index, root_mailbox_hash, mailbox_counts, + config, } = self; let mut buf = Vec::with_capacity(4096); @@ -72,7 +74,7 @@ impl MaildirWatch { for mut pathbuf in event.paths { if pathbuf.is_in_new() { // This creates a Rename event that we will receive later - pathbuf = match move_to_cur(&pathbuf) { + pathbuf = match move_to_cur(&config, &pathbuf) { Ok(p) => p, Err(err) => { log::error!( diff --git a/melib/tests/integration/configs.rs b/melib/tests/integration/configs.rs new file mode 100644 index 00000000..24bf6ce2 --- /dev/null +++ b/melib/tests/integration/configs.rs @@ -0,0 +1,115 @@ +// +// melib +// +// Copyright 2024 Emmanouil Pitsidianakis +// +// This file is part of melib. +// +// melib is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// melib is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with melib. If not, see . +// +// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later + +#[test] +fn test_maildir_config() { + use melib::maildir::Configuration; + use regex::Regex; + use tempfile::TempDir; + + let tmp_dir = TempDir::new().unwrap(); + + let config = Configuration { + rename_regex: Some(Regex::new(r",U=\d\d*").unwrap()), + }; + + let mut s: melib::AccountSettings = toml::from_str(&format!( + r#" +name = "foo" +root_mailbox = "{}" +format = "maildir" +identity = "foo@example.com" +subscribed_mailboxes = [] + "#, + tmp_dir.path().display() + )) + .unwrap(); + + melib::maildir::MaildirType::validate_config(&mut s).unwrap(); + let mut s: melib::AccountSettings = toml::from_str(&format!( + r#" +name = "foo" +root_mailbox = "{}" +format = "maildir" +identity = "foo@example.com" +subscribed_mailboxes = [] +rename_regex = ',U=\d\d*' + "#, + tmp_dir.path().display() + )) + .unwrap(); + assert_eq!( + melib::maildir::Configuration::new(&s) + .unwrap() + .rename_regex + .unwrap() + .as_str(), + config.rename_regex.as_ref().unwrap().as_str() + ); + + melib::maildir::MaildirType::validate_config(&mut s).unwrap(); + let mut s: melib::AccountSettings = toml::from_str(&format!( + r#" +name = "foo" +root_mailbox = "{}" +format = "maildir" +identity = "foo@example.com" +subscribed_mailboxes = [] +rename_regex = ",U=\\d\\d*" + "#, + tmp_dir.path().display() + )) + .unwrap(); + assert_eq!( + melib::maildir::Configuration::new(&s) + .unwrap() + .rename_regex + .unwrap() + .as_str(), + config.rename_regex.as_ref().unwrap().as_str() + ); + + melib::maildir::MaildirType::validate_config(&mut s).unwrap(); + let mut s: melib::AccountSettings = toml::from_str(&format!( + r#" +name = "foo" +root_mailbox = "{}" +format = "maildir" +identity = "foo@example.com" +subscribed_mailboxes = [] +rename_regex = ',U=\d\d*' + "#, + tmp_dir.path().display() + )) + .unwrap(); + + assert_eq!( + melib::maildir::Configuration::new(&s) + .unwrap() + .rename_regex + .unwrap() + .as_str(), + config.rename_regex.as_ref().unwrap().as_str() + ); + melib::maildir::MaildirType::validate_config(&mut s).unwrap(); + _ = tmp_dir.close(); +} diff --git a/melib/tests/integration/main.rs b/melib/tests/integration/main.rs index 8dbc538d..9c12b9db 100644 --- a/melib/tests/integration/main.rs +++ b/melib/tests/integration/main.rs @@ -19,6 +19,7 @@ // along with meli. If not, see . // +mod configs; mod generating_email; mod mbox_parse;