Rewrite email flag modifications

Flag and tag modifications are now somewhat typed better, and the
frontend applies them on its own on success. This means that if you set
an unseen mail as seen but it was already seen in the backend, you will
see the change locally. Previously it would remain unseen.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
pull/314/head
Manos Pitsidianakis 7 months ago
parent f81a1e2338
commit 6506fffb94
No known key found for this signature in database
GPG Key ID: 7729C7707F7E09D0

@ -61,6 +61,8 @@ use crate::{
MainLoopHandler, StatusEvent, ThreadEvent, MainLoopHandler, StatusEvent, ThreadEvent,
}; };
mod backend_ops;
#[macro_export] #[macro_export]
macro_rules! try_recv_timeout { macro_rules! try_recv_timeout {
($oneshot:expr) => {{ ($oneshot:expr) => {{
@ -242,6 +244,7 @@ pub enum JobRequest {
}, },
SetFlags { SetFlags {
env_hashes: EnvelopeHashBatch, env_hashes: EnvelopeHashBatch,
flags: SmallVec<[FlagOp; 8]>,
handle: JoinHandle<Result<()>>, handle: JoinHandle<Result<()>>,
}, },
SaveMessage { SaveMessage {
@ -325,7 +328,13 @@ impl std::fmt::Debug for JobRequest {
} }
JobRequest::IsOnline { .. } => write!(f, "JobRequest::IsOnline"), JobRequest::IsOnline { .. } => write!(f, "JobRequest::IsOnline"),
JobRequest::Refresh { .. } => write!(f, "JobRequest::Refresh"), JobRequest::Refresh { .. } => write!(f, "JobRequest::Refresh"),
JobRequest::SetFlags { .. } => write!(f, "JobRequest::SetFlags"), JobRequest::SetFlags {
env_hashes, flags, ..
} => f
.debug_struct(stringify!(JobRequest::SetFlags))
.field("env_hashes", &env_hashes)
.field("flags", &flags)
.finish(),
JobRequest::SaveMessage { .. } => write!(f, "JobRequest::SaveMessage"), JobRequest::SaveMessage { .. } => write!(f, "JobRequest::SaveMessage"),
JobRequest::DeleteMessages { .. } => write!(f, "JobRequest::DeleteMessages"), JobRequest::DeleteMessages { .. } => write!(f, "JobRequest::DeleteMessages"),
JobRequest::CreateMailbox { .. } => write!(f, "JobRequest::CreateMailbox"), JobRequest::CreateMailbox { .. } => write!(f, "JobRequest::CreateMailbox"),
@ -356,11 +365,14 @@ impl std::fmt::Display for JobRequest {
JobRequest::Fetch { .. } => write!(f, "Mailbox fetch"), JobRequest::Fetch { .. } => write!(f, "Mailbox fetch"),
JobRequest::IsOnline { .. } => write!(f, "Online status check"), JobRequest::IsOnline { .. } => write!(f, "Online status check"),
JobRequest::Refresh { .. } => write!(f, "Refresh mailbox"), JobRequest::Refresh { .. } => write!(f, "Refresh mailbox"),
JobRequest::SetFlags { env_hashes, .. } => write!( JobRequest::SetFlags {
env_hashes, flags, ..
} => write!(
f, f,
"Set flags for {} message{}", "Set flags for {} message{}: {:?}",
env_hashes.len(), env_hashes.len(),
if env_hashes.len() == 1 { "" } else { "s" } if env_hashes.len() == 1 { "" } else { "s" },
flags
), ),
JobRequest::SaveMessage { .. } => write!(f, "Save message"), JobRequest::SaveMessage { .. } => write!(f, "Save message"),
JobRequest::DeleteMessages { env_hashes, .. } => write!( JobRequest::DeleteMessages { env_hashes, .. } => write!(
@ -777,42 +789,7 @@ impl Account {
); );
} }
#[cfg(feature = "sqlite3")] #[cfg(feature = "sqlite3")]
if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 { self.update_cached_env(*envelope.clone(), Some(old_hash));
match crate::sqlite3::remove(old_hash).map(|_| {
crate::sqlite3::insert(
(*envelope).clone(),
self.backend.clone(),
self.name.clone(),
)
}) {
Err(err) => {
log::error!(
"Failed to update envelope {} in cache: {}",
envelope.message_id_display(),
err
);
}
Ok(job) => {
let handle = self
.main_loop_handler
.job_executor
.spawn_blocking("sqlite3::update".into(), job);
self.insert_job(
handle.job_id,
JobRequest::Generic {
name: format!(
"Update envelope {} in sqlite3 cache",
envelope.message_id_display()
)
.into(),
handle,
log_level: LogLevel::TRACE,
on_finish: None,
},
);
}
}
}
self.collection.update(old_hash, *envelope, mailbox_hash); self.collection.update(old_hash, *envelope, mailbox_hash);
return Some(EnvelopeUpdate(old_hash)); return Some(EnvelopeUpdate(old_hash));
} }
@ -833,43 +810,18 @@ impl Account {
entry.set_flags(flags); entry.set_flags(flags);
}); });
#[cfg(feature = "sqlite3")] #[cfg(feature = "sqlite3")]
if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 { if let Some(env) = {
match crate::sqlite3::remove(env_hash).map(|_| { let temp = self
crate::sqlite3::insert( .collection
self.collection.envelopes.read().unwrap()[&env_hash].clone(), .envelopes
self.backend.clone(), .read()
self.name.clone(), .unwrap()
) .get(&env_hash)
}) { .cloned();
Ok(job) => {
let handle = self temp
.main_loop_handler } {
.job_executor self.update_cached_env(env, None);
.spawn_blocking("sqlite3::remove".into(), job);
self.insert_job(
handle.job_id,
JobRequest::Generic {
name: format!(
"Update envelope {} in sqlite3 cache",
self.collection.envelopes.read().unwrap()[&env_hash]
.message_id_display()
)
.into(),
handle,
log_level: LogLevel::TRACE,
on_finish: None,
},
);
}
Err(err) => {
log::error!(
"Failed to update envelope {} in cache: {}",
self.collection.envelopes.read().unwrap()[&env_hash]
.message_id_display(),
err
);
}
}
} }
self.collection.update_flags(env_hash, mailbox_hash); self.collection.update_flags(env_hash, mailbox_hash);
return Some(EnvelopeUpdate(env_hash)); return Some(EnvelopeUpdate(env_hash));
@ -880,43 +832,18 @@ impl Account {
return Some(EnvelopeRename(old_hash, new_hash)); return Some(EnvelopeRename(old_hash, new_hash));
} }
#[cfg(feature = "sqlite3")] #[cfg(feature = "sqlite3")]
if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 { if let Some(env) = {
match crate::sqlite3::remove(old_hash).map(|_| { let temp = self
crate::sqlite3::insert( .collection
self.collection.envelopes.read().unwrap()[&new_hash].clone(), .envelopes
self.backend.clone(), .read()
self.name.clone(), .unwrap()
) .get(&new_hash)
}) { .cloned();
Err(err) => {
log::error!( temp
"Failed to update envelope {} in cache: {}", } {
&self.collection.envelopes.read().unwrap()[&new_hash] self.update_cached_env(env, Some(old_hash));
.message_id_display(),
err
);
}
Ok(job) => {
let handle = self
.main_loop_handler
.job_executor
.spawn_blocking("sqlite3::rename".into(), job);
self.insert_job(
handle.job_id,
JobRequest::Generic {
name: format!(
"Update envelope {} in sqlite3 cache",
self.collection.envelopes.read().unwrap()[&new_hash]
.message_id_display()
)
.into(),
handle,
log_level: LogLevel::TRACE,
on_finish: None,
},
);
}
}
} }
return Some(EnvelopeRename(old_hash, new_hash)); return Some(EnvelopeRename(old_hash, new_hash));
} }
@ -1952,8 +1879,12 @@ impl Account {
} }
} }
} }
JobRequest::SetFlags { ref mut handle, .. } => { JobRequest::SetFlags {
if let Ok(Some(Err(err))) = handle.chan.try_recv() { ref mut handle,
ref env_hashes,
ref flags,
} => match handle.chan.try_recv() {
Ok(Some(Err(err))) => {
self.main_loop_handler self.main_loop_handler
.job_executor .job_executor
.set_job_success(job_id, false); .set_job_success(job_id, false);
@ -1964,7 +1895,50 @@ impl Account {
Some(crate::types::NotificationType::Error(err.kind)), Some(crate::types::NotificationType::Error(err.kind)),
))); )));
} }
} Ok(Some(Ok(()))) => {
for env_hash in env_hashes.iter() {
if !self.collection.contains_key(&env_hash) {
continue;
}
let mut env_lck = self.collection.envelopes.write().unwrap();
env_lck.entry(env_hash).and_modify(|entry| {
for op in flags.iter() {
match op {
FlagOp::Set(f) => {
let mut flags = entry.flags();
flags.set(*f, true);
entry.set_flags(flags);
}
FlagOp::UnSet(f) => {
let mut flags = entry.flags();
flags.set(*f, false);
entry.set_flags(flags);
}
FlagOp::SetTag(t) => {
entry
.tags_mut()
.insert(TagHash::from_bytes(t.as_bytes()));
}
FlagOp::UnSetTag(t) => {
entry
.tags_mut()
.remove(&TagHash::from_bytes(t.as_bytes()));
}
}
}
});
#[cfg(feature = "sqlite3")]
if let Some(env) = env_lck.get(&env_hash).cloned() {
drop(env_lck);
self.update_cached_env(env, None);
}
self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::EnvelopeUpdate(env_hash)));
}
}
_ => {}
},
JobRequest::SaveMessage { JobRequest::SaveMessage {
ref mut handle, ref mut handle,
ref bytes, ref bytes,

@ -0,0 +1,86 @@
/*
* meli - accounts module.
*
* Copyright 2023 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/>.
*/
//! Account mail backend operations.
use super::*;
impl Account {
pub fn set_flags(
&mut self,
env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash,
flags: SmallVec<[FlagOp; 8]>,
) -> Result<JobId> {
let fut = self.backend.write().unwrap().set_flags(
env_hashes.clone(),
mailbox_hash,
flags.clone(),
)?;
let handle = self
.main_loop_handler
.job_executor
.spawn_specialized("set_unseen".into(), fut);
let job_id = handle.job_id;
self.insert_job(
job_id,
JobRequest::SetFlags {
env_hashes,
flags,
handle,
},
);
Ok(job_id)
}
#[cfg(not(feature = "sqlite3"))]
pub(super) fn update_cached_env(&mut self, _: Envelope, _: Option<EnvelopeHash>) {}
#[cfg(feature = "sqlite3")]
pub(super) fn update_cached_env(&mut self, env: Envelope, old_hash: Option<EnvelopeHash>) {
if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 {
let msg_id = env.message_id_display().to_string();
match crate::sqlite3::remove(old_hash.unwrap_or(env.hash()))
.map(|_| crate::sqlite3::insert(env, self.backend.clone(), self.name.clone()))
{
Ok(job) => {
let handle = self
.main_loop_handler
.job_executor
.spawn_blocking("sqlite3::remove".into(), job);
self.insert_job(
handle.job_id,
JobRequest::Generic {
name: format!("Update envelope {} in sqlite3 cache", msg_id).into(),
handle,
log_level: LogLevel::TRACE,
on_finish: None,
},
);
}
Err(err) => {
log::error!("Failed to update envelope {} in cache: {}", msg_id, err);
}
}
}
}
}

@ -31,7 +31,8 @@ use std::{
use futures::future::try_join_all; use futures::future::try_join_all;
use melib::{ use melib::{
backends::EnvelopeHashBatch, mbox::MboxMetadata, utils::datetime, Address, UnixTimestamp, backends::EnvelopeHashBatch, mbox::MboxMetadata, utils::datetime, Address, FlagOp,
UnixTimestamp,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
@ -505,99 +506,47 @@ pub trait MailListingTrait: ListingTrait {
let account = &mut context.accounts[&account_hash]; let account = &mut context.accounts[&account_hash];
match a { match a {
ListingAction::SetSeen => { ListingAction::SetSeen => {
let job = account.backend.write().unwrap().set_flags( if let Err(err) = account.set_flags(
env_hashes.clone(), env_hashes.clone(),
mailbox_hash, mailbox_hash,
smallvec::smallvec![(Ok(Flag::SEEN), true)], smallvec::smallvec![FlagOp::Set(Flag::SEEN)],
); ) {
match job { context.replies.push_back(UIEvent::StatusEvent(
Err(err) => { StatusEvent::DisplayMessage(err.to_string()),
context.replies.push_back(UIEvent::StatusEvent( ));
StatusEvent::DisplayMessage(err.to_string()),
));
}
Ok(fut) => {
let handle = account
.main_loop_handler
.job_executor
.spawn_specialized("set_seen".into(), fut);
account.insert_job(
handle.job_id,
JobRequest::SetFlags { env_hashes, handle },
);
}
} }
} }
ListingAction::SetUnseen => { ListingAction::SetUnseen => {
let job = account.backend.write().unwrap().set_flags( if let Err(err) = account.set_flags(
env_hashes.clone(), env_hashes.clone(),
mailbox_hash, mailbox_hash,
smallvec::smallvec![(Ok(Flag::SEEN), false)], smallvec::smallvec![FlagOp::UnSet(Flag::SEEN)],
); ) {
match job { context.replies.push_back(UIEvent::StatusEvent(
Err(err) => { StatusEvent::DisplayMessage(err.to_string()),
context.replies.push_back(UIEvent::StatusEvent( ));
StatusEvent::DisplayMessage(err.to_string()),
));
}
Ok(fut) => {
let handle = account
.main_loop_handler
.job_executor
.spawn_specialized("set_unseen".into(), fut);
account.insert_job(
handle.job_id,
JobRequest::SetFlags { env_hashes, handle },
);
}
} }
} }
ListingAction::Tag(Remove(ref tag_str)) => { ListingAction::Tag(Add(ref tag_str)) => {
let job = account.backend.write().unwrap().set_flags( if let Err(err) = account.set_flags(
env_hashes.clone(), env_hashes.clone(),
mailbox_hash, mailbox_hash,
smallvec::smallvec![(Err(tag_str.to_string()), false)], smallvec::smallvec![FlagOp::SetTag(tag_str.into())],
); ) {
match job { context.replies.push_back(UIEvent::StatusEvent(
Err(err) => { StatusEvent::DisplayMessage(err.to_string()),
context.replies.push_back(UIEvent::StatusEvent( ));
StatusEvent::DisplayMessage(err.to_string()),
));
}
Ok(fut) => {
let handle = account
.main_loop_handler
.job_executor
.spawn_specialized("remove_tag".into(), fut);
account.insert_job(
handle.job_id,
JobRequest::SetFlags { env_hashes, handle },
);
}
} }
} }
ListingAction::Tag(Add(ref tag_str)) => { ListingAction::Tag(Remove(ref tag_str)) => {
let job = account.backend.write().unwrap().set_flags( if let Err(err) = account.set_flags(
env_hashes.clone(), env_hashes.clone(),
mailbox_hash, mailbox_hash,
smallvec::smallvec![(Err(tag_str.to_string()), true)], smallvec::smallvec![FlagOp::UnSetTag(tag_str.into())],
); ) {
match job { context.replies.push_back(UIEvent::StatusEvent(
Err(err) => { StatusEvent::DisplayMessage(err.to_string()),
context.replies.push_back(UIEvent::StatusEvent( ));
StatusEvent::DisplayMessage(err.to_string()),
));
}
Ok(fut) => {
let handle = account
.main_loop_handler
.job_executor
.spawn_specialized("add_tag".into(), fut);
account.insert_job(
handle.job_id,
JobRequest::SetFlags { env_hashes, handle },
);
}
} }
} }
ListingAction::Delete => { ListingAction::Delete => {

@ -29,7 +29,7 @@ use std::{
use melib::{ use melib::{
email::attachment_types::ContentType, list_management, mailto::Mailto, parser::BytesExt, email::attachment_types::ContentType, list_management, mailto::Mailto, parser::BytesExt,
utils::datetime, Card, Draft, HeaderName, SpecialUsageMailbox, utils::datetime, Card, Draft, FlagOp, HeaderName, SpecialUsageMailbox,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
@ -310,32 +310,16 @@ impl Component for MailView {
{ {
let account = &mut context.accounts[&coordinates.0]; let account = &mut context.accounts[&coordinates.0];
if !account.collection.get_env(coordinates.2).is_seen() { if !account.collection.get_env(coordinates.2).is_seen() {
let job = account.backend.write().unwrap().set_flags( if let Err(err) = account.set_flags(
coordinates.2.into(), coordinates.2.into(),
coordinates.1, coordinates.1,
smallvec::smallvec![(Ok(Flag::SEEN), true)], smallvec::smallvec![FlagOp::Set(Flag::SEEN)],
); ) {
match job { context.replies.push_back(UIEvent::StatusEvent(
Ok(fut) => { StatusEvent::DisplayMessage(format!(
let handle = account "Could not set message as seen: {err}",
.main_loop_handler )),
.job_executor ));
.spawn_specialized("set_flags".into(), fut);
account.insert_job(
handle.job_id,
JobRequest::SetFlags {
env_hashes: coordinates.2.into(),
handle,
},
);
}
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Could not set message as seen: {err}",
)),
));
}
} }
} }
} }

@ -320,6 +320,37 @@ impl Deref for BackendEventConsumer {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum FlagOp {
Set(Flag),
SetTag(String),
UnSet(Flag),
UnSetTag(String),
}
impl From<&FlagOp> for bool {
fn from(val: &FlagOp) -> Self {
matches!(val, FlagOp::Set(_) | FlagOp::SetTag(_))
}
}
impl FlagOp {
#[inline]
pub fn is_flag(&self) -> bool {
matches!(self, Self::Set(_) | Self::UnSet(_))
}
#[inline]
pub fn is_tag(&self) -> bool {
matches!(self, Self::SetTag(_) | Self::UnSetTag(_))
}
#[inline]
pub fn as_bool(&self) -> bool {
self.into()
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MailBackendCapabilities { pub struct MailBackendCapabilities {
pub is_async: bool, pub is_async: bool,
@ -376,7 +407,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
&mut self, &mut self,
env_hashes: EnvelopeHashBatch, env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash, mailbox_hash: MailboxHash,
flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>, flags: SmallVec<[FlagOp; 8]>,
) -> ResultFuture<()>; ) -> ResultFuture<()>;
fn delete_messages( fn delete_messages(

@ -116,6 +116,7 @@ use imap_codec::imap_types::{
fetch::{MacroOrMessageDataItemNames, MessageDataItemName, Section}, fetch::{MacroOrMessageDataItemNames, MessageDataItemName, Section},
flag::Flag as ImapCodecFlag, flag::Flag as ImapCodecFlag,
}; };
use indexmap::IndexSet;
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::{ use crate::{
@ -287,7 +288,7 @@ pub struct Envelope {
pub thread: ThreadNodeHash, pub thread: ThreadNodeHash,
pub flags: Flag, pub flags: Flag,
pub has_attachments: bool, pub has_attachments: bool,
pub tags: SmallVec<[TagHash; 8]>, pub tags: IndexSet<TagHash>,
} }
impl std::fmt::Debug for Envelope { impl std::fmt::Debug for Envelope {
@ -330,7 +331,7 @@ impl Envelope {
thread: ThreadNodeHash::null(), thread: ThreadNodeHash::null(),
has_attachments: false, has_attachments: false,
flags: Flag::default(), flags: Flag::default(),
tags: SmallVec::new(), tags: IndexSet::new(),
} }
} }
@ -851,11 +852,11 @@ impl Envelope {
self.has_attachments self.has_attachments
} }
pub fn tags(&self) -> &SmallVec<[TagHash; 8]> { pub fn tags(&self) -> &IndexSet<TagHash> {
&self.tags &self.tags
} }
pub fn tags_mut(&mut self) -> &mut SmallVec<[TagHash; 8]> { pub fn tags_mut(&mut self) -> &mut IndexSet<TagHash> {
&mut self.tags &mut self.tags
} }
} }

@ -25,6 +25,7 @@ use imap_codec::imap_types::{
sequence::SequenceSet, sequence::SequenceSet,
status::StatusDataItemName, status::StatusDataItemName,
}; };
use indexmap::IndexSet;
use super::*; use super::*;
@ -173,7 +174,7 @@ impl ImapConnection {
for f in keywords { for f in keywords {
let hash = TagHash::from_bytes(f.as_bytes()); let hash = TagHash::from_bytes(f.as_bytes());
tag_lck.entry(hash).or_insert_with(|| f.to_string()); tag_lck.entry(hash).or_insert_with(|| f.to_string());
env.tags_mut().push(hash); env.tags_mut().insert(hash);
} }
} }
} }
@ -266,7 +267,7 @@ impl ImapConnection {
!= &tags != &tags
.iter() .iter()
.map(|t| TagHash::from_bytes(t.as_bytes())) .map(|t| TagHash::from_bytes(t.as_bytes()))
.collect::<SmallVec<[TagHash; 8]>>() .collect::<IndexSet<TagHash>>()
{ {
env_lck.entry(env_hash).and_modify(|entry| { env_lck.entry(env_hash).and_modify(|entry| {
entry.inner.set_flags(flags); entry.inner.set_flags(flags);
@ -466,7 +467,7 @@ impl ImapConnection {
for f in keywords { for f in keywords {
let hash = TagHash::from_bytes(f.as_bytes()); let hash = TagHash::from_bytes(f.as_bytes());
tag_lck.entry(hash).or_insert_with(|| f.to_string()); tag_lck.entry(hash).or_insert_with(|| f.to_string());
env.tags_mut().push(hash); env.tags_mut().insert(hash);
} }
} }
} }
@ -556,7 +557,7 @@ impl ImapConnection {
!= &tags != &tags
.iter() .iter()
.map(|t| TagHash::from_bytes(t.as_bytes())) .map(|t| TagHash::from_bytes(t.as_bytes()))
.collect::<SmallVec<[TagHash; 8]>>() .collect::<IndexSet<TagHash>>()
{ {
env_lck.entry(env_hash).and_modify(|entry| { env_lck.entry(env_hash).and_modify(|entry| {
entry.inner.set_flags(flags); entry.inner.set_flags(flags);

@ -701,7 +701,7 @@ impl MailBackend for ImapType {
&mut self, &mut self,
env_hashes: EnvelopeHashBatch, env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash, mailbox_hash: MailboxHash,
flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>, flags: SmallVec<[FlagOp; 8]>,
) -> ResultFuture<()> { ) -> ResultFuture<()> {
let connection = self.connection.clone(); let connection = self.connection.clone();
let uid_store = self.uid_store.clone(); let uid_store = self.uid_store.clone();
@ -724,7 +724,7 @@ impl MailBackend for ImapType {
let mut conn = connection.lock().await; let mut conn = connection.lock().await;
conn.select_mailbox(mailbox_hash, &mut response, false) conn.select_mailbox(mailbox_hash, &mut response, false)
.await?; .await?;
if flags.iter().any(|(_, b)| *b) { if flags.iter().any(<bool>::from) {
/* Set flags/tags to true */ /* Set flags/tags to true */
let mut set_seen = false; let mut set_seen = false;
let command = { let command = {
@ -734,28 +734,25 @@ impl MailBackend for ImapType {
cmd = format!("{},{}", cmd, uid); cmd = format!("{},{}", cmd, uid);
} }
cmd = format!("{} +FLAGS (", cmd); cmd = format!("{} +FLAGS (", cmd);
for (f, v) in flags.iter() { for op in flags.iter().filter(|op| <bool>::from(*op)) {
if !*v { match op {
continue; FlagOp::Set(flag) if *flag == Flag::REPLIED => {
}
match f {
Ok(flag) if *flag == Flag::REPLIED => {
cmd.push_str("\\Answered "); cmd.push_str("\\Answered ");
} }
Ok(flag) if *flag == Flag::FLAGGED => { FlagOp::Set(flag) if *flag == Flag::FLAGGED => {
cmd.push_str("\\Flagged "); cmd.push_str("\\Flagged ");
} }
Ok(flag) if *flag == Flag::TRASHED => { FlagOp::Set(flag) if *flag == Flag::TRASHED => {
cmd.push_str("\\Deleted "); cmd.push_str("\\Deleted ");
} }
Ok(flag) if *flag == Flag::SEEN => { FlagOp::Set(flag) if *flag == Flag::SEEN => {
cmd.push_str("\\Seen "); cmd.push_str("\\Seen ");
set_seen = true; set_seen = true;
} }
Ok(flag) if *flag == Flag::DRAFT => { FlagOp::Set(flag) if *flag == Flag::DRAFT => {
cmd.push_str("\\Draft "); cmd.push_str("\\Draft ");
} }
Ok(_) => { FlagOp::Set(_) => {
log::error!( log::error!(
"Application error: more than one flag bit set in set_flags: \ "Application error: more than one flag bit set in set_flags: \
{:?}", {:?}",
@ -768,12 +765,13 @@ impl MailBackend for ImapType {
)) ))
.set_kind(crate::ErrorKind::Bug)); .set_kind(crate::ErrorKind::Bug));
} }
Err(tag) => { FlagOp::SetTag(tag) => {
let hash = TagHash::from_bytes(tag.as_bytes()); let hash = TagHash::from_bytes(tag.as_bytes());
tag_lck.entry(hash).or_insert_with(|| tag.to_string()); tag_lck.entry(hash).or_insert_with(|| tag.to_string());
cmd.push_str(tag); cmd.push_str(tag);
cmd.push(' '); cmd.push(' ');
} }
_ => {}
} }
} }
// pop last space // pop last space
@ -794,7 +792,7 @@ impl MailBackend for ImapType {
} }
} }
} }
if flags.iter().any(|(_, b)| !*b) { if flags.iter().any(|b| !<bool>::from(b)) {
let mut set_unseen = false; let mut set_unseen = false;
/* Set flags/tags to false */ /* Set flags/tags to false */
let command = { let command = {
@ -803,28 +801,25 @@ impl MailBackend for ImapType {
cmd = format!("{},{}", cmd, uid); cmd = format!("{},{}", cmd, uid);
} }
cmd = format!("{} -FLAGS (", cmd); cmd = format!("{} -FLAGS (", cmd);
for (f, v) in flags.iter() { for op in flags.iter().filter(|op| !<bool>::from(*op)) {
if *v { match op {
continue; FlagOp::UnSet(flag) if *flag == Flag::REPLIED => {
}
match f {
Ok(flag) if *flag == Flag::REPLIED => {
cmd.push_str("\\Answered "); cmd.push_str("\\Answered ");
} }
Ok(flag) if *flag == Flag::FLAGGED => { FlagOp::UnSet(flag) if *flag == Flag::FLAGGED => {
cmd.push_str("\\Flagged "); cmd.push_str("\\Flagged ");
} }
Ok(flag) if *flag == Flag::TRASHED => { FlagOp::UnSet(flag) if *flag == Flag::TRASHED => {
cmd.push_str("\\Deleted "); cmd.push_str("\\Deleted ");
} }
Ok(flag) if *flag == Flag::SEEN => { FlagOp::UnSet(flag) if *flag == Flag::SEEN => {
cmd.push_str("\\Seen "); cmd.push_str("\\Seen ");
set_unseen = true; set_unseen = true;
} }
Ok(flag) if *flag == Flag::DRAFT => { FlagOp::UnSet(flag) if *flag == Flag::DRAFT => {
cmd.push_str("\\Draft "); cmd.push_str("\\Draft ");
} }
Ok(_) => { FlagOp::UnSet(_) => {
log::error!( log::error!(
"Application error: more than one flag bit set in set_flags: \ "Application error: more than one flag bit set in set_flags: \
{:?}", {:?}",
@ -836,12 +831,14 @@ impl MailBackend for ImapType {
flags flags
))); )));
} }
Err(tag) => { FlagOp::UnSetTag(tag) => {
cmd.push_str(tag); cmd.push_str(tag);
cmd.push(' '); cmd.push(' ');
} }
_ => {}
} }
} }
// [ref:TODO] there should be a check that cmd is not empty here.
// pop last space // pop last space
cmd.pop(); cmd.pop();
cmd.push(')'); cmd.push(')');
@ -872,7 +869,7 @@ impl MailBackend for ImapType {
let flag_future = self.set_flags( let flag_future = self.set_flags(
env_hashes, env_hashes,
mailbox_hash, mailbox_hash,
smallvec::smallvec![(Ok(Flag::TRASHED), true)], smallvec::smallvec![FlagOp::Set(Flag::TRASHED)],
)?; )?;
let connection = self.connection.clone(); let connection = self.connection.clone();
Ok(Box::pin(async move { Ok(Box::pin(async move {
@ -1811,7 +1808,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
for f in keywords { for f in keywords {
let hash = TagHash::from_bytes(f.as_bytes()); let hash = TagHash::from_bytes(f.as_bytes());
tag_lck.entry(hash).or_insert_with(|| f.to_string()); tag_lck.entry(hash).or_insert_with(|| f.to_string());
env.tags_mut().push(hash); env.tags_mut().insert(hash);
} }
} }
} }

@ -246,7 +246,7 @@ impl ImapConnection {
for f in keywords { for f in keywords {
let hash = TagHash::from_bytes(f.as_bytes()); let hash = TagHash::from_bytes(f.as_bytes());
tag_lck.entry(hash).or_insert_with(|| f.to_string()); tag_lck.entry(hash).or_insert_with(|| f.to_string());
env.tags_mut().push(hash); env.tags_mut().insert(hash);
} }
} }
mailbox.exists.lock().unwrap().insert_new(env.hash()); mailbox.exists.lock().unwrap().insert_new(env.hash());
@ -379,7 +379,7 @@ impl ImapConnection {
for f in keywords { for f in keywords {
let hash = TagHash::from_bytes(f.as_bytes()); let hash = TagHash::from_bytes(f.as_bytes());
tag_lck.entry(hash).or_insert_with(|| f.to_string()); tag_lck.entry(hash).or_insert_with(|| f.to_string());
env.tags_mut().push(hash); env.tags_mut().insert(hash);
} }
} }
mailbox.exists.lock().unwrap().insert_new(env.hash()); mailbox.exists.lock().unwrap().insert_new(env.hash());

@ -379,7 +379,7 @@ pub async fn examine_updates(
for f in keywords { for f in keywords {
let hash = TagHash::from_bytes(f.as_bytes()); let hash = TagHash::from_bytes(f.as_bytes());
tag_lck.entry(hash).or_insert_with(|| f.to_string()); tag_lck.entry(hash).or_insert_with(|| f.to_string());
env.tags_mut().push(hash); env.tags_mut().insert(hash);
} }
} }
} }

@ -29,7 +29,7 @@ use std::{
}; };
use futures::{lock::Mutex as FutureMutex, Stream}; use futures::{lock::Mutex as FutureMutex, Stream};
use indexmap::IndexMap; use indexmap::{IndexMap, IndexSet};
use isahc::{config::RedirectPolicy, AsyncReadResponseExt, HttpClient}; use isahc::{config::RedirectPolicy, AsyncReadResponseExt, HttpClient};
use serde_json::{json, Value}; use serde_json::{json, Value};
use smallvec::SmallVec; use smallvec::SmallVec;
@ -209,7 +209,7 @@ pub struct Store {
impl Store { impl Store {
pub fn add_envelope(&self, obj: EmailObject) -> Envelope { pub fn add_envelope(&self, obj: EmailObject) -> Envelope {
let mut flags = Flag::default(); let mut flags = Flag::default();
let mut labels: SmallVec<[TagHash; 8]> = SmallVec::new(); let mut labels: IndexSet<TagHash> = IndexSet::new();
let mut tag_lck = self.collection.tag_index.write().unwrap(); let mut tag_lck = self.collection.tag_index.write().unwrap();
for t in obj.keywords().keys() { for t in obj.keywords().keys() {
match t.as_str() { match t.as_str() {
@ -229,7 +229,7 @@ impl Store {
_ => { _ => {
let tag_hash = TagHash::from_bytes(t.as_bytes()); let tag_hash = TagHash::from_bytes(t.as_bytes());
tag_lck.entry(tag_hash).or_insert_with(|| t.to_string()); tag_lck.entry(tag_hash).or_insert_with(|| t.to_string());
labels.push(tag_hash); labels.insert(tag_hash);
} }
} }
} }
@ -240,7 +240,7 @@ impl Store {
drop(tag_lck); drop(tag_lck);
let mut ret: Envelope = obj.into(); let mut ret: Envelope = obj.into();
ret.set_flags(flags); ret.set_flags(flags);
ret.tags_mut().append(&mut labels); ret.tags_mut().extend(labels);
let mut id_store_lck = self.id_store.lock().unwrap(); let mut id_store_lck = self.id_store.lock().unwrap();
let mut reverse_id_store_lck = self.reverse_id_store.lock().unwrap(); let mut reverse_id_store_lck = self.reverse_id_store.lock().unwrap();
@ -760,7 +760,7 @@ impl MailBackend for JmapType {
&mut self, &mut self,
env_hashes: EnvelopeHashBatch, env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash, mailbox_hash: MailboxHash,
flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>, flags: SmallVec<[FlagOp; 8]>,
) -> ResultFuture<()> { ) -> ResultFuture<()> {
let store = self.store.clone(); let store = self.store.clone();
let connection = self.connection.clone(); let connection = self.connection.clone();
@ -769,9 +769,9 @@ impl MailBackend for JmapType {
let mut ids: Vec<Id<EmailObject>> = Vec::with_capacity(env_hashes.rest.len() + 1); let mut ids: Vec<Id<EmailObject>> = Vec::with_capacity(env_hashes.rest.len() + 1);
let mut id_map: IndexMap<Id<EmailObject>, EnvelopeHash> = IndexMap::default(); let mut id_map: IndexMap<Id<EmailObject>, EnvelopeHash> = IndexMap::default();
let mut update_keywords: IndexMap<String, Value> = IndexMap::default(); let mut update_keywords: IndexMap<String, Value> = IndexMap::default();
for (flag, value) in flags.iter() { for op in flags.iter() {
match flag { match op {
Ok(f) => { FlagOp::Set(f) => {
update_keywords.insert( update_keywords.insert(
format!( format!(
"keywords/{}", "keywords/{}",
@ -785,23 +785,32 @@ impl MailBackend for JmapType {
_ => continue, // [ref:VERIFY] _ => continue, // [ref:VERIFY]
} }
), ),
if *value { serde_json::json!(true),
serde_json::json!(true)
} else {
serde_json::json!(null)
},
); );
} }
Err(t) => { FlagOp::UnSet(f) => {
update_keywords.insert( update_keywords.insert(
format!("keywords/{}", t), format!(
if *value { "keywords/{}",
serde_json::json!(true) match *f {
} else { Flag::DRAFT => "$draft",
serde_json::json!(null) Flag::FLAGGED => "$flagged",
}, Flag::SEEN => "$seen",
Flag::REPLIED => "$answered",
Flag::TRASHED => "$junk",
Flag::PASSED => "$passed",
_ => continue, // [ref:VERIFY]
}
),
serde_json::json!(null),
); );
} }
FlagOp::SetTag(t) => {
update_keywords.insert(format!("keywords/{}", t), serde_json::json!(true));
}
FlagOp::UnSetTag(t) => {
update_keywords.insert(format!("keywords/{}", t), serde_json::json!(null));
}
} }
} }
{ {
@ -866,14 +875,9 @@ impl MailBackend for JmapType {
{ {
let mut tag_index_lck = store.collection.tag_index.write().unwrap(); let mut tag_index_lck = store.collection.tag_index.write().unwrap();
for (flag, value) in flags.iter() { for op in flags.iter() {
match flag { if let FlagOp::SetTag(t) = op {
Ok(_) => {} tag_index_lck.insert(TagHash::from_bytes(t.as_bytes()), t.clone());
Err(t) => {
if *value {
tag_index_lck.insert(TagHash::from_bytes(t.as_bytes()), t.clone());
}
}
} }
} }
drop(tag_index_lck); drop(tag_index_lck);

@ -809,10 +809,10 @@ impl MailBackend for MaildirType {
&mut self, &mut self,
env_hashes: EnvelopeHashBatch, env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash, mailbox_hash: MailboxHash,
flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>, flags: SmallVec<[FlagOp; 8]>,
) -> ResultFuture<()> { ) -> ResultFuture<()> {
let hash_index = self.hash_indexes.clone(); let hash_index = self.hash_indexes.clone();
if flags.iter().any(|(f, _)| f.is_err()) { if flags.iter().any(|op| op.is_tag()) {
return Err(Error::new("Maildir doesn't support tags.")); return Err(Error::new("Maildir doesn't support tags."));
} }
@ -841,8 +841,10 @@ impl MailBackend for MaildirType {
.ok_or_else(|| Error::new(format!("Invalid email filename: {:?}", path)))? .ok_or_else(|| Error::new(format!("Invalid email filename: {:?}", path)))?
+ 3; + 3;
let mut new_name: String = path[..idx].to_string(); let mut new_name: String = path[..idx].to_string();
for (f, value) in flags.iter() { for op in flags.iter() {
env_flags.set(*f.as_ref().unwrap(), *value); if let FlagOp::Set(f) | FlagOp::UnSet(f) = op {
env_flags.set(*f, op.as_bool());
}
} }
if !(env_flags & Flag::DRAFT).is_empty() { if !(env_flags & Flag::DRAFT).is_empty() {
@ -867,9 +869,9 @@ impl MailBackend for MaildirType {
hash_index.entry(env_hash).or_default().modified = hash_index.entry(env_hash).or_default().modified =
Some(PathMod::Path(new_name.clone())); Some(PathMod::Path(new_name.clone()));
debug!("renaming {:?} to {:?}", path, new_name); log::debug!("renaming {:?} to {:?}", path, new_name);
fs::rename(path, &new_name)?; fs::rename(path, &new_name)?;
debug!("success in rename"); log::debug!("success in rename");
} }
Ok(()) Ok(())
})) }))

@ -1152,7 +1152,7 @@ impl MailBackend for MboxType {
&mut self, &mut self,
_env_hashes: EnvelopeHashBatch, _env_hashes: EnvelopeHashBatch,
_mailbox_hash: MailboxHash, _mailbox_hash: MailboxHash,
_flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>, _flags: SmallVec<[FlagOp; 8]>,
) -> ResultFuture<()> { ) -> ResultFuture<()> {
Err(Error::new( Err(Error::new(
"Setting flags is currently unimplemented for mbox backend", "Setting flags is currently unimplemented for mbox backend",

@ -438,7 +438,7 @@ impl MailBackend for NntpType {
&mut self, &mut self,
env_hashes: EnvelopeHashBatch, env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash, mailbox_hash: MailboxHash,
flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>, flags: SmallVec<[FlagOp; 8]>,
) -> ResultFuture<()> { ) -> ResultFuture<()> {
let uid_store = self.uid_store.clone(); let uid_store = self.uid_store.clone();
Ok(Box::pin(async move { Ok(Box::pin(async move {
@ -461,11 +461,11 @@ impl MailBackend for NntpType {
let fsets = &uid_store.mailboxes.lock().await[&mailbox_hash]; let fsets = &uid_store.mailboxes.lock().await[&mailbox_hash];
let store_lck = uid_store.store.lock().await; let store_lck = uid_store.store.lock().await;
if let Some(s) = store_lck.as_ref() { if let Some(s) = store_lck.as_ref() {
for (flag, on) in flags { for op in flags {
if let Ok(f) = flag { if let FlagOp::Set(f) | FlagOp::UnSet(f) = op {
for (env_hash, uid) in &uids { for (env_hash, uid) in &uids {
let mut current_val = s.flags(*env_hash, mailbox_hash, *uid)?; let mut current_val = s.flags(*env_hash, mailbox_hash, *uid)?;
current_val.set(f, on); current_val.set(f, <bool>::from(&op));
if !current_val.intersects(Flag::SEEN) { if !current_val.intersects(Flag::SEEN) {
fsets.unseen.lock().unwrap().insert_new(*env_hash); fsets.unseen.lock().unwrap().insert_new(*env_hash);
} else { } else {

@ -101,7 +101,7 @@ impl<'m> Message<'m> {
for tag in tags { for tag in tags {
let num = TagHash::from_bytes(tag.as_bytes()); let num = TagHash::from_bytes(tag.as_bytes());
tag_lock.entry(num).or_insert(tag); tag_lock.entry(num).or_insert(tag);
env.tags_mut().push(num); env.tags_mut().insert(num);
} }
unsafe { unsafe {
use crate::email::parser::address::rfc2822address_list; use crate::email::parser::address::rfc2822address_list;

@ -850,7 +850,7 @@ impl MailBackend for NotmuchDb {
&mut self, &mut self,
env_hashes: EnvelopeHashBatch, env_hashes: EnvelopeHashBatch,
_mailbox_hash: MailboxHash, _mailbox_hash: MailboxHash,
flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>, flags: SmallVec<[FlagOp; 8]>,
) -> ResultFuture<()> { ) -> ResultFuture<()> {
let database = Self::new_connection( let database = Self::new_connection(
self.path.as_path(), self.path.as_path(),
@ -906,32 +906,29 @@ impl MailBackend for NotmuchDb {
}}; }};
} }
for (f, v) in flags.iter() { for op in flags.iter() {
let value = *v; match op {
debug!(&f); FlagOp::Set(Flag::DRAFT) => add_tag!(b"draft\0"),
debug!(&value); FlagOp::UnSet(Flag::DRAFT) => remove_tag!(b"draft\0"),
match f { FlagOp::Set(Flag::FLAGGED) => add_tag!(b"flagged\0"),
Ok(Flag::DRAFT) if value => add_tag!(b"draft\0"), FlagOp::UnSet(Flag::FLAGGED) => remove_tag!(b"flagged\0"),
Ok(Flag::DRAFT) => remove_tag!(b"draft\0"), FlagOp::Set(Flag::PASSED) => add_tag!(b"passed\0"),
Ok(Flag::FLAGGED) if value => add_tag!(b"flagged\0"), FlagOp::UnSet(Flag::PASSED) => remove_tag!(b"passed\0"),
Ok(Flag::FLAGGED) => remove_tag!(b"flagged\0"), FlagOp::Set(Flag::REPLIED) => add_tag!(b"replied\0"),
Ok(Flag::PASSED) if value => add_tag!(b"passed\0"), FlagOp::UnSet(Flag::REPLIED) => remove_tag!(b"replied\0"),
Ok(Flag::PASSED) => remove_tag!(b"passed\0"), FlagOp::Set(Flag::SEEN) => remove_tag!(b"unread\0"),
Ok(Flag::REPLIED) if value => add_tag!(b"replied\0"), FlagOp::UnSet(Flag::SEEN) => add_tag!(b"unread\0"),
Ok(Flag::REPLIED) => remove_tag!(b"replied\0"), FlagOp::Set(Flag::TRASHED) => add_tag!(b"trashed\0"),
Ok(Flag::SEEN) if value => remove_tag!(b"unread\0"), FlagOp::UnSet(Flag::TRASHED) => remove_tag!(b"trashed\0"),
Ok(Flag::SEEN) => add_tag!(b"unread\0"), FlagOp::SetTag(tag) => {
Ok(Flag::TRASHED) if value => add_tag!(b"trashed\0"),
Ok(Flag::TRASHED) => remove_tag!(b"trashed\0"),
Ok(_) => debug!("flags is {:?} value = {}", f, value),
Err(tag) if value => {
let c_tag = CString::new(tag.as_str()).unwrap(); let c_tag = CString::new(tag.as_str()).unwrap();
add_tag!(&c_tag.as_ref()); add_tag!(&c_tag.as_ref());
} }
Err(tag) => { FlagOp::UnSetTag(tag) => {
let c_tag = CString::new(tag.as_str()).unwrap(); let c_tag = CString::new(tag.as_str()).unwrap();
remove_tag!(&c_tag.as_ref()); remove_tag!(&c_tag.as_ref());
} }
_ => debug!("flag_op is {:?}", op),
} }
} }
@ -943,8 +940,8 @@ impl MailBackend for NotmuchDb {
*p = msg_id.into(); *p = msg_id.into();
} }
} }
for (f, v) in flags.iter() { for op in flags.iter() {
if let (Err(tag), true) = (f, v) { if let FlagOp::SetTag(tag) = op {
let hash = TagHash::from_bytes(tag.as_bytes()); let hash = TagHash::from_bytes(tag.as_bytes());
tag_index.write().unwrap().insert(hash, tag.to_string()); tag_index.write().unwrap().insert(hash, tag.to_string());
} }

Loading…
Cancel
Save