Browse Source

melib: add set_tags command in BackendOp

tags/alpha-0.4.1
Manos Pitsidianakis 2 years ago
parent
commit
0eaf17871a
Signed by: epilys GPG Key ID: 73627C2F690DF710
  1. 14
      melib/src/backends.rs
  2. 14
      melib/src/backends/imap.rs
  3. 54
      melib/src/backends/imap/operations.rs
  4. 19
      melib/src/backends/imap/protocol_parser.rs
  5. 79
      melib/src/backends/imap/watch.rs
  6. 4
      melib/src/backends/maildir.rs
  7. 4
      melib/src/backends/mbox.rs
  8. 57
      melib/src/backends/notmuch.rs
  9. 11
      ui/src/cache.rs
  10. 45
      ui/src/components/mail/listing.rs
  11. 3
      ui/src/components/mail/listing/conversations.rs
  12. 10
      ui/src/terminal/cells.rs

14
melib/src/backends.rs

@ -18,6 +18,16 @@
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
#[macro_export]
macro_rules! tag_hash {
($tag:ident) => {{
let mut hasher = DefaultHasher::new();
hasher.write($tag.as_bytes());
hasher.finish()
}};
}
#[cfg(feature = "imap_backend")]
pub mod imap;
#[cfg(feature = "maildir_backend")]
@ -288,6 +298,7 @@ pub trait BackendOp: ::std::fmt::Debug + ::std::marker::Send {
//fn copy(&self
fn fetch_flags(&self) -> Flag;
fn set_flag(&mut self, envelope: &mut Envelope, flag: Flag, value: bool) -> Result<()>;
fn set_tag(&mut self, envelope: &mut Envelope, tag: String, value: bool) -> Result<()>;
}
/// Wrapper for BackendOps that are to be set read-only.
@ -318,6 +329,9 @@ impl BackendOp for ReadOnlyOp {
fn set_flag(&mut self, _envelope: &mut Envelope, _flag: Flag, _value: bool) -> Result<()> {
Err(MeliError::new("read-only set."))
}
fn set_tag(&mut self, _envelope: &mut Envelope, _tag: String, _value: bool) -> Result<()> {
Err(MeliError::new("read-only set."))
}
}
#[derive(Debug, Copy, Hash, Eq, Clone, Serialize, Deserialize, PartialEq)]

14
melib/src/backends/imap.rs

@ -153,6 +153,7 @@ impl MailBackend for ImapType {
let handle = {
let tx = w.tx();
let uid_store = self.uid_store.clone();
let tag_index = self.tag_index.clone();
let can_create_flags = self.can_create_flags.clone();
let folder_path = folder.path().to_string();
let folder_hash = folder.hash();
@ -209,6 +210,7 @@ impl MailBackend for ImapType {
conn.read_response(&mut response)
);
let mut tag_lck = tag_index.write().unwrap();
while exists > 1 {
let mut envelopes = vec![];
exit_on_error!(&tx,
@ -231,8 +233,15 @@ impl MailBackend for ImapType {
h.write_usize(uid);
h.write(folder_path.as_bytes());
env.set_hash(h.finish());
if let Some(flags) = flags {
if let Some((flags, keywords)) = flags {
env.set_flags(flags);
for f in keywords {
let hash = tag_hash!(f);
if !tag_lck.contains_key(&hash) {
tag_lck.insert(hash, f);
}
env.labels_mut().push(hash);
}
}
uid_store
.hash_index
@ -272,6 +281,7 @@ impl MailBackend for ImapType {
.capabilities
.contains(&b"IDLE"[0..]);
let folders = self.folders.clone();
let tag_index = self.tag_index.clone();
let conn = ImapConnection::new_connection(&self.server_conf);
let main_conn = self.connection.clone();
let is_online = self.online.clone();
@ -292,6 +302,7 @@ impl MailBackend for ImapType {
folders,
sender,
work_context,
tag_index,
};
if has_idle {
idle(kit).ok().take();
@ -338,6 +349,7 @@ impl MailBackend for ImapType {
.to_string(),
self.connection.clone(),
self.uid_store.clone(),
self.tag_index.clone(),
))
}

54
melib/src/backends/imap/operations.rs

@ -38,6 +38,7 @@ pub struct ImapOp {
flags: Cell<Option<Flag>>,
connection: Arc<Mutex<ImapConnection>>,
uid_store: Arc<UIDStore>,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
}
impl ImapOp {
@ -46,6 +47,7 @@ impl ImapOp {
folder_path: String,
connection: Arc<Mutex<ImapConnection>>,
uid_store: Arc<UIDStore>,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
) -> Self {
ImapOp {
uid,
@ -56,6 +58,7 @@ impl ImapOp {
folder_path,
flags: Cell::new(None),
uid_store,
tag_index,
}
}
}
@ -89,7 +92,7 @@ impl BackendOp for ImapOp {
.to_full_result()
.map_err(MeliError::from)
{
Ok(v) => {
Ok(mut v) => {
if v.len() != 1 {
debug!("responses len is {}", v.len());
/* TODO: Trigger cache invalidation here. */
@ -98,11 +101,11 @@ impl BackendOp for ImapOp {
self.uid
)));
}
let (uid, flags, b) = v[0];
let (uid, flags, b) = v.remove(0);
assert_eq!(uid, self.uid);
if flags.is_some() {
self.flags.set(flags);
cache.flags = flags;
if let Some((flags, _)) = flags {
self.flags.set(Some(flags));
cache.flags = Some(flags);
}
cache.bytes = Some(unsafe { std::str::from_utf8_unchecked(b).to_string() });
}
@ -147,7 +150,7 @@ impl BackendOp for ImapOp {
/* TODO: Trigger cache invalidation here. */
panic!(format!("message with UID {} was not found", self.uid));
}
let (uid, flags) = v[0];
let (uid, (flags, _)) = v[0];
assert_eq!(uid, self.uid);
cache.flags = Some(flags);
self.flags.set(Some(flags));
@ -184,7 +187,7 @@ impl BackendOp for ImapOp {
Ok(v) => {
if v.len() == 1 {
debug!("responses len is {}", v.len());
let (uid, flags) = v[0];
let (uid, (flags, _)) = v[0];
assert_eq!(uid, self.uid);
self.flags.set(Some(flags));
}
@ -196,4 +199,41 @@ impl BackendOp for ImapOp {
cache.flags = Some(flags);
Ok(())
}
fn set_tag(&mut self, envelope: &mut Envelope, tag: String, value: bool) -> Result<()> {
let mut response = String::with_capacity(8 * 1024);
let mut conn = self.connection.lock().unwrap();
conn.send_command(format!("SELECT \"{}\"", &self.folder_path,).as_bytes())?;
conn.read_response(&mut response)?;
conn.send_command(
format!(
"UID STORE {} {}FLAGS.SILENT ({})",
self.uid,
if value { "+" } else { "-" },
&tag
)
.as_bytes(),
)?;
conn.read_response(&mut response)?;
protocol_parser::uid_fetch_flags_response(response.as_bytes())
.to_full_result()
.map_err(MeliError::from)?;
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(())
}
}

19
melib/src/backends/imap/protocol_parser.rs

@ -108,7 +108,7 @@ named!(
* "* 1 FETCH (FLAGS (\Seen) UID 1 RFC822.HEADER {5224} "
*/
named!(
pub uid_fetch_response<Vec<(usize, Option<Flag>, &[u8])>>,
pub uid_fetch_response<Vec<(usize, Option<(Flag, Vec<String>)>, &[u8])>>,
many0!(
do_parse!(
tag!("* ")
@ -124,7 +124,7 @@ named!(
);
named!(
pub uid_fetch_flags_response<Vec<(usize, Flag)>>,
pub uid_fetch_flags_response<Vec<(usize, (Flag, Vec<String>))>>,
many0!(
do_parse!(
tag!("* ")
@ -297,11 +297,11 @@ named!(
pub struct SelectResponse {
pub exists: usize,
pub recent: usize,
pub flags: Flag,
pub flags: (Flag, Vec<String>),
pub unseen: usize,
pub uidvalidity: usize,
pub uidnext: usize,
pub permanentflags: Flag,
pub permanentflags: (Flag, Vec<String>),
/// if SELECT returns \* we can set arbritary flags permanently.
pub can_create_flags: bool,
pub read_only: bool,
@ -366,8 +366,9 @@ pub fn select_response(input: &str) -> Result<SelectResponse> {
}
}
pub fn flags(input: &str) -> IResult<&str, Flag> {
pub fn flags(input: &str) -> IResult<&str, (Flag, Vec<String>)> {
let mut ret = Flag::default();
let mut keywords = Vec::new();
let mut input = input;
while !input.starts_with(")") && !input.is_empty() {
@ -399,16 +400,16 @@ pub fn flags(input: &str) -> IResult<&str, Flag> {
ret.set(Flag::DRAFT, true);
}
f => {
debug!("unknown Flag token value: {}", f);
keywords.push(f.to_string());
}
}
input = &input[match_end..];
input = input.trim_start();
}
IResult::Done(input, ret)
IResult::Done(input, (ret, keywords))
}
pub fn byte_flags(input: &[u8]) -> IResult<&[u8], Flag> {
pub fn byte_flags(input: &[u8]) -> IResult<&[u8], (Flag, Vec<String>)> {
let i = unsafe { std::str::from_utf8_unchecked(input) };
match flags(i) {
IResult::Done(rest, ret) => IResult::Done(rest.as_bytes(), ret),
@ -587,7 +588,7 @@ named!(
alt_complete!(map!(ws!(tag!("NIL")), |_| None) | map!(quoted, |v| Some(v))));
named!(
pub uid_fetch_envelopes_response<Vec<(usize, Option<Flag>, Envelope)>>,
pub uid_fetch_envelopes_response<Vec<(usize, Option<(Flag, Vec<String>)>, Envelope)>>,
many0!(
do_parse!(
tag!("* ")

79
melib/src/backends/imap/watch.rs

@ -30,6 +30,7 @@ pub struct ImapWatchKit {
pub folders: Arc<RwLock<FnvHashMap<FolderHash, ImapFolder>>>,
pub sender: RefreshEventConsumer,
pub work_context: WorkContext,
pub tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
}
macro_rules! exit_on_error {
@ -57,6 +58,7 @@ pub fn poll_with_examine(kit: ImapWatchKit) -> Result<()> {
folders,
sender,
work_context,
tag_index,
} = kit;
loop {
if *is_online.lock().unwrap() {
@ -81,7 +83,14 @@ pub fn poll_with_examine(kit: ImapWatchKit) -> Result<()> {
format!("examining `{}` for updates...", folder.path()),
))
.unwrap();
examine_updates(folder, &sender, &mut conn, &uid_store, &work_context)?;
examine_updates(
folder,
&sender,
&mut conn,
&uid_store,
&work_context,
&tag_index,
)?;
}
let mut main_conn = main_conn.lock().unwrap();
main_conn.send_command(b"NOOP").unwrap();
@ -101,6 +110,7 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> {
folders,
sender,
work_context,
tag_index,
} = kit;
loop {
if *is_online.lock().unwrap() {
@ -236,7 +246,14 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> {
folder_hash,
work_context,
thread_id,
examine_updates(folder, &sender, &mut conn, &uid_store, &work_context)
examine_updates(
folder,
&sender,
&mut conn,
&uid_store,
&work_context,
&tag_index
)
);
}
work_context
@ -301,7 +318,9 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> {
format!("parsing {}/{} envelopes..", ctr, len),
))
.unwrap();
if let Ok(env) = Envelope::from_bytes(&b, flags) {
if let Ok(mut env) =
Envelope::from_bytes(&b, flags.as_ref().map(|&(f, _)| f))
{
ctr += 1;
uid_store
.hash_index
@ -315,6 +334,16 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> {
env.subject(),
folder.path(),
);
if let Some((_, keywords)) = flags {
let mut tag_lck = tag_index.write().unwrap();
for f in keywords {
let hash = tag_hash!(f);
if !tag_lck.contains_key(&hash) {
tag_lck.insert(hash, f);
}
env.labels_mut().push(hash);
}
}
sender.send(RefreshEvent {
hash: folder_hash,
kind: Create(Box::new(env)),
@ -402,7 +431,9 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> {
ctr += 1;
continue;
}
if let Ok(env) = Envelope::from_bytes(&b, flags) {
if let Ok(mut env) =
Envelope::from_bytes(&b, flags.as_ref().map(|&(f, _)| f))
{
ctr += 1;
uid_store
.hash_index
@ -410,6 +441,16 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> {
.unwrap()
.insert(env.hash(), (uid, folder_hash));
uid_store.uid_index.lock().unwrap().insert(uid, env.hash());
if let Some((_, keywords)) = flags {
let mut tag_lck = tag_index.write().unwrap();
for f in keywords {
let hash = tag_hash!(f);
if !tag_lck.contains_key(&hash) {
tag_lck.insert(hash, f);
}
env.labels_mut().push(hash);
}
}
debug!(
"Create event {} {} {}",
env.hash(),
@ -467,6 +508,7 @@ fn examine_updates(
conn: &mut ImapConnection,
uid_store: &Arc<UIDStore>,
work_context: &WorkContext,
tag_index: &Arc<RwLock<BTreeMap<u64, String>>>,
) -> Result<()> {
let thread_id: std::thread::ThreadId = std::thread::current().id();
let folder_hash = folder.hash();
@ -551,7 +593,10 @@ fn examine_updates(
{
Ok(v) => {
for (uid, flags, b) in v {
if let Ok(env) = Envelope::from_bytes(&b, flags) {
if let Ok(mut env) = Envelope::from_bytes(
&b,
flags.as_ref().map(|&(f, _)| f),
) {
uid_store
.hash_index
.lock()
@ -568,6 +613,16 @@ fn examine_updates(
env.subject(),
folder.path(),
);
if let Some((_, keywords)) = flags {
let mut tag_lck = tag_index.write().unwrap();
for f in keywords {
let hash = tag_hash!(f);
if !tag_lck.contains_key(&hash) {
tag_lck.insert(hash, f);
}
env.labels_mut().push(hash);
}
}
sender.send(RefreshEvent {
hash: folder_hash,
kind: Create(Box::new(env)),
@ -614,13 +669,25 @@ fn examine_updates(
{
Ok(v) => {
for (uid, flags, b) in v {
if let Ok(env) = Envelope::from_bytes(&b, flags) {
if let Ok(mut env) =
Envelope::from_bytes(&b, flags.as_ref().map(|&(f, _)| f))
{
uid_store
.hash_index
.lock()
.unwrap()
.insert(env.hash(), (uid, folder_hash));
uid_store.uid_index.lock().unwrap().insert(uid, env.hash());
if let Some((_, keywords)) = flags {
let mut tag_lck = tag_index.write().unwrap();
for f in keywords {
let hash = tag_hash!(f);
if !tag_lck.contains_key(&hash) {
tag_lck.insert(hash, f);
}
env.labels_mut().push(hash);
}
}
debug!(
"Create event {} {} {}",
env.hash(),

4
melib/src/backends/maildir.rs

@ -162,6 +162,10 @@ impl<'a> BackendOp for MaildirOp {
debug!("success in rename");
Ok(())
}
fn set_tag(&mut self, _envelope: &mut Envelope, _tag: String, _value: bool) -> Result<()> {
Err(MeliError::new("Maildir doesn't support tags."))
}
}
#[derive(Debug, Default)]

4
melib/src/backends/mbox.rs

@ -241,6 +241,10 @@ impl BackendOp for MboxOp {
fn set_flag(&mut self, _envelope: &mut Envelope, _flag: Flag, _value: bool) -> Result<()> {
Ok(())
}
fn set_tag(&mut self, _envelope: &mut Envelope, _tag: String, _value: bool) -> Result<()> {
Err(MeliError::new("mbox doesn't support tags."))
}
}
pub fn mbox_parse(

57
melib/src/backends/notmuch.rs

@ -11,7 +11,7 @@ use crate::structs::StackVec;
use fnv::FnvHashMap;
use std::collections::hash_map::DefaultHasher;
use std::collections::BTreeMap;
use std::ffi::CStr;
use std::ffi::{CStr, CString};
use std::hash::{Hash, Hasher};
use std::io::Read;
use std::path::{Path, PathBuf};
@ -313,6 +313,7 @@ impl MailBackend for NotmuchDb {
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();
@ -372,6 +373,7 @@ impl MailBackend for NotmuchDb {
hash,
index: self.index.clone(),
bytes: None,
tag_index: self.tag_index.clone(),
})
}
@ -410,6 +412,7 @@ impl MailBackend for NotmuchDb {
struct NotmuchOp {
hash: EnvelopeHash,
index: Arc<RwLock<FnvHashMap<EnvelopeHash, &'static CStr>>>,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
database: DbWrapper,
bytes: Option<String>,
}
@ -547,6 +550,58 @@ impl BackendOp for NotmuchOp {
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 {
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 {
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 {
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 {

11
ui/src/cache.rs

@ -154,7 +154,10 @@ pub mod query_parser {
fn flags<'a>() -> impl Parser<'a, Query> {
move |input| {
whitespace_wrap(either(
match_literal_anycase("flags:"),
either(
match_literal_anycase("flags:"),
match_literal_anycase("tags:"),
),
match_literal_anycase("is:"),
))
.parse(input)
@ -322,6 +325,10 @@ pub mod query_parser {
Ok(("", Flags(vec!["test".to_string(), "testtest".to_string()]))),
query().parse_complete("flags:test,testtest")
);
assert_eq!(
query().parse_complete("flags:test,testtest"),
query().parse_complete("tags:test,testtest")
);
}
}
@ -387,7 +394,7 @@ pub fn query_to_imap(q: &Query) -> String {
s.push_str(" UNANSWERED ");
}
keyword => {
s.push_str(" ");
s.push_str(" KEYWORD ");
s.extend(keyword.chars());
s.push_str(" ");
}

45
ui/src/components/mail/listing.rs

@ -105,42 +105,27 @@ pub trait MailListingTrait: ListingTrait {
continue;
}
ListingAction::Tag(Remove(ref tag_str)) => {
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
let h = {
let mut hasher = DefaultHasher::new();
hasher.write(tag_str.as_bytes());
hasher.finish()
};
let backend_lck = account.backend.write().unwrap();
if let Some(t) = backend_lck.tags() {
let mut tags_lck = t.write().unwrap();
if !tags_lck.contains_key(&h) {
tags_lck.insert(h, tag_str.to_string());
}
if let Some(pos) = envelope.labels().iter().position(|&el| el == h) {
envelope.labels_mut().remove(pos);
}
} else {
let mut op = backend_lck.operation(envelope.hash());
if let Err(err) = op.set_tag(&mut envelope, tag_str.to_string(), false) {
context.replies.push_back(UIEvent::Notification(
Some("Could not set tag.".to_string()),
err.to_string(),
Some(NotificationType::ERROR),
));
return;
}
}
ListingAction::Tag(Add(ref tag_str)) => {
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
let h = {
let mut hasher = DefaultHasher::new();
hasher.write(tag_str.as_bytes());
hasher.finish()
};
let backend_lck = account.backend.write().unwrap();
if let Some(t) = backend_lck.tags() {
let mut tags_lck = t.write().unwrap();
if !tags_lck.contains_key(&h) {
tags_lck.insert(h, tag_str.to_string());
}
envelope.labels_mut().push(h);
} else {
let mut op = backend_lck.operation(envelope.hash());
if let Err(err) = op.set_tag(&mut envelope, tag_str.to_string(), true) {
context.replies.push_back(UIEvent::Notification(
Some("Could not set tag.".to_string()),
err.to_string(),
Some(NotificationType::ERROR),
));
return;
}
}

3
ui/src/components/mail/listing/conversations.rs

@ -1131,6 +1131,9 @@ impl Component for ConversationsListing {
area,
Some(get_x(upper_left)),
);
for c in grid.row_iter((x, get_x(bottom_right)), y) {
grid[c] = Cell::default();
}
clear_area(grid, ((x, y), set_y(bottom_right, y)));
context
.dirty_areas

10
ui/src/terminal/cells.rs

@ -1576,9 +1576,6 @@ pub fn write_string_to_grid(
if c == '\r' {
continue;
}
grid[(x, y)].set_attrs(attrs);
grid[(x, y)].set_fg(fg_color);
grid[(x, y)].set_bg(bg_color);
if c == '\t' {
grid[(x, y)].set_ch(' ');
x += 1;
@ -1587,6 +1584,9 @@ pub fn write_string_to_grid(
} else {
grid[(x, y)].set_ch(c);
}
grid[(x, y)].set_attrs(attrs);
grid[(x, y)].set_fg(fg_color);
grid[(x, y)].set_bg(bg_color);
match wcwidth(u32::from(c)) {
Some(0) | None => {
@ -1598,6 +1598,10 @@ pub fn write_string_to_grid(
* drawn over. Set it as empty to skip drawing it. */
x += 1;
inspect_bounds!(grid, area, x, y, line_break);
grid[(x, y)] = Cell::default();
grid[(x, y)].set_attrs(attrs);
grid[(x, y)].set_fg(fg_color);
grid[(x, y)].set_bg(bg_color);
grid[(x, y)].empty = true;
}
_ => {}

Loadingโ€ฆ
Cancel
Save