/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ 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; use registry::schema::structs::{Task, TaskIndexDocument, TaskStatus}; use std::future::Future; use store::write::key::DeserializeBigEndian; use store::write::{IndexPropertyClass, now}; use store::{IterateParams, U32_LEN, U64_LEN, ValueKey}; use store::{ roaring::RoaringBitmap, write::{BatchBuilder, ValueClass}, }; use trc::AddContext; use types::collection::{Collection, SyncCollection, VanishedCollection}; use types::field::{EmailField, EmailSubmissionField}; pub trait EmailDeletion: Sync + Send { fn emails_delete( &self, account_id: u32, tenant_id: Option, batch: &mut BatchBuilder, document_ids: RoaringBitmap, ) -> impl Future> + Send; fn purge_account(&self, account_id: u32) -> impl Future> + Send; fn purge_email_submissions( &self, account_id: u32, hold_period: u64, ) -> impl Future> + Send; fn emails_auto_expunge( &self, account_id: u32, hold_period: u64, ) -> impl Future> + Send; fn log_emptied_threads( &self, account_id: u32, batch: &mut BatchBuilder, thread_ids: RoaringBitmap, deleted_ids: &RoaringBitmap, ) -> impl Future> + Send; } impl EmailDeletion for Server { async fn emails_delete( &self, account_id: u32, tenant_id: Option, batch: &mut BatchBuilder, document_ids: RoaringBitmap, ) -> trc::Result { let mut deleted_ids = RoaringBitmap::new(); let mut thread_ids = RoaringBitmap::new(); batch .with_account_id(account_id) .with_collection(Collection::Email); self.archives( account_id, Collection::Email, &document_ids, |document_id, data_| { // Add changes to batch let metadata = data_ .to_unarchived::() .caused_by(trc::location!())?; for mailbox in metadata.inner.mailboxes.iter() { batch.log_vanished_item( VanishedCollection::Email, (mailbox.mailbox_id.to_native(), mailbox.uid.to_native()), ); } thread_ids.insert(metadata.inner.thread_id.to_native()); batch .with_document(document_id) .custom( ObjectIndexBuilder::<_, ()>::new() .with_tenant_id(tenant_id) .with_current(metadata), ) .caused_by(trc::location!())? .schedule_task(Task::UnindexDocument(TaskIndexDocument { account_id: account_id.into(), document_id: document_id.into(), document_type: IndexDocumentType::Email, status: TaskStatus::now(), })) .commit_point(); deleted_ids.insert(document_id); Ok(true) }, ) .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 { deleted_ids ^= document_ids; deleted_ids }; 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 { self.emails_auto_expunge(account_id, hold_period) .await .caused_by(trc::location!())?; } // Auto-expunge iMIP messages if let Some(hold_period) = self.core.groupware.itip_inbox_auto_expunge { self.itip_auto_expunge(account_id, hold_period) .await .caused_by(trc::location!())?; } // Delete old e-mail submissions if let Some(hold_period) = self.core.email.email_submission_autoexpunge_after { self.purge_email_submissions(account_id, hold_period) .await .caused_by(trc::location!())?; } // Purge changelogs self.delete_changes( account_id, self.core.email.changes_max_history, self.core.email.share_notification_max_history, ) .await .caused_by(trc::location!())?; Ok(()) } async fn emails_auto_expunge(&self, account_id: u32, hold_period: u64) -> trc::Result<()> { // Filter messages by received date let mut destroy_ids = RoaringBitmap::new(); let cutoff = now().saturating_sub(hold_period); self.store() .iterate( IterateParams::new( ValueKey { account_id, collection: Collection::Email.into(), document_id: 0, class: ValueClass::Property(EmailField::DeletedAt.into()), }, ValueKey { account_id, collection: Collection::Email.into(), document_id: u32::MAX, class: ValueClass::Property(EmailField::DeletedAt.into()), }, ) .ascending(), |key, value| { let deleted_at = value.deserialize_be_u64(0)?; if deleted_at <= cutoff { destroy_ids.insert(key.deserialize_be_u32(key.len() - U32_LEN)?); } Ok(true) }, ) .await .caused_by(trc::location!())?; if destroy_ids.is_empty() { return Ok(()); } trc::event!( Store(trc::StoreEvent::AutoExpunge), Collection = Collection::Email.as_str(), AccountId = account_id, Total = destroy_ids.len(), ); // Delete messages let mut batch = BatchBuilder::new(); let tenant_id = self .account(account_id) .await .caused_by(trc::location!())? .tenant_id(); self.emails_delete(account_id, tenant_id, &mut batch, destroy_ids) .await?; self.commit_batch(batch).await?; self.notify_task_queue(); Ok(()) } async fn purge_email_submissions(&self, account_id: u32, hold_period: u64) -> trc::Result<()> { // Filter messages by received date let mut destroy_ids = Vec::new(); self.store() .iterate( IterateParams::new( ValueKey { account_id, collection: Collection::EmailSubmission.into(), document_id: 0, class: ValueClass::IndexProperty(IndexPropertyClass::Integer { property: EmailSubmissionField::Metadata.into(), value: 0, }), }, ValueKey { account_id, collection: Collection::Email.into(), document_id: u32::MAX, class: ValueClass::IndexProperty(IndexPropertyClass::Integer { property: EmailSubmissionField::Metadata.into(), value: now().saturating_sub(hold_period), }), }, ) .ascending() .no_values(), |key, _| { destroy_ids.push(( key.deserialize_be_u32(key.len() - U32_LEN)?, key.deserialize_be_u64(key.len() - U32_LEN - U64_LEN)?, )); Ok(true) }, ) .await .caused_by(trc::location!())?; if destroy_ids.is_empty() { return Ok(()); } trc::event!( Store(trc::StoreEvent::AutoExpunge), Collection = Collection::EmailSubmission.as_str(), AccountId = account_id, Total = destroy_ids.len(), ); // Delete messages let mut batch = BatchBuilder::new(); batch .with_account_id(account_id) .with_collection(Collection::EmailSubmission); for (document_id, send_at) in destroy_ids { batch .with_document(document_id) .clear(EmailSubmissionField::Metadata) .clear(ValueClass::IndexProperty(IndexPropertyClass::Integer { property: EmailSubmissionField::Metadata.into(), value: send_at, })) .commit_point(); } self.commit_batch(batch).await?; Ok(()) } }