Fix JMAP: Thread/changes never emits a container delete when a thread becomes empty

This commit is contained in:
Maurus Decimus
2026-06-18 16:57:02 +02:00
parent 63284599d7
commit 64619e4e3e
4 changed files with 67 additions and 6 deletions

View File

@@ -7,7 +7,7 @@
use super::*;
use crate::{
cache::{MessageCacheFetch, email::MessageCacheAccess},
message::metadata::MessageData,
message::{delete::EmailDeletion, metadata::MessageData},
};
use common::{
Server, auth::AccessToken, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder,
@@ -88,6 +88,8 @@ impl MailboxDestroy for Server {
// If the message is in multiple mailboxes, untag it from the current mailbox,
// otherwise delete it.
let mut deleted_ids = RoaringBitmap::new();
let mut thread_ids = RoaringBitmap::new();
self.archives(
account_id,
Collection::Email,
@@ -114,6 +116,8 @@ impl MailboxDestroy for Server {
(mailbox.mailbox_id.to_native(), mailbox.uid.to_native()),
);
}
deleted_ids.insert(message_id);
thread_ids.insert(prev_message_data.inner.thread_id.to_native());
batch
.with_collection(Collection::Email)
.with_document(message_id)
@@ -168,6 +172,10 @@ impl MailboxDestroy for Server {
)
.await
.caused_by(trc::location!())?;
self.log_emptied_threads(account_id, &mut batch, thread_ids, &deleted_ids)
.await
.caused_by(trc::location!())?;
} else {
return Ok(Err(MailboxDestroyError::HasEmails));
}

View File

@@ -5,6 +5,7 @@
*/
use super::metadata::MessageData;
use crate::cache::{MessageCacheFetch, email::MessageCacheAccess};
use common::{Server, storage::index::ObjectIndexBuilder};
use groupware::calendar::storage::ItipAutoExpunge;
use registry::schema::enums::IndexDocumentType;
@@ -18,7 +19,7 @@ use store::{
write::{BatchBuilder, ValueClass},
};
use trc::AddContext;
use types::collection::{Collection, VanishedCollection};
use types::collection::{Collection, SyncCollection, VanishedCollection};
use types::field::{EmailField, EmailSubmissionField};
pub trait EmailDeletion: Sync + Send {
@@ -43,6 +44,14 @@ pub trait EmailDeletion: Sync + Send {
account_id: u32,
hold_period: u64,
) -> impl Future<Output = trc::Result<()>> + Send;
fn log_emptied_threads(
&self,
account_id: u32,
batch: &mut BatchBuilder,
thread_ids: RoaringBitmap,
deleted_ids: &RoaringBitmap,
) -> impl Future<Output = trc::Result<()>> + Send;
}
impl EmailDeletion for Server {
@@ -54,6 +63,7 @@ impl EmailDeletion for Server {
document_ids: RoaringBitmap,
) -> trc::Result<RoaringBitmap> {
let mut deleted_ids = RoaringBitmap::new();
let mut thread_ids = RoaringBitmap::new();
batch
.with_account_id(account_id)
.with_collection(Collection::Email);
@@ -72,6 +82,7 @@ impl EmailDeletion for Server {
(mailbox.mailbox_id.to_native(), mailbox.uid.to_native()),
);
}
thread_ids.insert(metadata.inner.thread_id.to_native());
batch
.with_document(document_id)
.custom(
@@ -95,6 +106,9 @@ impl EmailDeletion for Server {
)
.await?;
self.log_emptied_threads(account_id, batch, thread_ids, &deleted_ids)
.await?;
let not_destroyed = if document_ids.len() == deleted_ids.len() {
RoaringBitmap::new()
} else {
@@ -105,6 +119,35 @@ impl EmailDeletion for Server {
Ok(not_destroyed)
}
async fn log_emptied_threads(
&self,
account_id: u32,
batch: &mut BatchBuilder,
thread_ids: RoaringBitmap,
deleted_ids: &RoaringBitmap,
) -> trc::Result<()> {
if !thread_ids.is_empty() {
let cache = self
.get_cached_messages(account_id)
.await
.caused_by(trc::location!())?;
for thread_id in &thread_ids {
if cache
.in_thread(thread_id)
.all(|message| deleted_ids.contains(message.document_id))
{
batch
.with_account_id(account_id)
.with_collection(Collection::Thread)
.with_document(thread_id)
.log_container_delete(SyncCollection::Thread);
}
}
}
Ok(())
}
async fn purge_account(&self, account_id: u32) -> trc::Result<()> {
// Auto-expunge deleted and junk messages
if let Some(hold_period) = self.core.email.mail_autoexpunge_after {