init notmuch backend e2e tests

This commit is contained in:
Clément DOUIN 2022-03-01 18:17:44 +01:00
parent e544536e01
commit 886b66a017
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
5 changed files with 231 additions and 83 deletions

View file

@ -9,7 +9,7 @@ use std::{convert::TryInto, fs, path::PathBuf};
use crate::{ use crate::{
backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes}, backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes},
config::{AccountConfig, MaildirBackendConfig, DEFAULT_INBOX_FOLDER}, config::{AccountConfig, MaildirBackendConfig},
mbox::Mboxes, mbox::Mboxes,
msg::{Envelopes, Msg}, msg::{Envelopes, Msg},
}; };
@ -40,17 +40,10 @@ impl<'a> MaildirBackend<'a> {
} }
/// Creates a maildir instance from a string slice. /// Creates a maildir instance from a string slice.
fn get_mdir_from_dir(&self, dir: &str) -> Result<maildir::Maildir> { pub fn get_mdir_from_dir(&self, dir: &str) -> Result<maildir::Maildir> {
let inbox_folder = self
.account_config
.mailboxes
.get("inbox")
.map(|s| s.as_str())
.unwrap_or(DEFAULT_INBOX_FOLDER);
// If the dir points to the inbox folder, creates a maildir // If the dir points to the inbox folder, creates a maildir
// instance from the root folder. // instance from the root folder.
if dir == inbox_folder { if dir == "inbox" {
self.validate_mdir_path(self.mdir.path().to_owned()) self.validate_mdir_path(self.mdir.path().to_owned())
.map(maildir::Maildir::from) .map(maildir::Maildir::from)
} else { } else {

View file

@ -4,17 +4,17 @@ use anyhow::{anyhow, Context, Result};
use log::{debug, info, trace}; use log::{debug, info, trace};
use crate::{ use crate::{
backends::{Backend, IdMapper, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes}, backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes},
config::{AccountConfig, NotmuchBackendConfig}, config::{AccountConfig, NotmuchBackendConfig},
mbox::Mboxes, mbox::Mboxes,
msg::{Envelopes, Msg}, msg::{Envelopes, Msg},
}; };
/// Represents the Notmuch backend. /// Represents the Notmuch backend.
#[derive(Debug)]
pub struct NotmuchBackend<'a> { pub struct NotmuchBackend<'a> {
account_config: &'a AccountConfig, account_config: &'a AccountConfig,
notmuch_config: &'a NotmuchBackendConfig, notmuch_config: &'a NotmuchBackendConfig,
pub mdir: &'a mut MaildirBackend<'a>,
db: notmuch::Database, db: notmuch::Database,
} }
@ -22,12 +22,14 @@ impl<'a> NotmuchBackend<'a> {
pub fn new( pub fn new(
account_config: &'a AccountConfig, account_config: &'a AccountConfig,
notmuch_config: &'a NotmuchBackendConfig, notmuch_config: &'a NotmuchBackendConfig,
) -> Result<Self> { mdir: &'a mut MaildirBackend<'a>,
) -> Result<NotmuchBackend<'a>> {
info!(">> create new notmuch backend"); info!(">> create new notmuch backend");
let backend = Self { let backend = Self {
account_config, account_config,
notmuch_config, notmuch_config,
mdir,
db: notmuch::Database::open( db: notmuch::Database::open(
notmuch_config.notmuch_database_dir.clone(), notmuch_config.notmuch_database_dir.clone(),
notmuch::DatabaseMode::ReadWrite, notmuch::DatabaseMode::ReadWrite,
@ -39,7 +41,6 @@ impl<'a> NotmuchBackend<'a> {
) )
})?, })?,
}; };
trace!("backend: {:?}", backend);
info!("<< create new notmuch backend"); info!("<< create new notmuch backend");
Ok(backend) Ok(backend)
@ -68,10 +69,10 @@ impl<'a> NotmuchBackend<'a> {
let page_begin = page * page_size; let page_begin = page * page_size;
debug!("page begin: {:?}", page_begin); debug!("page begin: {:?}", page_begin);
if page_begin > envelopes.len() { if page_begin > envelopes.len() {
return Err(anyhow!(format!( return Err(anyhow!(
"cannot get notmuch envelopes at page {:?} (out of bounds)", "cannot get notmuch envelopes at page {:?} (out of bounds)",
page_begin + 1, page_begin + 1,
))); ));
} }
let page_end = envelopes.len().min(page_begin + page_size); let page_end = envelopes.len().min(page_begin + page_size);
debug!("page end: {:?}", page_end); debug!("page end: {:?}", page_end);
@ -192,17 +193,75 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
Ok(envelopes) Ok(envelopes)
} }
fn add_msg( fn add_msg(&mut self, dir: &str, msg: &[u8], tags: &str) -> Result<Box<dyn ToString>> {
&mut self,
_virt_mbox: &str,
_msg: &[u8],
_flags: &str,
) -> Result<Box<dyn ToString>> {
info!(">> add notmuch envelopes"); 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"); info!("<< add notmuch envelopes");
Err(anyhow!( Ok(Box::new(hash))
"cannot add notmuch envelopes: feature not implemented"
))
} }
fn get_msg(&mut self, _virt_mbox: &str, short_hash: &str) -> Result<Msg> { fn get_msg(&mut self, _virt_mbox: &str, short_hash: &str) -> Result<Msg> {
@ -288,34 +347,35 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
Ok(()) 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"); info!(">> add notmuch message flags");
debug!("tags: {:?}", tags); debug!("tags: {:?}", tags);
debug!("query: {:?}", query);
let query = self let dir = &self.notmuch_config.notmuch_database_dir;
.account_config let id = IdMapper::new(dir)
.mailboxes .with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
.get(virt_mbox) .find(short_hash)
.map(|s| s.as_str()) .with_context(|| {
.unwrap_or(query); format!(
debug!("final query: {:?}", query); "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 tags: Vec<_> = tags.split_whitespace().collect();
let query_builder = self let query_builder = self
.db .db
.create_query(query) .create_query(&query)
.with_context(|| format!("cannot create notmuch query from {:?}", query))?; .with_context(|| format!("cannot create notmuch query from {:?}", query))?;
let envelopes = query_builder let msgs = query_builder
.search_messages() .search_messages()
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
for envelope in envelopes { for msg in msgs {
for tag in tags.iter() { for tag in tags.iter() {
envelope.add_tag(*tag).with_context(|| { msg.add_tag(*tag).with_context(|| {
format!( format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id())
"cannot add tag {:?} to notmuch message {:?}",
tag,
envelope.id()
)
})? })?
} }
} }
@ -324,40 +384,38 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
Ok(()) 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"); info!(">> set notmuch message flags");
debug!("tags: {:?}", tags); debug!("tags: {:?}", tags);
debug!("query: {:?}", query);
let query = self let dir = &self.notmuch_config.notmuch_database_dir;
.account_config let id = IdMapper::new(dir)
.mailboxes .with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
.get(virt_mbox) .find(short_hash)
.map(|s| s.as_str()) .with_context(|| {
.unwrap_or(query); format!(
debug!("final query: {:?}", query); "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 tags: Vec<_> = tags.split_whitespace().collect();
let query_builder = self let query_builder = self
.db .db
.create_query(query) .create_query(&query)
.with_context(|| format!("cannot create notmuch query from {:?}", query))?; .with_context(|| format!("cannot create notmuch query from {:?}", query))?;
let envelopes = query_builder let msgs = query_builder
.search_messages() .search_messages()
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
for envelope in envelopes { for msg in msgs {
envelope.remove_all_tags().with_context(|| { msg.remove_all_tags().with_context(|| {
format!( format!("cannot remove all tags from notmuch message {:?}", msg.id())
"cannot remove all tags from notmuch message {:?}",
envelope.id()
)
})?; })?;
for tag in tags.iter() { for tag in tags.iter() {
envelope.add_tag(*tag).with_context(|| { msg.add_tag(*tag).with_context(|| {
format!( format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id())
"cannot add tag {:?} to notmuch message {:?}",
tag,
envelope.id()
)
})? })?
} }
} }
@ -366,33 +424,38 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> {
Ok(()) 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"); info!(">> delete notmuch message flags");
debug!("tags: {:?}", tags); debug!("tags: {:?}", tags);
debug!("query: {:?}", query);
let query = self let dir = &self.notmuch_config.notmuch_database_dir;
.account_config let id = IdMapper::new(dir)
.mailboxes .with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
.get(virt_mbox) .find(short_hash)
.map(|s| s.as_str()) .with_context(|| {
.unwrap_or(query); format!(
debug!("final query: {:?}", query); "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 tags: Vec<_> = tags.split_whitespace().collect();
let query_builder = self let query_builder = self
.db .db
.create_query(query) .create_query(&query)
.with_context(|| format!("cannot create notmuch query from {:?}", query))?; .with_context(|| format!("cannot create notmuch query from {:?}", query))?;
let envelopes = query_builder let msgs = query_builder
.search_messages() .search_messages()
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?; .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
for envelope in envelopes { for msg in msgs {
for tag in tags.iter() { for tag in tags.iter() {
envelope.remove_tag(*tag).with_context(|| { msg.remove_tag(*tag).with_context(|| {
format!( format!(
"cannot delete tag {:?} from notmuch message {:?}", "cannot delete tag {:?} from notmuch message {:?}",
tag, tag,
envelope.id() msg.id()
) )
})? })?
} }

View file

@ -7,7 +7,7 @@ use himalaya::{
compl::{compl_arg, compl_handler}, compl::{compl_arg, compl_handler},
config::{ config::{
account_args, config_args, AccountConfig, BackendConfig, DeserializedConfig, account_args, config_args, AccountConfig, BackendConfig, DeserializedConfig,
DEFAULT_INBOX_FOLDER, MaildirBackendConfig, DEFAULT_INBOX_FOLDER,
}, },
mbox::{mbox_arg, mbox_handler}, mbox::{mbox_arg, mbox_handler},
msg::{flag_arg, flag_handler, msg_arg, msg_handler, tpl_arg, tpl_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 imap;
let mut maildir; let mut maildir;
let maildir_config;
#[cfg(feature = "notmuch")] #[cfg(feature = "notmuch")]
let mut notmuch; let mut notmuch;
let backend: Box<&mut dyn Backend> = match backend_config { let backend: Box<&mut dyn Backend> = match backend_config {
@ -64,7 +65,11 @@ fn main() -> Result<()> {
} }
#[cfg(feature = "notmuch")] #[cfg(feature = "notmuch")]
BackendConfig::Notmuch(ref notmuch_config) => { 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) Box::new(&mut notmuch)
} }
}; };
@ -95,6 +100,7 @@ fn main() -> Result<()> {
let mut printer = StdoutPrinter::try_from(m.value_of("output"))?; let mut printer = StdoutPrinter::try_from(m.value_of("output"))?;
let mut imap; let mut imap;
let mut maildir; let mut maildir;
let maildir_config;
#[cfg(feature = "notmuch")] #[cfg(feature = "notmuch")]
let mut notmuch; let mut notmuch;
let backend: Box<&mut dyn Backend> = match backend_config { let backend: Box<&mut dyn Backend> = match backend_config {
@ -108,7 +114,11 @@ fn main() -> Result<()> {
} }
#[cfg(feature = "notmuch")] #[cfg(feature = "notmuch")]
BackendConfig::Notmuch(ref notmuch_config) => { 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) Box::new(&mut notmuch)
} }
}; };

View file

@ -2,5 +2,6 @@ From: alice@localhost
To: patrick@localhost To: patrick@localhost
Subject: Plain message Subject: Plain message
Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8
Date: Tue, 1 Mar 2022 12:00:00 +0000
Ceci est un message. Ceci est un message.

View file

@ -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, &notmuch_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()));
}