melib: add notmuch backend
Missing: - Watching for updates functionality - Using tags - Searchjmap
parent
7463248da8
commit
77936e0cd5
2
Makefile
2
Makefile
|
@ -1,7 +1,7 @@
|
||||||
.POSIX:
|
.POSIX:
|
||||||
.SUFFIXES:
|
.SUFFIXES:
|
||||||
meli:
|
meli:
|
||||||
cargo build --release
|
cargo build --features="$(MELI_FEATURES)" --release
|
||||||
|
|
||||||
PREFIX=/usr/local
|
PREFIX=/usr/local
|
||||||
|
|
||||||
|
|
11
README
11
README
|
@ -54,6 +54,17 @@ BUILDING IN DEBIAN
|
||||||
Building with Debian's packaged cargo might require the installation of these
|
Building with Debian's packaged cargo might require the installation of these
|
||||||
two packages: librust-openssl-sys-dev and librust-libdbus-sys-dev
|
two packages: librust-openssl-sys-dev and librust-libdbus-sys-dev
|
||||||
|
|
||||||
|
BUILDING WITH NOTMUCH
|
||||||
|
=====================
|
||||||
|
|
||||||
|
To use the optional notmuch backend feature, you must have libnotmuch installed in your system. In Debian-like systems, install the "libnotmuch" package.
|
||||||
|
|
||||||
|
To build with notmuch support, prepend the environment variable "MELI_FEATURES='melib/notmuch_backend'" to your make invocation:
|
||||||
|
|
||||||
|
# MELI_FEATURES="melib/notmuch_backend" make
|
||||||
|
|
||||||
|
or if building directly with cargo, use the flag '--features="melib/notmuch_backend"'.
|
||||||
|
|
||||||
DEVELOPMENT
|
DEVELOPMENT
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
|
15
meli.1
15
meli.1
|
@ -139,8 +139,21 @@ To open a draft for editing later, select your draft in the mail listing and pre
|
||||||
.Cm e Ns
|
.Cm e Ns
|
||||||
\&.
|
\&.
|
||||||
.Sh SEARCH
|
.Sh SEARCH
|
||||||
|
Each e-mail storage backend has its default search method.
|
||||||
|
.Em IMAP
|
||||||
|
uses the SEARCH command,
|
||||||
|
.Em notmuch
|
||||||
|
uses libnotmuch and
|
||||||
|
.Em Maildir/mbox
|
||||||
|
have to perform a very slow and I/O bound linear search. Thus it is advised to use a cache on
|
||||||
|
.Em Maildir/mbox
|
||||||
|
accounts.
|
||||||
.Nm Ns
|
.Nm Ns
|
||||||
, if built with sqlite3, includes the ability to perform full text search on the following fields: From, To, Cc, Bcc, In-Reply-To, References, Subject and Date. The message body (in plain text human readable form) and the flags can also be queried. To create the sqlite3 index issue command
|
, if built with sqlite3, includes the ability to perform full text search on the following fields: From, To, Cc, Bcc, In-Reply-To, References, Subject and Date. The message body (in plain text human readable form) and the flags can also be queried. To enable sqlite3 indexing for an account set
|
||||||
|
.Em cache_type
|
||||||
|
to
|
||||||
|
.Em sqlite3
|
||||||
|
in the configuration file and to create the sqlite3 index issue command
|
||||||
.Ic index Ar ACCOUNT_NAME Ns \&.
|
.Ic index Ar ACCOUNT_NAME Ns \&.
|
||||||
|
|
||||||
To search in the message body type your keywords without any special formatting.
|
To search in the message body type your keywords without any special formatting.
|
||||||
|
|
26
meli.conf.5
26
meli.conf.5
|
@ -93,10 +93,10 @@ theme = "light"
|
||||||
available options are listed below.
|
available options are listed below.
|
||||||
.Sy default values are shown in parentheses.
|
.Sy default values are shown in parentheses.
|
||||||
.Sh ACCOUNTS
|
.Sh ACCOUNTS
|
||||||
.Bl -tag -width "danger_accept_invalid_certs boolean" -offset -indent
|
.Bl -tag -width "format String [maildir mbox imap notmuch]" -offset -indent
|
||||||
.It Cm root_folder Ar String
|
.It Cm root_folder Ar String
|
||||||
the backend-specific path of the root_folder, usually INBOX
|
the backend-specific path of the root_folder, usually INBOX.
|
||||||
.It Cm format Ar String Op maildir mbox imap
|
.It Cm format Ar String Op maildir mbox imap notmuch
|
||||||
the format of the mail backend.
|
the format of the mail backend.
|
||||||
.It Cm subscribed_folders Ar [String,]
|
.It Cm subscribed_folders Ar [String,]
|
||||||
an array of folder paths to display in the UI. Paths are relative to the root folder (eg "INBOX/Sent", not "Sent")
|
an array of folder paths to display in the UI. Paths are relative to the root folder (eg "INBOX/Sent", not "Sent")
|
||||||
|
@ -129,6 +129,26 @@ choose which cache backend to use. Available options are 'none' and 'sqlite3'
|
||||||
\&.
|
\&.
|
||||||
.El
|
.El
|
||||||
.Pp
|
.Pp
|
||||||
|
.Sh notmuch only
|
||||||
|
.Cm root_folder
|
||||||
|
points to the directory which contains the
|
||||||
|
.Pa .notmuch/
|
||||||
|
subdirectory. notmuch folders are virtual, since they are defined by user-given notmuch queries. Thus you have to explicitly state the folders you want in the
|
||||||
|
.Cm folders
|
||||||
|
field and set the
|
||||||
|
.Ar query
|
||||||
|
property to each of them. Example:
|
||||||
|
.Pp
|
||||||
|
.Bd -literal
|
||||||
|
[accounts.notmuch]
|
||||||
|
format = "notmuch"
|
||||||
|
\&...
|
||||||
|
[accounts.notmuch.folders]
|
||||||
|
"INBOX" = { query="tag:inbox", subscribe = true }
|
||||||
|
"Drafts" = { query="tag:draft", subscribe = true }
|
||||||
|
"Sent" = { query="from:username@server.tld from:username2@server.tld", subscribe = true }
|
||||||
|
.Ed
|
||||||
|
.Sh IMAP only
|
||||||
IMAP specific options are:
|
IMAP specific options are:
|
||||||
.Bl -tag -width "danger_accept_invalid_certs boolean" -offset -indent
|
.Bl -tag -width "danger_accept_invalid_certs boolean" -offset -indent
|
||||||
.It Cm server_hostname Ar String
|
.It Cm server_hostname Ar String
|
||||||
|
|
|
@ -4,6 +4,7 @@ version = "0.3.2"
|
||||||
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
|
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
|
||||||
workspace = ".."
|
workspace = ".."
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bitflags = "1.0"
|
bitflags = "1.0"
|
||||||
|
@ -34,4 +35,5 @@ unicode_algorithms = ["text_processing"]
|
||||||
imap_backend = ["native-tls"]
|
imap_backend = ["native-tls"]
|
||||||
maildir_backend = ["notify", "notify-rust", "memmap"]
|
maildir_backend = ["notify", "notify-rust", "memmap"]
|
||||||
mbox_backend = ["notify", "notify-rust", "memmap"]
|
mbox_backend = ["notify", "notify-rust", "memmap"]
|
||||||
|
notmuch_backend = []
|
||||||
vcard = []
|
vcard = []
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
fn main() {
|
||||||
|
#[cfg(feature = "notmuch_backend")]
|
||||||
|
{
|
||||||
|
println!("cargo:rustc-link-lib=notmuch");
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,10 @@ pub mod imap;
|
||||||
pub mod maildir;
|
pub mod maildir;
|
||||||
#[cfg(feature = "mbox_backend")]
|
#[cfg(feature = "mbox_backend")]
|
||||||
pub mod mbox;
|
pub mod mbox;
|
||||||
|
#[cfg(feature = "notmuch_backend")]
|
||||||
|
pub mod notmuch;
|
||||||
|
#[cfg(feature = "notmuch_backend")]
|
||||||
|
pub use self::notmuch::NotmuchDb;
|
||||||
|
|
||||||
#[cfg(feature = "imap_backend")]
|
#[cfg(feature = "imap_backend")]
|
||||||
pub use self::imap::ImapType;
|
pub use self::imap::ImapType;
|
||||||
|
@ -85,6 +89,13 @@ impl Backends {
|
||||||
Box::new(|| Box::new(|f, i| Box::new(ImapType::new(f, i)))),
|
Box::new(|| Box::new(|f, i| Box::new(ImapType::new(f, i)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "notmuch_backend")]
|
||||||
|
{
|
||||||
|
b.register(
|
||||||
|
"notmuch".to_string(),
|
||||||
|
Box::new(|| Box::new(|f, i| Box::new(NotmuchDb::new(f, i)))),
|
||||||
|
);
|
||||||
|
}
|
||||||
b
|
b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -493,57 +493,7 @@ impl MailBackend for MaildirType {
|
||||||
fn save(&self, bytes: &[u8], folder: &str, flags: Option<Flag>) -> Result<()> {
|
fn save(&self, bytes: &[u8], folder: &str, flags: Option<Flag>) -> Result<()> {
|
||||||
for f in self.folders.values() {
|
for f in self.folders.values() {
|
||||||
if f.name == folder || f.path.to_str().unwrap() == folder {
|
if f.name == folder || f.path.to_str().unwrap() == folder {
|
||||||
let mut path = f.fs_path.clone();
|
return MaildirType::save_to_folder(f.fs_path.clone(), bytes, flags);
|
||||||
path.push("cur");
|
|
||||||
{
|
|
||||||
let mut rand_buf = [0u8; 16];
|
|
||||||
let mut f = fs::File::open("/dev/urandom")
|
|
||||||
.expect("Could not open /dev/urandom for reading");
|
|
||||||
f.read_exact(&mut rand_buf)
|
|
||||||
.expect("Could not read from /dev/urandom/");
|
|
||||||
let mut hostn_buf = String::with_capacity(256);
|
|
||||||
let mut f = fs::File::open("/etc/hostname")
|
|
||||||
.expect("Could not open /etc/hostname for reading");
|
|
||||||
f.read_to_string(&mut hostn_buf)
|
|
||||||
.expect("Could not read from /etc/hostname");
|
|
||||||
let timestamp = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_millis();
|
|
||||||
let mut filename = format!(
|
|
||||||
"{}.{:x}_{}.{}:2,",
|
|
||||||
timestamp,
|
|
||||||
u128::from_be_bytes(rand_buf),
|
|
||||||
std::process::id(),
|
|
||||||
hostn_buf.trim()
|
|
||||||
);
|
|
||||||
if let Some(flags) = flags {
|
|
||||||
if !(flags & Flag::DRAFT).is_empty() {
|
|
||||||
filename.push('D');
|
|
||||||
}
|
|
||||||
if !(flags & Flag::FLAGGED).is_empty() {
|
|
||||||
filename.push('F');
|
|
||||||
}
|
|
||||||
if !(flags & Flag::PASSED).is_empty() {
|
|
||||||
filename.push('P');
|
|
||||||
}
|
|
||||||
if !(flags & Flag::REPLIED).is_empty() {
|
|
||||||
filename.push('R');
|
|
||||||
}
|
|
||||||
if !(flags & Flag::SEEN).is_empty() {
|
|
||||||
filename.push('S');
|
|
||||||
}
|
|
||||||
if !(flags & Flag::TRASHED).is_empty() {
|
|
||||||
filename.push('T');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
path.push(filename);
|
|
||||||
}
|
|
||||||
debug!("saving at {}", path.display());
|
|
||||||
let file = fs::File::create(path).unwrap();
|
|
||||||
let mut writer = io::BufWriter::new(file);
|
|
||||||
writer.write_all(bytes).unwrap();
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -863,6 +813,59 @@ impl MaildirType {
|
||||||
};
|
};
|
||||||
w.build(handle)
|
w.build(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save_to_folder(mut path: PathBuf, bytes: &[u8], flags: Option<Flag>) -> Result<()> {
|
||||||
|
path.push("cur");
|
||||||
|
{
|
||||||
|
let mut rand_buf = [0u8; 16];
|
||||||
|
let mut f =
|
||||||
|
fs::File::open("/dev/urandom").expect("Could not open /dev/urandom for reading");
|
||||||
|
f.read_exact(&mut rand_buf)
|
||||||
|
.expect("Could not read from /dev/urandom/");
|
||||||
|
let mut hostn_buf = String::with_capacity(256);
|
||||||
|
let mut f =
|
||||||
|
fs::File::open("/etc/hostname").expect("Could not open /etc/hostname for reading");
|
||||||
|
f.read_to_string(&mut hostn_buf)
|
||||||
|
.expect("Could not read from /etc/hostname");
|
||||||
|
let timestamp = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis();
|
||||||
|
let mut filename = format!(
|
||||||
|
"{}.{:x}_{}.{}:2,",
|
||||||
|
timestamp,
|
||||||
|
u128::from_be_bytes(rand_buf),
|
||||||
|
std::process::id(),
|
||||||
|
hostn_buf.trim()
|
||||||
|
);
|
||||||
|
if let Some(flags) = flags {
|
||||||
|
if !(flags & Flag::DRAFT).is_empty() {
|
||||||
|
filename.push('D');
|
||||||
|
}
|
||||||
|
if !(flags & Flag::FLAGGED).is_empty() {
|
||||||
|
filename.push('F');
|
||||||
|
}
|
||||||
|
if !(flags & Flag::PASSED).is_empty() {
|
||||||
|
filename.push('P');
|
||||||
|
}
|
||||||
|
if !(flags & Flag::REPLIED).is_empty() {
|
||||||
|
filename.push('R');
|
||||||
|
}
|
||||||
|
if !(flags & Flag::SEEN).is_empty() {
|
||||||
|
filename.push('S');
|
||||||
|
}
|
||||||
|
if !(flags & Flag::TRASHED).is_empty() {
|
||||||
|
filename.push('T');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path.push(filename);
|
||||||
|
}
|
||||||
|
debug!("saving at {}", path.display());
|
||||||
|
let file = fs::File::create(path).unwrap();
|
||||||
|
let mut writer = io::BufWriter::new(file);
|
||||||
|
writer.write_all(bytes).unwrap();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_path_to_index(
|
fn add_path_to_index(
|
||||||
|
|
|
@ -0,0 +1,510 @@
|
||||||
|
use crate::async_workers::{Async, AsyncBuilder, AsyncStatus, WorkContext};
|
||||||
|
use crate::backends::FolderHash;
|
||||||
|
use crate::backends::{
|
||||||
|
BackendFolder, BackendOp, Folder, FolderPermissions, MailBackend, RefreshEventConsumer,
|
||||||
|
};
|
||||||
|
use crate::conf::AccountSettings;
|
||||||
|
use crate::email::{Envelope, EnvelopeHash, Flag};
|
||||||
|
use crate::error::{MeliError, Result};
|
||||||
|
use crate::shellexpand::ShellExpandTrait;
|
||||||
|
use fnv::FnvHashMap;
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, 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,
|
||||||
|
folders: Arc<RwLock<FnvHashMap<FolderHash, NotmuchFolder>>>,
|
||||||
|
index: Arc<RwLock<FnvHashMap<EnvelopeHash, &'static CStr>>>,
|
||||||
|
path: PathBuf,
|
||||||
|
save_messages_to: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for NotmuchDb {}
|
||||||
|
unsafe impl Sync for NotmuchDb {}
|
||||||
|
|
||||||
|
impl Drop for NotmuchDb {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
for f in self.folders.write().unwrap().values_mut() {
|
||||||
|
if let Some(query) = f.query.take() {
|
||||||
|
unsafe {
|
||||||
|
notmuch_query_destroy(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let inner = self.database.inner.write().unwrap();
|
||||||
|
unsafe {
|
||||||
|
notmuch_database_close(*inner);
|
||||||
|
notmuch_database_destroy(*inner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct NotmuchFolder {
|
||||||
|
hash: FolderHash,
|
||||||
|
children: Vec<FolderHash>,
|
||||||
|
parent: Option<FolderHash>,
|
||||||
|
name: String,
|
||||||
|
path: String,
|
||||||
|
query_str: String,
|
||||||
|
query: Option<*mut notmuch_query_t>,
|
||||||
|
phantom: std::marker::PhantomData<&'static mut notmuch_query_t>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackendFolder for NotmuchFolder {
|
||||||
|
fn hash(&self) -> FolderHash {
|
||||||
|
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) -> Folder {
|
||||||
|
Box::new(std::clone::Clone::clone(self))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> &[FolderHash] {
|
||||||
|
&self.children
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parent(&self) -> Option<FolderHash> {
|
||||||
|
self.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
fn permissions(&self) -> FolderPermissions {
|
||||||
|
FolderPermissions::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for NotmuchFolder {}
|
||||||
|
unsafe impl Sync for NotmuchFolder {}
|
||||||
|
|
||||||
|
impl NotmuchDb {
|
||||||
|
pub fn new(s: &AccountSettings, _is_subscribed: Box<dyn Fn(&str) -> bool>) -> Self {
|
||||||
|
let mut database: *mut notmuch_database_t = std::ptr::null_mut();
|
||||||
|
let path = Path::new(s.root_folder.as_str()).expand().to_path_buf();
|
||||||
|
if !path.exists() {
|
||||||
|
panic!(
|
||||||
|
"\"root_folder\" {} for account {} is not a valid path.",
|
||||||
|
s.root_folder.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 {
|
||||||
|
bindings::notmuch_database_open(
|
||||||
|
path_ptr,
|
||||||
|
notmuch_database_mode_t_NOTMUCH_DATABASE_MODE_READ_WRITE,
|
||||||
|
&mut database as *mut _,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if status != 0 {
|
||||||
|
panic!("notmuch_database_open returned {}.", status);
|
||||||
|
}
|
||||||
|
assert!(!database.is_null());
|
||||||
|
let mut folders = FnvHashMap::default();
|
||||||
|
for (k, f) in s.folders.iter() {
|
||||||
|
if let Some(query_str) = f.extra.get("query") {
|
||||||
|
let hash = {
|
||||||
|
let mut h = DefaultHasher::new();
|
||||||
|
k.hash(&mut h);
|
||||||
|
h.finish()
|
||||||
|
};
|
||||||
|
folders.insert(
|
||||||
|
hash,
|
||||||
|
NotmuchFolder {
|
||||||
|
hash,
|
||||||
|
name: k.to_string(),
|
||||||
|
path: k.to_string(),
|
||||||
|
children: vec![],
|
||||||
|
parent: None,
|
||||||
|
query: None,
|
||||||
|
query_str: query_str.to_string(),
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"notmuch folder configuration entry \"{}\" should have a \"query\" value set.",
|
||||||
|
k
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NotmuchDb {
|
||||||
|
database: DbWrapper {
|
||||||
|
inner: Arc::new(RwLock::new(database)),
|
||||||
|
database_ph: std::marker::PhantomData,
|
||||||
|
},
|
||||||
|
path,
|
||||||
|
index: Arc::new(RwLock::new(Default::default())),
|
||||||
|
folders: Arc::new(RwLock::new(folders)),
|
||||||
|
save_messages_to: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MailBackend for NotmuchDb {
|
||||||
|
fn is_online(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn get(&mut self, folder: &Folder) -> Async<Result<Vec<Envelope>>> {
|
||||||
|
let mut w = AsyncBuilder::new();
|
||||||
|
let folder_hash = folder.hash();
|
||||||
|
let database = self.database.clone();
|
||||||
|
let index = self.index.clone();
|
||||||
|
let folders = self.folders.clone();
|
||||||
|
let handle = {
|
||||||
|
let tx = w.tx();
|
||||||
|
let closure = move |_work_context| {
|
||||||
|
let mut ret: Vec<Envelope> = Vec::new();
|
||||||
|
let database = database.clone();
|
||||||
|
let database_lck = database.inner.read().unwrap();
|
||||||
|
let folders = folders.clone();
|
||||||
|
let tx = tx.clone();
|
||||||
|
let mut folders_lck = folders.write().unwrap();
|
||||||
|
let folder = folders_lck.get_mut(&folder_hash).unwrap();
|
||||||
|
let query_str = std::ffi::CString::new(folder.query_str.as_str()).unwrap();
|
||||||
|
let query: *mut notmuch_query_t =
|
||||||
|
unsafe { notmuch_query_create(*database_lck, query_str.as_ptr()) };
|
||||||
|
if query.is_null() {
|
||||||
|
panic!("Out of memory.");
|
||||||
|
}
|
||||||
|
let mut messages: *mut notmuch_messages_t = std::ptr::null_mut();
|
||||||
|
let status =
|
||||||
|
unsafe { notmuch_query_search_messages(query, &mut messages as *mut _) };
|
||||||
|
if status != 0 {
|
||||||
|
panic!(status);
|
||||||
|
}
|
||||||
|
assert!(!messages.is_null());
|
||||||
|
let iter = MessageIterator { messages };
|
||||||
|
for message in iter {
|
||||||
|
let mut response = String::new();
|
||||||
|
let fs_path = unsafe { 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(),
|
||||||
|
hash: env_hash,
|
||||||
|
index: index.clone(),
|
||||||
|
bytes: Some(response),
|
||||||
|
});
|
||||||
|
if let Some(env) = Envelope::from_token(op, env_hash) {
|
||||||
|
ret.push(env);
|
||||||
|
} else {
|
||||||
|
debug!("could not parse path {:?}", c_str);
|
||||||
|
index.write().unwrap().remove(&env_hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
folder.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 folders(&self) -> FnvHashMap<FolderHash, Folder> {
|
||||||
|
self.folders
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|(k, f)| (*k, BackendFolder::clone(f)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
fn operation(&self, hash: EnvelopeHash) -> Box<dyn BackendOp> {
|
||||||
|
Box::new(NotmuchOp {
|
||||||
|
database: self.database.clone(),
|
||||||
|
hash,
|
||||||
|
index: self.index.clone(),
|
||||||
|
bytes: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self, bytes: &[u8], _folder: &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 folder",
|
||||||
|
path.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
path.push("cur");
|
||||||
|
}
|
||||||
|
crate::backends::MaildirType::save_to_folder(path, bytes, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn::std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct NotmuchOp {
|
||||||
|
hash: EnvelopeHash,
|
||||||
|
index: Arc<RwLock<FnvHashMap<EnvelopeHash, &'static CStr>>>,
|
||||||
|
database: DbWrapper,
|
||||||
|
bytes: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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_headers(&mut self) -> Result<&[u8]> {
|
||||||
|
let raw = self.as_bytes()?;
|
||||||
|
let result = crate::email::parser::headers_raw(raw).to_full_result()?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_body(&mut self) -> Result<&[u8]> {
|
||||||
|
let raw = self.as_bytes()?;
|
||||||
|
let result = crate::email::parser::body_raw(raw).to_full_result()?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 { notmuch_message_get_tags(message) },
|
||||||
|
})
|
||||||
|
.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 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 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 { notmuch_message_tags_to_maildir_flags(message) }
|
||||||
|
!= _notmuch_status_NOTMUCH_STATUS_SUCCESS
|
||||||
|
{
|
||||||
|
return Err(MeliError::new("Could not set tag."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let fs_path = unsafe { 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MessageIterator {
|
||||||
|
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 { notmuch_messages_valid(self.messages) } == 1 {
|
||||||
|
let ret = Some(unsafe { notmuch_messages_get(self.messages) });
|
||||||
|
unsafe {
|
||||||
|
notmuch_messages_move_to_next(self.messages);
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
} else {
|
||||||
|
self.messages = std::ptr::null_mut();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TagIterator {
|
||||||
|
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 { notmuch_tags_valid(self.tags) } == 1 {
|
||||||
|
let ret = Some(unsafe { CStr::from_ptr(notmuch_tags_get(self.tags)) });
|
||||||
|
unsafe {
|
||||||
|
notmuch_tags_move_to_next(self.tags);
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
} else {
|
||||||
|
self.tags = std::ptr::null_mut();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -39,6 +39,19 @@
|
||||||
#display_name = "Name Name"
|
#display_name = "Name Name"
|
||||||
#subscribed_folders = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
|
#subscribed_folders = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
|
||||||
|
|
||||||
|
# Setting up an account for an already existing notmuch database
|
||||||
|
#[accounts.notmuch]
|
||||||
|
#root_folder = "/path/to/folder" # where .notmuch/ directory is located
|
||||||
|
#format = "notmuch"
|
||||||
|
#index_style = "conversations"
|
||||||
|
#identity="username@server.tld"
|
||||||
|
#display_name = "Name Name"
|
||||||
|
# # notmuch folders are virtual, they are defined by their alias and the notmuch query that corresponds to their content.
|
||||||
|
# [accounts.notmuch.folders]
|
||||||
|
# "INBOX" = { query="tag:inbox", subscribe = true }
|
||||||
|
# "Drafts" = { query="tag:draft", subscribe = true }
|
||||||
|
# "Sent" = { query="from:username@server.tld from:username2@server.tld", subscribe = true }
|
||||||
|
#
|
||||||
#[pager]
|
#[pager]
|
||||||
#filter = "/usr/bin/pygmentize"
|
#filter = "/usr/bin/pygmentize"
|
||||||
#pager_context = 0 # default, optional
|
#pager_context = 0 # default, optional
|
||||||
|
|
Loading…
Reference in New Issue