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::{
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<maildir::Maildir> {
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<maildir::Maildir> {
// 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 {

View file

@ -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<Self> {
mdir: &'a mut MaildirBackend<'a>,
) -> Result<NotmuchBackend<'a>> {
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<Box<dyn ToString>> {
fn add_msg(&mut self, dir: &str, msg: &[u8], tags: &str) -> Result<Box<dyn ToString>> {
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<Msg> {
@ -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()
)
})?
}

View file

@ -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)
}
};

View file

@ -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.

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