meli/melib/src/backends/notmuch.rs

730 lines
25 KiB
Rust

/*
* meli - notmuch backend
*
* Copyright 2019 - 2020 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli 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.
*
* meli 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 meli. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::async_workers::{Async, AsyncBuilder, AsyncStatus, WorkContext};
use crate::backends::MailboxHash;
use crate::backends::{
BackendMailbox, BackendOp, MailBackend, Mailbox, MailboxPermissions, RefreshEventConsumer,
SpecialUsageMailbox,
};
use crate::conf::AccountSettings;
use crate::email::{Envelope, EnvelopeHash, Flag};
use crate::error::{MeliError, Result};
use crate::shellexpand::ShellExpandTrait;
use fnv::FnvHashMap;
use smallvec::SmallVec;
use std::collections::hash_map::DefaultHasher;
use std::collections::BTreeMap;
use std::ffi::{CStr, CString};
use std::hash::{Hash, Hasher};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, RwLock};
pub mod bindings;
use bindings::*;
#[derive(Debug, Clone)]
struct DbWrapper {
inner: Arc<RwLock<*mut notmuch_database_t>>,
database_ph: std::marker::PhantomData<&'static mut notmuch_database_t>,
}
unsafe impl Send for DbWrapper {}
unsafe impl Sync for DbWrapper {}
#[derive(Debug)]
pub struct NotmuchDb {
database: DbWrapper,
lib: Arc<libloading::Library>,
mailboxes: Arc<RwLock<FnvHashMap<MailboxHash, NotmuchMailbox>>>,
index: Arc<RwLock<FnvHashMap<EnvelopeHash, &'static CStr>>>,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
path: PathBuf,
save_messages_to: Option<PathBuf>,
}
unsafe impl Send for NotmuchDb {}
unsafe impl Sync for NotmuchDb {}
macro_rules! call {
($lib:expr, $func:ty) => {{
let func: libloading::Symbol<$func> = $lib.get(stringify!($func).as_bytes()).unwrap();
func
}};
}
impl Drop for NotmuchDb {
fn drop(&mut self) {
for f in self.mailboxes.write().unwrap().values_mut() {
if let Some(query) = f.query.take() {
unsafe {
call!(self.lib, notmuch_query_destroy)(query);
}
}
}
let inner = self.database.inner.write().unwrap();
unsafe {
call!(self.lib, notmuch_database_close)(*inner);
call!(self.lib, notmuch_database_destroy)(*inner);
}
}
}
#[derive(Debug, Clone, Default)]
struct NotmuchMailbox {
hash: MailboxHash,
children: Vec<MailboxHash>,
parent: Option<MailboxHash>,
name: String,
path: String,
query_str: String,
query: Option<*mut notmuch_query_t>,
phantom: std::marker::PhantomData<&'static mut notmuch_query_t>,
usage: Arc<RwLock<SpecialUsageMailbox>>,
total: Arc<Mutex<usize>>,
unseen: Arc<Mutex<usize>>,
}
impl BackendMailbox for NotmuchMailbox {
fn hash(&self) -> MailboxHash {
self.hash
}
fn name(&self) -> &str {
self.name.as_str()
}
fn path(&self) -> &str {
self.path.as_str()
}
fn change_name(&mut self, _s: &str) {}
fn clone(&self) -> Mailbox {
Box::new(std::clone::Clone::clone(self))
}
fn children(&self) -> &[MailboxHash] {
&self.children
}
fn parent(&self) -> Option<MailboxHash> {
self.parent
}
fn special_usage(&self) -> SpecialUsageMailbox {
*self.usage.read().unwrap()
}
fn permissions(&self) -> MailboxPermissions {
MailboxPermissions::default()
}
fn is_subscribed(&self) -> bool {
true
}
fn set_is_subscribed(&mut self, _new_val: bool) -> Result<()> {
Ok(())
}
fn set_special_usage(&mut self, new_val: SpecialUsageMailbox) -> Result<()> {
*self.usage.write()? = new_val;
Ok(())
}
fn count(&self) -> Result<(usize, usize)> {
Ok((*self.unseen.lock()?, *self.total.lock()?))
}
}
unsafe impl Send for NotmuchMailbox {}
unsafe impl Sync for NotmuchMailbox {}
impl NotmuchDb {
pub fn new(
s: &AccountSettings,
_is_subscribed: Box<dyn Fn(&str) -> bool>,
) -> Result<Box<dyn MailBackend>> {
let lib = Arc::new(libloading::Library::new("libnotmuch.so.5")?);
let mut database: *mut notmuch_database_t = std::ptr::null_mut();
let path = Path::new(s.root_mailbox.as_str()).expand().to_path_buf();
if !path.exists() {
return Err(MeliError::new(format!(
"\"root_mailbox\" {} for account {} is not a valid path.",
s.root_mailbox.as_str(),
s.name()
)));
}
let path_c = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let path_ptr = path_c.as_ptr();
let status = unsafe {
call!(lib, notmuch_database_open)(
path_ptr,
notmuch_database_mode_t_NOTMUCH_DATABASE_MODE_READ_WRITE,
&mut database as *mut _,
)
};
if status != 0 {
return Err(MeliError::new(format!(
"Could not open notmuch database at path {}. notmuch_database_open returned {}.",
s.root_mailbox.as_str(),
status
)));
}
assert!(!database.is_null());
let mut mailboxes = FnvHashMap::default();
for (k, f) in s.mailboxes.iter() {
if let Some(query_str) = f.extra.get("query") {
let hash = {
let mut h = DefaultHasher::new();
k.hash(&mut h);
h.finish()
};
mailboxes.insert(
hash,
NotmuchMailbox {
hash,
name: k.to_string(),
path: k.to_string(),
children: vec![],
parent: None,
query: None,
query_str: query_str.to_string(),
usage: Arc::new(RwLock::new(SpecialUsageMailbox::Normal)),
phantom: std::marker::PhantomData,
total: Arc::new(Mutex::new(0)),
unseen: Arc::new(Mutex::new(0)),
},
);
} else {
return Err(MeliError::new(format!(
"notmuch mailbox configuration entry \"{}\" should have a \"query\" value set.",
k
)));
}
}
Ok(Box::new(NotmuchDb {
lib,
database: DbWrapper {
inner: Arc::new(RwLock::new(database)),
database_ph: std::marker::PhantomData,
},
path,
index: Arc::new(RwLock::new(Default::default())),
tag_index: Arc::new(RwLock::new(Default::default())),
mailboxes: Arc::new(RwLock::new(mailboxes)),
save_messages_to: None,
}))
}
pub fn validate_config(s: &AccountSettings) -> Result<()> {
let path = Path::new(s.root_mailbox.as_str()).expand().to_path_buf();
if !path.exists() {
return Err(MeliError::new(format!(
"\"root_mailbox\" {} for account {} is not a valid path.",
s.root_mailbox.as_str(),
s.name()
)));
}
for (k, f) in s.mailboxes.iter() {
if f.extra.get("query").is_none() {
return Err(MeliError::new(format!(
"notmuch mailbox configuration entry \"{}\" should have a \"query\" value set.",
k
)));
}
}
Ok(())
}
pub fn search(&self, query_s: &str) -> Result<SmallVec<[EnvelopeHash; 512]>> {
let database_lck = self.database.inner.read().unwrap();
let query_str = std::ffi::CString::new(query_s).unwrap();
let query: *mut notmuch_query_t =
unsafe { call!(self.lib, notmuch_query_create)(*database_lck, query_str.as_ptr()) };
if query.is_null() {
return Err(MeliError::new("Could not create query. Out of memory?"));
}
let mut messages: *mut notmuch_messages_t = std::ptr::null_mut();
let status = unsafe {
call!(self.lib, notmuch_query_search_messages)(query, &mut messages as *mut _)
};
if status != 0 {
return Err(MeliError::new(format!(
"Search for {} returned {}",
query_s, status,
)));
}
assert!(!messages.is_null());
let iter = MessageIterator {
messages,
lib: self.lib.clone(),
};
let mut ret = SmallVec::new();
for message in iter {
let fs_path = unsafe { call!(self.lib, notmuch_message_get_filename)(message) };
let c_str = unsafe { CStr::from_ptr(fs_path) };
let env_hash = {
let mut hasher = DefaultHasher::default();
c_str.hash(&mut hasher);
hasher.finish()
};
ret.push(env_hash);
}
Ok(ret)
}
}
impl MailBackend for NotmuchDb {
fn is_online(&self) -> Result<()> {
Ok(())
}
fn get(&mut self, mailbox: &Mailbox) -> Async<Result<Vec<Envelope>>> {
let mut w = AsyncBuilder::new();
let mailbox_hash = mailbox.hash();
let database = self.database.clone();
let index = self.index.clone();
let tag_index = self.tag_index.clone();
let mailboxes = self.mailboxes.clone();
let lib = self.lib.clone();
let handle = {
let tx = w.tx();
let closure = move |_work_context| {
let mut ret: Vec<Envelope> = Vec::new();
let database_lck = database.inner.read().unwrap();
let mut mailboxes_lck = mailboxes.write().unwrap();
let mailbox = mailboxes_lck.get_mut(&mailbox_hash).unwrap();
let query_str = std::ffi::CString::new(mailbox.query_str.as_str()).unwrap();
let query: *mut notmuch_query_t =
unsafe { call!(lib, notmuch_query_create)(*database_lck, query_str.as_ptr()) };
if query.is_null() {
tx.send(AsyncStatus::Payload(Err(MeliError::new(
"Could not create query. Out of memory?",
))))
.unwrap();
tx.send(AsyncStatus::Finished).unwrap();
return;
}
let mut messages: *mut notmuch_messages_t = std::ptr::null_mut();
let status = unsafe {
call!(lib, notmuch_query_search_messages)(query, &mut messages as *mut _)
};
if status != 0 {
tx.send(AsyncStatus::Payload(Err(MeliError::new(format!(
"Search for {} returned {}",
mailbox.query_str.as_str(),
status,
)))))
.unwrap();
tx.send(AsyncStatus::Finished).unwrap();
return;
}
assert!(!messages.is_null());
let iter = MessageIterator {
messages,
lib: lib.clone(),
};
for message in iter {
let mut response = String::new();
let fs_path = unsafe { call!(lib, notmuch_message_get_filename)(message) };
let mut f = match std::fs::File::open(unsafe {
CStr::from_ptr(fs_path)
.to_string_lossy()
.into_owned()
.as_str()
}) {
Ok(f) => f,
Err(e) => {
debug!("could not open fs_path {:?} {}", fs_path, e);
continue;
}
};
response.clear();
if let Err(e) = f.read_to_string(&mut response) {
debug!("could not read fs_path {:?} {}", fs_path, e);
continue;
}
let c_str = unsafe { CStr::from_ptr(fs_path) };
let env_hash = {
let mut hasher = DefaultHasher::default();
c_str.hash(&mut hasher);
hasher.finish()
};
index.write().unwrap().insert(env_hash, c_str);
let op = Box::new(NotmuchOp {
database: database.clone(),
lib: lib.clone(),
hash: env_hash,
index: index.clone(),
bytes: Some(response),
tag_index: tag_index.clone(),
});
if let Some(mut env) = Envelope::from_token(op, env_hash) {
let mut tag_lock = tag_index.write().unwrap();
for tag in (TagIterator {
tags: unsafe { call!(lib, notmuch_message_get_tags)(message) },
lib: lib.clone(),
}) {
let tag = tag.to_string_lossy().into_owned();
let mut hasher = DefaultHasher::new();
hasher.write(tag.as_bytes());
let num = hasher.finish();
if !tag_lock.contains_key(&num) {
tag_lock.insert(num, tag);
}
env.labels_mut().push(num);
}
ret.push(env);
} else {
debug!("could not parse path {:?}", c_str);
index.write().unwrap().remove(&env_hash);
}
}
mailbox.query = Some(query);
tx.send(AsyncStatus::Payload(Ok(ret))).unwrap();
tx.send(AsyncStatus::Finished).unwrap();
};
Box::new(closure)
};
w.build(handle)
}
fn watch(
&self,
_sender: RefreshEventConsumer,
_work_context: WorkContext,
) -> Result<std::thread::ThreadId> {
let handle = std::thread::Builder::new()
.name(format!(
"watching {}",
self.path.file_name().unwrap().to_str().unwrap()
))
.spawn(move || {})?;
Ok(handle.thread().id())
}
fn mailboxes(&self) -> Result<FnvHashMap<MailboxHash, Mailbox>> {
Ok(self
.mailboxes
.read()
.unwrap()
.iter()
.map(|(k, f)| (*k, BackendMailbox::clone(f)))
.collect())
}
fn operation(&self, hash: EnvelopeHash) -> Box<dyn BackendOp> {
Box::new(NotmuchOp {
database: self.database.clone(),
lib: self.lib.clone(),
hash,
index: self.index.clone(),
bytes: None,
tag_index: self.tag_index.clone(),
})
}
fn save(&self, bytes: &[u8], _mailbox: &str, flags: Option<Flag>) -> Result<()> {
let mut path = self
.save_messages_to
.as_ref()
.unwrap_or(&self.path)
.to_path_buf();
if !(path.ends_with("cur") || path.ends_with("new") || path.ends_with("tmp")) {
for d in &["cur", "new", "tmp"] {
path.push(d);
if !path.is_dir() {
return Err(MeliError::new(format!(
"{} is not a valid maildir mailbox",
path.display()
)));
}
path.pop();
}
path.push("cur");
}
crate::backends::MaildirType::save_to_mailbox(path, bytes, flags)
}
fn as_any(&self) -> &dyn::std::any::Any {
self
}
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
Some(self.tag_index.clone())
}
}
#[derive(Debug)]
struct NotmuchOp {
hash: EnvelopeHash,
index: Arc<RwLock<FnvHashMap<EnvelopeHash, &'static CStr>>>,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
database: DbWrapper,
bytes: Option<String>,
lib: Arc<libloading::Library>,
}
impl BackendOp for NotmuchOp {
fn description(&self) -> String {
String::new()
}
fn as_bytes(&mut self) -> Result<&[u8]> {
let path = &self.index.read().unwrap()[&self.hash];
let mut f = std::fs::File::open(path.to_str().unwrap())?;
let mut response = String::new();
f.read_to_string(&mut response)?;
self.bytes = Some(response);
Ok(self.bytes.as_ref().unwrap().as_bytes())
}
fn fetch_flags(&self) -> Flag {
let mut flag = Flag::default();
let path = self.index.read().unwrap()[&self.hash].to_str().unwrap();
if !path.contains(":2,") {
return flag;
}
for f in path.chars().rev() {
match f {
',' => break,
'D' => flag |= Flag::DRAFT,
'F' => flag |= Flag::FLAGGED,
'P' => flag |= Flag::PASSED,
'R' => flag |= Flag::REPLIED,
'S' => flag |= Flag::SEEN,
'T' => flag |= Flag::TRASHED,
_ => {
debug!("DEBUG: in fetch_flags, path is {}", path);
}
}
}
flag
}
fn set_flag(&mut self, _envelope: &mut Envelope, f: Flag, value: bool) -> Result<()> {
let mut message: *mut notmuch_message_t = std::ptr::null_mut();
let mut index_lck = self.index.write().unwrap();
unsafe {
call!(self.lib, notmuch_database_find_message_by_filename)(
*self.database.inner.read().unwrap(),
index_lck[&self.hash].as_ptr(),
&mut message as *mut _,
)
};
if message.is_null() {
return Err(MeliError::new(format!(
"Error, message with path {:?} not found in notmuch database.",
index_lck[&self.hash]
)));
}
let tags = (TagIterator {
tags: unsafe { call!(self.lib, notmuch_message_get_tags)(message) },
lib: self.lib.clone(),
})
.collect::<Vec<&CStr>>();
debug!(&tags);
macro_rules! cstr {
($l:literal) => {
CStr::from_bytes_with_nul_unchecked($l)
};
}
macro_rules! add_tag {
($l:literal) => {
unsafe {
if tags.contains(&cstr!($l)) {
return Ok(());
}
if call!(self.lib, notmuch_message_add_tag)(message, cstr!($l).as_ptr())
!= _notmuch_status_NOTMUCH_STATUS_SUCCESS
{
return Err(MeliError::new("Could not set tag."));
}
}
};
}
macro_rules! remove_tag {
($l:literal) => {
unsafe {
if !tags.contains(&cstr!($l)) {
return Ok(());
}
if call!(self.lib, notmuch_message_remove_tag)(message, cstr!($l).as_ptr())
!= _notmuch_status_NOTMUCH_STATUS_SUCCESS
{
return Err(MeliError::new("Could not set tag."));
}
}
};
}
match f {
Flag::DRAFT if value => add_tag!(b"draft\0"),
Flag::DRAFT => remove_tag!(b"draft\0"),
Flag::FLAGGED if value => add_tag!(b"flagged\0"),
Flag::FLAGGED => remove_tag!(b"flagged\0"),
Flag::PASSED if value => add_tag!(b"passed\0"),
Flag::PASSED => remove_tag!(b"passed\0"),
Flag::REPLIED if value => add_tag!(b"replied\0"),
Flag::REPLIED => remove_tag!(b"replied\0"),
Flag::SEEN if value => remove_tag!(b"unread\0"),
Flag::SEEN => add_tag!(b"unread\0"),
Flag::TRASHED if value => add_tag!(b"trashed\0"),
Flag::TRASHED => remove_tag!(b"trashed\0"),
_ => debug!("flags is {:?} value = {}", f, value),
}
/* Update message filesystem path. */
if unsafe { call!(self.lib, notmuch_message_tags_to_maildir_flags)(message) }
!= _notmuch_status_NOTMUCH_STATUS_SUCCESS
{
return Err(MeliError::new("Could not set tag."));
}
let fs_path = unsafe { call!(self.lib, notmuch_message_get_filename)(message) };
let c_str = unsafe { CStr::from_ptr(fs_path) };
if let Some(p) = index_lck.get_mut(&self.hash) {
*p = c_str;
}
let new_hash = {
let mut hasher = DefaultHasher::default();
c_str.hash(&mut hasher);
hasher.finish()
};
index_lck.insert(new_hash, c_str);
Ok(())
}
fn set_tag(&mut self, envelope: &mut Envelope, tag: String, value: bool) -> Result<()> {
let mut message: *mut notmuch_message_t = std::ptr::null_mut();
let index_lck = self.index.read().unwrap();
unsafe {
call!(self.lib, notmuch_database_find_message_by_filename)(
*self.database.inner.read().unwrap(),
index_lck[&self.hash].as_ptr(),
&mut message as *mut _,
)
};
if message.is_null() {
return Err(MeliError::new(format!(
"Error, message with path {:?} not found in notmuch database.",
index_lck[&self.hash]
)));
}
if value {
if unsafe {
call!(self.lib, notmuch_message_add_tag)(
message,
CString::new(tag.as_str()).unwrap().as_ptr(),
)
} != _notmuch_status_NOTMUCH_STATUS_SUCCESS
{
return Err(MeliError::new("Could not set tag."));
}
debug!("added tag {}", &tag);
} else {
if unsafe {
call!(self.lib, notmuch_message_remove_tag)(
message,
CString::new(tag.as_str()).unwrap().as_ptr(),
)
} != _notmuch_status_NOTMUCH_STATUS_SUCCESS
{
return Err(MeliError::new("Could not set tag."));
}
debug!("removed tag {}", &tag);
}
let hash = tag_hash!(tag);
if value {
self.tag_index.write().unwrap().insert(hash, tag);
} else {
self.tag_index.write().unwrap().remove(&hash);
}
if !envelope.labels().iter().any(|&h_| h_ == hash) {
if value {
envelope.labels_mut().push(hash);
}
}
if !value {
if let Some(pos) = envelope.labels().iter().position(|&h_| h_ == hash) {
envelope.labels_mut().remove(pos);
}
}
Ok(())
}
}
pub struct MessageIterator {
lib: Arc<libloading::Library>,
messages: *mut notmuch_messages_t,
}
impl Iterator for MessageIterator {
type Item = *mut notmuch_message_t;
fn next(&mut self) -> Option<Self::Item> {
if self.messages.is_null() {
None
} else if unsafe { call!(self.lib, notmuch_messages_valid)(self.messages) } == 1 {
let ret = Some(unsafe { call!(self.lib, notmuch_messages_get)(self.messages) });
unsafe {
call!(self.lib, notmuch_messages_move_to_next)(self.messages);
}
ret
} else {
self.messages = std::ptr::null_mut();
None
}
}
}
pub struct TagIterator {
lib: Arc<libloading::Library>,
tags: *mut notmuch_tags_t,
}
impl Iterator for TagIterator {
type Item = &'static CStr;
fn next(&mut self) -> Option<Self::Item> {
if self.tags.is_null() {
None
} else if unsafe { call!(self.lib, notmuch_tags_valid)(self.tags) } == 1 {
let ret = Some(unsafe { CStr::from_ptr(call!(self.lib, notmuch_tags_get)(self.tags)) });
unsafe {
call!(self.lib, notmuch_tags_move_to_next)(self.tags);
}
ret
} else {
self.tags = std::ptr::null_mut();
None
}
}
}