🐝 I really like where this mua is(was?) headed, but it seems as though there has not been much activity recently.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

729 lines
25 KiB

/*
* 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
}
}
}