Files
inbuxa-server/crates/services/src/task_manager/destroy_account.rs
T
jcoffey-dev b2ded0a776 Merge upstream v0.16.23
Five conflicts, resolved:

- crates/common/src/auth/authentication.rs: upstream's get_directory_for_token
  and JwtClaims replace extract_jwt_domain; the per-domain directory code
  (DIR-1, DIR-5 to DIR-7) is kept, and the token lookup routes through it.
  The release's one new Enterprise snippet was the body of
  get_directory_for_issuer, which stays returning None: a token naming no
  address gets the server default, as DIR-2 specifies and as v0.16.22 did.
- crates/common/src/manager/application.rs: upstream's rewrite of the tests,
  with the temp directory names renamed again, and the 5(a) notice the
  name-purge change should have added.
- crates/common/src/network/mta.rs: both sides' imports.
- crates/main/Cargo.toml: the AGPL-only license kept, version 0.16.23.
- Cargo.lock: upstream's, with the fork's crates added by Cargo.
2026-09-22 16:57:06 -07:00

247 lines
7.8 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use crate::task_manager::{TaskResult, deferred_retry_time};
use common::Server;
use email::{message::metadata::MessageMetadata, sieve::SieveScript};
use groupware::file::FileNode;
use registry::{
schema::{
prelude::{ObjectType, Property},
structs::{ArchivedItem, TaskDestroyAccount},
},
types::EnumImpl,
};
use store::{
SerializeInfallible, ValueKey,
registry::RegistryQuery,
search::SearchQuery,
write::{BatchBuilder, BlobLink, BlobOp, RegistryClass, SearchIndex, ValueClass},
};
use trc::AddContext;
use types::{
blob_hash::BlobHash,
collection::Collection,
field::{EmailField, Field},
id::Id,
};
pub(crate) trait DestroyAccountTask: Sync + Send {
fn destroy_account(&self, task: &TaskDestroyAccount)
-> impl Future<Output = TaskResult> + Send;
}
impl DestroyAccountTask for Server {
async fn destroy_account(&self, task: &TaskDestroyAccount) -> TaskResult {
match destroy_account(self, task).await {
Ok(result) => result,
Err(err) => {
let result = TaskResult::deferred(deferred_retry_time(&err), err.to_string());
trc::error!(
err.account_id(task.account_id.document_id())
.details("Failed to destroy account")
);
result
}
}
}
}
async fn destroy_account(server: &Server, task: &TaskDestroyAccount) -> trc::Result<TaskResult> {
let account_id = task.account_id.document_id();
// Destroy public keys and masked emails
for object in [ObjectType::PublicKey, ObjectType::MaskedEmail] {
let mut batch = BatchBuilder::new();
let ids = server
.registry()
.query::<Vec<Id>>(RegistryQuery::new(object).with_account(account_id))
.await?;
let object_id = object.to_id();
for id in ids {
batch
.clear(ValueClass::Registry(RegistryClass::Item {
object_id,
item_id: id.id(),
}))
.clear(ValueClass::Registry(RegistryClass::IndexId {
object_id,
item_id: id.id(),
}))
.clear(ValueClass::Registry(RegistryClass::Index {
index_id: Property::AccountId as u16,
object_id,
item_id: id.id(),
key: (account_id as u64).serialize(),
}))
.clear(ValueClass::Registry(RegistryClass::Reference {
to_object_id: ObjectType::Account as u16,
to_item_id: account_id as u64,
from_object_id: object_id,
from_item_id: id.id(),
}));
}
if !batch.is_empty() {
server.store().write(batch.build_all()).await?;
}
}
// inbuxa: UD-15: the account's hold and undelete's own records go first
inbuxa_features::undelete::accounts::forget(
&server.core.storage.data,
server.registry(),
account_id,
)
.await?;
// Remove archived items
let mut batch = BatchBuilder::new();
let ids = server
.registry()
.query::<Vec<Id>>(RegistryQuery::new(ObjectType::ArchivedItem).with_account(account_id))
.await?;
for id in ids {
let object_id = ObjectType::ArchivedItem.to_id();
let item_id = id.id();
if let Some(item) = server
.store()
.get_value::<ArchivedItem>(ValueKey::from(ValueClass::Registry(RegistryClass::Item {
object_id,
item_id,
})))
.await?
{
let until = item.archived_until().timestamp() as u64;
let blob_hash = item.into_blob_id().hash;
batch
.with_account_id(account_id)
.clear(BlobOp::Link {
hash: blob_hash,
to: BlobLink::Temporary { until },
})
.clear(ValueClass::Registry(RegistryClass::Index {
index_id: Property::AccountId.to_id(),
object_id,
item_id,
key: (account_id as u64).serialize(),
}))
.clear(ValueClass::Registry(RegistryClass::Item {
object_id,
item_id,
}));
}
}
if !batch.is_empty() {
server.store().write(batch.build_all()).await?;
}
// Remove search index
for index in [
SearchIndex::Email,
SearchIndex::Contacts,
SearchIndex::Calendar,
] {
server
.search_store()
.unindex(SearchQuery::new(index).with_account_id(account_id))
.await?;
}
// Unlink all accounts's blobs
destroy_account_blobs(server, account_id).await?;
// Destroy account data
server
.store()
.danger_destroy_account(account_id)
.await
.caused_by(trc::location!())?;
Ok(TaskResult::Success(vec![]))
}
pub async fn destroy_account_blobs(server: &Server, account_id: u32) -> trc::Result<()> {
let mut delete_keys = Vec::new();
for (collection, field) in [
(Collection::Email, u8::from(EmailField::Metadata)),
(Collection::FileNode, u8::from(Field::ARCHIVE)),
(Collection::SieveScript, u8::from(Field::ARCHIVE)),
] {
server
.all_archives(account_id, collection, field, |document_id, archive| {
match collection {
Collection::Email => {
let message = archive.unarchive::<MessageMetadata>()?;
delete_keys.push((
collection,
document_id,
BlobHash::from(&message.blob_hash),
));
}
Collection::FileNode => {
if let Some(file) = archive.unarchive::<FileNode>()?.file.as_ref() {
delete_keys.push((
collection,
document_id,
BlobHash::from(&file.blob_hash),
));
}
}
Collection::SieveScript => {
let sieve = archive.unarchive::<SieveScript>()?;
delete_keys.push((
collection,
document_id,
BlobHash::from(&sieve.blob_hash),
));
}
_ => {}
}
Ok(())
})
.await
.caused_by(trc::location!())?;
}
let mut batch = BatchBuilder::new();
batch.with_account_id(account_id);
for (collection, document_id, hash) in delete_keys {
if batch.is_large_batch() {
server
.store()
.write(batch.build_all())
.await
.caused_by(trc::location!())?;
batch = BatchBuilder::new();
batch.with_account_id(account_id);
}
batch
.with_collection(collection)
.with_document(document_id)
.clear(ValueClass::Blob(BlobOp::Link {
hash,
to: BlobLink::Document,
}));
}
if !batch.is_empty() {
server
.store()
.write(batch.build_all())
.await
.caused_by(trc::location!())?;
}
Ok(())
}