mirror of https://git.meli.delivery/meli/meli
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
417 lines
15 KiB
Rust
417 lines
15 KiB
Rust
/*
|
|
* melib - notmuch backend
|
|
*
|
|
* Copyright 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 std::{cell::Cell, marker::PhantomData, ptr::NonNull};
|
|
|
|
use super::*;
|
|
use crate::{
|
|
notmuch::ffi::{
|
|
notmuch_database_find_message, notmuch_message_add_tag, notmuch_message_destroy,
|
|
notmuch_message_freeze, notmuch_message_get_date, notmuch_message_get_filename,
|
|
notmuch_message_get_header, notmuch_message_get_message_id, notmuch_message_get_replies,
|
|
notmuch_message_remove_tag, notmuch_message_tags_to_maildir_flags, notmuch_message_thaw,
|
|
notmuch_messages_get, notmuch_messages_move_to_next, notmuch_messages_valid,
|
|
NOTMUCH_STATUS_READ_ONLY_DATABASE, NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
|
|
},
|
|
thread::{ThreadHash, ThreadNode, ThreadNodeHash},
|
|
};
|
|
|
|
#[derive(Clone)]
|
|
pub struct Message<'m> {
|
|
pub lib: Arc<NotmuchLibrary>,
|
|
pub message: NonNull<ffi::notmuch_message_t>,
|
|
pub is_from_thread: bool,
|
|
pub freezes: Cell<u8>,
|
|
pub _ph: PhantomData<&'m ffi::notmuch_message_t>,
|
|
}
|
|
|
|
impl<'m> Message<'m> {
|
|
pub fn find_message(db: &'m DbConnection, msg_id: &CStr) -> Result<Message<'m>> {
|
|
let mut message: *mut ffi::notmuch_message_t = std::ptr::null_mut();
|
|
let lib = db.lib.clone();
|
|
unsafe {
|
|
call!(lib, notmuch_database_find_message)(
|
|
db.inner.lock().unwrap().as_mut(),
|
|
msg_id.as_ptr(),
|
|
std::ptr::addr_of_mut!(message),
|
|
)
|
|
};
|
|
Ok(Message {
|
|
lib,
|
|
message: NonNull::new(message).ok_or_else(|| {
|
|
Error::new(format!(
|
|
"Message with message id {:?} not found in notmuch database.",
|
|
msg_id
|
|
))
|
|
})?,
|
|
is_from_thread: false,
|
|
freezes: 0.into(),
|
|
_ph: PhantomData,
|
|
})
|
|
}
|
|
|
|
pub fn env_hash(&self) -> EnvelopeHash {
|
|
let msg_id =
|
|
unsafe { call!(self.lib, notmuch_message_get_message_id)(self.message.as_ptr()) };
|
|
let c_str = unsafe { CStr::from_ptr(msg_id) };
|
|
EnvelopeHash::from_bytes(c_str.to_bytes_with_nul())
|
|
}
|
|
|
|
pub fn header(&self, header: &CStr) -> Option<&[u8]> {
|
|
let header_val = unsafe {
|
|
call!(self.lib, notmuch_message_get_header)(self.message.as_ptr(), header.as_ptr())
|
|
};
|
|
if header_val.is_null() {
|
|
None
|
|
} else {
|
|
Some(unsafe { CStr::from_ptr(header_val).to_bytes() })
|
|
}
|
|
}
|
|
|
|
pub fn msg_id(&self) -> &[u8] {
|
|
let c_str = self.msg_id_cstr();
|
|
c_str.to_bytes()
|
|
}
|
|
|
|
pub fn msg_id_cstr(&self) -> &CStr {
|
|
let msg_id =
|
|
unsafe { call!(self.lib, notmuch_message_get_message_id)(self.message.as_ptr()) };
|
|
unsafe { CStr::from_ptr(msg_id) }
|
|
}
|
|
|
|
pub fn date(&self) -> crate::UnixTimestamp {
|
|
(unsafe { call!(self.lib, notmuch_message_get_date)(self.message.as_ptr()) }) as u64
|
|
}
|
|
|
|
pub fn into_envelope(
|
|
self,
|
|
index: &RwLock<HashMap<EnvelopeHash, CString>>,
|
|
tag_index: &RwLock<BTreeMap<TagHash, String>>,
|
|
) -> Envelope {
|
|
let env_hash = self.env_hash();
|
|
let mut env = Envelope::new(env_hash);
|
|
index
|
|
.write()
|
|
.unwrap()
|
|
.insert(env_hash, self.msg_id_cstr().into());
|
|
let mut tag_lock = tag_index.write().unwrap();
|
|
let (flags, tags) = TagIterator::new(&self).collect_flags_and_tags();
|
|
for tag in tags {
|
|
let num = TagHash::from_bytes(tag.as_bytes());
|
|
tag_lock.entry(num).or_insert(tag);
|
|
env.tags_mut().insert(num);
|
|
}
|
|
unsafe {
|
|
use crate::email::parser::address::rfc2822address_list;
|
|
env.set_message_id(self.msg_id())
|
|
.set_date(
|
|
self.header(CStr::from_bytes_with_nul_unchecked(b"Date\0"))
|
|
.unwrap_or_default(),
|
|
)
|
|
.set_from(
|
|
rfc2822address_list(
|
|
self.header(CStr::from_bytes_with_nul_unchecked(b"From\0"))
|
|
.unwrap_or_default(),
|
|
)
|
|
.map(|(_, v)| v)
|
|
.unwrap_or_default(),
|
|
)
|
|
.set_to(
|
|
rfc2822address_list(
|
|
self.header(CStr::from_bytes_with_nul_unchecked(b"To\0"))
|
|
.unwrap_or_default(),
|
|
)
|
|
.map(|(_, v)| v)
|
|
.unwrap_or_default(),
|
|
)
|
|
.set_cc(
|
|
rfc2822address_list(
|
|
self.header(CStr::from_bytes_with_nul_unchecked(b"Cc\0"))
|
|
.unwrap_or_default(),
|
|
)
|
|
.map(|(_, v)| v)
|
|
.unwrap_or_default(),
|
|
)
|
|
.set_bcc(
|
|
rfc2822address_list(
|
|
self.header(CStr::from_bytes_with_nul_unchecked(b"Bcc\0"))
|
|
.unwrap_or_default(),
|
|
)
|
|
.map(|(_, v)| v)
|
|
.unwrap_or_default()
|
|
.to_vec(),
|
|
)
|
|
.set_subject(
|
|
self.header(CStr::from_bytes_with_nul_unchecked(b"Subject\0"))
|
|
.unwrap_or_default()
|
|
.to_vec(),
|
|
)
|
|
.set_references(
|
|
self.header(CStr::from_bytes_with_nul_unchecked(b"References\0"))
|
|
.unwrap_or_default(),
|
|
)
|
|
.set_in_reply_to(
|
|
self.header(CStr::from_bytes_with_nul_unchecked(b"In-Reply-To\0"))
|
|
.unwrap_or_default(),
|
|
)
|
|
.set_datetime(self.date())
|
|
.set_flags(flags);
|
|
}
|
|
env
|
|
}
|
|
|
|
pub fn replies_iter(&self) -> Option<MessageIterator> {
|
|
if self.is_from_thread {
|
|
let messages = Some(NonNull::new(unsafe {
|
|
call!(self.lib, notmuch_message_get_replies)(self.message.as_ptr())
|
|
})?);
|
|
Some(MessageIterator {
|
|
lib: self.lib.clone(),
|
|
messages,
|
|
_ph: PhantomData,
|
|
is_from_thread: true,
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn into_thread_node(&self) -> (ThreadNodeHash, ThreadNode) {
|
|
let (flags, _) = TagIterator::new(self).collect_flags_and_tags();
|
|
(
|
|
ThreadNodeHash::from(self.msg_id()),
|
|
ThreadNode {
|
|
message: Some(self.env_hash()),
|
|
parent: None,
|
|
other_mailbox: false,
|
|
children: vec![],
|
|
date: self.date(),
|
|
show_subject: true,
|
|
group: ThreadHash::new(),
|
|
unseen: !flags.intersects(Flag::SEEN),
|
|
},
|
|
)
|
|
}
|
|
|
|
#[doc(alias = "notmuch_message_add_tag")]
|
|
pub fn add_tag(&self, tag: &CStr) -> Result<()> {
|
|
if let Err(err) = unsafe {
|
|
try_call!(
|
|
self.lib,
|
|
call!(self.lib, notmuch_message_add_tag)(self.message.as_ptr(), tag.as_ptr())
|
|
)
|
|
} {
|
|
return Err(Error::new("Could not set tag.").set_source(Some(Arc::new(err))));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[doc(alias = "notmuch_message_remove_tag")]
|
|
pub fn remove_tag(&self, tag: &CStr) -> Result<()> {
|
|
if let Err(err) = unsafe {
|
|
try_call!(
|
|
self.lib,
|
|
call!(self.lib, notmuch_message_remove_tag)(self.message.as_ptr(), tag.as_ptr())
|
|
)
|
|
} {
|
|
return Err(Error::new("Could not set tag.").set_source(Some(Arc::new(err))));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn tags(&'m self) -> TagIterator<'m> {
|
|
TagIterator::new(self)
|
|
}
|
|
|
|
#[doc(alias = "notmuch_message_tags_to_maildir_flags")]
|
|
pub fn tags_to_maildir_flags(&self) -> Result<()> {
|
|
if let Err(err) = unsafe {
|
|
try_call!(
|
|
self.lib,
|
|
call!(self.lib, notmuch_message_tags_to_maildir_flags)(self.message.as_ptr())
|
|
)
|
|
} {
|
|
return Err(Error::new("Could not set flags.").set_source(Some(Arc::new(err))));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Get a filename for the email corresponding to 'message'.
|
|
///
|
|
/// Quoted from `libnotmuch` C header:
|
|
///
|
|
/// > The returned filename is an absolute filename, (the initial
|
|
/// > component will match
|
|
/// > [`notmuch_database_get_path`](crate::notmuch::ffi::notmuch_database_get_path)).
|
|
/// >
|
|
/// > The returned string belongs to the message so should not be
|
|
/// > modified or freed by the caller (nor should it be referenced after
|
|
/// > the message is destroyed).
|
|
/// >
|
|
/// > Note: If this message corresponds to multiple files in the mail
|
|
/// > store, (that is, multiple files contain identical message IDs),
|
|
/// > this function will arbitrarily return a single one of those
|
|
/// > filenames. See
|
|
/// > [`notmuch_message_get_filenames`](crate::notmuch::ffi::notmuch_message_get_filenames)
|
|
/// > for returning the
|
|
/// > complete list of filenames.
|
|
#[doc(alias = "notmuch_message_get_filename")]
|
|
pub fn get_filename(&self) -> &OsStr {
|
|
let fs_path =
|
|
unsafe { call!(self.lib, notmuch_message_get_filename)(self.message.as_ptr()) };
|
|
let c_str = unsafe { CStr::from_ptr(fs_path) };
|
|
OsStr::from_bytes(c_str.to_bytes())
|
|
}
|
|
|
|
/// Freeze the current state of 'message' within the database.
|
|
///
|
|
/// Quoted from `libnotmuch` C header:
|
|
///
|
|
/// > This means that changes to the message state, (via
|
|
/// > [`notmuch_message_add_tag`], [`notmuch_message_remove_tag`](, and
|
|
/// > [`notmuch_message_remove_all_tags`](crate::notmuch::ffi::notmuch_message_remove_all_tags)), will not be committed to the
|
|
/// > database until the message is thawed with
|
|
/// > [`notmuch_message_thaw`].
|
|
/// >
|
|
/// > Multiple calls to freeze/thaw are valid and these calls will
|
|
/// > "stack". That is there must be as many calls to thaw as to freeze
|
|
/// > before a message is actually thawed.
|
|
/// >
|
|
/// > The ability to do freeze/thaw allows for safe transactions to
|
|
/// > change tag values. For example, explicitly setting a message to
|
|
/// > have a given set of tags might look like this:
|
|
///
|
|
/// ```c
|
|
/// notmuch_message_freeze (message);
|
|
///
|
|
/// notmuch_message_remove_all_tags (message);
|
|
///
|
|
/// for (i = 0; i < NUM_TAGS; i++)
|
|
/// notmuch_message_add_tag (message, tags[i]);
|
|
///
|
|
/// notmuch_message_thaw (message);
|
|
/// ```
|
|
///
|
|
/// > With freeze/thaw used like this, the message in the database is
|
|
/// > guaranteed to have either the full set of original tag values, or
|
|
/// > the full set of new tag values, but nothing in between.
|
|
/// >
|
|
/// > Imagine the example above without freeze/thaw and the operation
|
|
/// > somehow getting interrupted. This could result in the message being
|
|
/// > left with no tags if the interruption happened after
|
|
/// > [`notmuch_message_remove_all_tags`](crate::notmuch::ffi::notmuch_message_remove_all_tags) but before [`notmuch_message_add_tag`].
|
|
/// >
|
|
/// > Return value:
|
|
/// >
|
|
/// > - [`NOTMUCH_STATUS_SUCCESS`](crate::notmuch::ffi::NOTMUCH_STATUS_SUCCESS): Message successfully frozen.
|
|
/// > - [`NOTMUCH_STATUS_READ_ONLY_DATABASE`]: Database was opened in
|
|
/// > read-only
|
|
/// > mode so message cannot be modified.
|
|
#[doc(alias = "notmuch_message_freeze")]
|
|
pub fn freeze(&self) {
|
|
if NOTMUCH_STATUS_READ_ONLY_DATABASE
|
|
== unsafe { call!(self.lib, notmuch_message_freeze)(self.message.as_ptr()) }
|
|
{
|
|
return;
|
|
}
|
|
self.freezes.set(self.freezes.get() + 1);
|
|
}
|
|
|
|
/// Thaw the current 'message', synchronizing any changes that may have
|
|
/// occurred while 'message' was frozen into the notmuch database.
|
|
///
|
|
/// Quoted from `libnotmuch` C header:
|
|
///
|
|
/// > See [`notmuch_message_freeze`](Message::freeze) for an example of how
|
|
/// > to use this
|
|
/// > function to safely provide tag changes.
|
|
/// >
|
|
/// > Multiple calls to freeze/thaw are valid and these calls with
|
|
/// > "stack". That is there must be as many calls to thaw as to freeze
|
|
/// > before a message is actually thawed.
|
|
/// >
|
|
/// > Return value:
|
|
/// >
|
|
/// > - [`NOTMUCH_STATUS_SUCCESS`](crate::notmuch::ffi::NOTMUCH_STATUS_SUCCESS): Message successfully thawed, (or at
|
|
/// > least
|
|
/// > its frozen count has successfully been reduced by 1).
|
|
/// > - [`NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW`]: An attempt was made to
|
|
/// > thaw
|
|
/// > an unfrozen message. That is, there have been an unbalanced
|
|
/// > number of calls to
|
|
/// > [`notmuch_message_freeze`]
|
|
/// > and
|
|
/// > [`notmuch_message_thaw`].
|
|
#[doc(alias = "notmuch_message_thaw")]
|
|
pub fn thaw(&self) {
|
|
if self.freezes.get() == 0 {
|
|
return;
|
|
}
|
|
|
|
if NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW
|
|
== unsafe { call!(self.lib, notmuch_message_thaw)(self.message.as_ptr()) }
|
|
{
|
|
return;
|
|
}
|
|
self.freezes.set(self.freezes.get() - 1);
|
|
}
|
|
}
|
|
|
|
impl Drop for Message<'_> {
|
|
fn drop(&mut self) {
|
|
while self.freezes.get() > 0 {
|
|
self.thaw();
|
|
}
|
|
unsafe { call!(self.lib, notmuch_message_destroy)(self.message.as_ptr()) };
|
|
}
|
|
}
|
|
|
|
pub struct MessageIterator<'query> {
|
|
pub lib: Arc<NotmuchLibrary>,
|
|
pub messages: Option<NonNull<ffi::notmuch_messages_t>>,
|
|
pub is_from_thread: bool,
|
|
pub _ph: PhantomData<*const Query<'query>>,
|
|
}
|
|
|
|
impl<'q> Iterator for MessageIterator<'q> {
|
|
type Item = Message<'q>;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
let messages = self.messages?;
|
|
if unsafe { call!(self.lib, notmuch_messages_valid)(messages.as_ptr()) } == 1 {
|
|
let message = unsafe { call!(self.lib, notmuch_messages_get)(messages.as_ptr()) };
|
|
unsafe {
|
|
call!(self.lib, notmuch_messages_move_to_next)(messages.as_ptr());
|
|
}
|
|
Some(Message {
|
|
lib: self.lib.clone(),
|
|
message: NonNull::new(message)?,
|
|
is_from_thread: self.is_from_thread,
|
|
freezes: 0.into(),
|
|
_ph: PhantomData,
|
|
})
|
|
} else {
|
|
self.messages = None;
|
|
None
|
|
}
|
|
}
|
|
}
|