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] 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())); +}