Files
Stalwart/crates/migration/src/sieve.rs
2025-09-22 10:01:31 +02:00

230 lines
7.4 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::object::Object;
use crate::object::{Property, TryFromLegacy, Value};
use common::Server;
use email::sieve::{SieveScript, VacationResponse};
use store::{
SUBSPACE_BITMAP_TEXT, SUBSPACE_INDEXES, SUBSPACE_PROPERTY, Serialize, U64_LEN, ValueKey,
write::{
AlignedBytes, AnyKey, Archive, Archiver, BatchBuilder, ValueClass, key::KeySerializer,
},
};
use trc::{AddContext, StoreEvent};
use types::{
collection::Collection,
field::{Field, SieveField},
};
pub(crate) async fn migrate_sieve(server: &Server, account_id: u32) -> trc::Result<u64> {
// Obtain email ids
let script_ids = server
.get_document_ids(account_id, Collection::SieveScript)
.await
.caused_by(trc::location!())?
.unwrap_or_default();
let num_scripts = script_ids.len();
if num_scripts == 0 {
return Ok(0);
}
let mut did_migrate = false;
// Delete indexes
for subspace in [SUBSPACE_INDEXES, SUBSPACE_BITMAP_TEXT] {
server
.store()
.delete_range(
AnyKey {
subspace,
key: KeySerializer::new(U64_LEN)
.write(account_id)
.write(u8::from(Collection::SieveScript))
.finalize(),
},
AnyKey {
subspace,
key: KeySerializer::new(U64_LEN)
.write(account_id)
.write(u8::from(Collection::SieveScript))
.write(&[u8::MAX; 16][..])
.finalize(),
},
)
.await
.caused_by(trc::location!())?;
}
for script_id in &script_ids {
match server
.store()
.get_value::<Object<Value>>(ValueKey {
account_id,
collection: Collection::SieveScript.into(),
document_id: script_id,
class: ValueClass::Property(Field::ARCHIVE.into()),
})
.await
{
Ok(Some(legacy)) => {
if let Some(script) = SieveScript::try_from_legacy(legacy) {
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::SieveScript)
.update_document(script_id)
.index(
SieveField::IsActive,
if script.is_active {
vec![1u8]
} else {
vec![0u8]
},
)
.index(SieveField::Name, script.name.to_lowercase())
.set(
Field::ARCHIVE,
Archiver::new(script)
.serialize()
.caused_by(trc::location!())?,
);
did_migrate = true;
server
.store()
.write(batch.build_all())
.await
.caused_by(trc::location!())?;
} else {
trc::event!(
Store(StoreEvent::DataCorruption),
Details = "Failed to migrate SieveScript",
AccountId = account_id,
)
}
}
Ok(None) => (),
Err(err) => {
if server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey {
account_id,
collection: Collection::SieveScript.into(),
document_id: script_id,
class: ValueClass::Property(Field::ARCHIVE.into()),
})
.await
.is_err()
{
return Err(err
.account_id(account_id)
.document_id(script_id)
.caused_by(trc::location!()));
}
}
}
}
// Delete emailIds property
server
.store()
.delete_range(
AnyKey {
subspace: SUBSPACE_PROPERTY,
key: KeySerializer::new(U64_LEN)
.write(account_id)
.write(u8::from(Collection::SieveScript))
.write(u8::from(SieveField::Ids))
.finalize(),
},
AnyKey {
subspace: SUBSPACE_PROPERTY,
key: KeySerializer::new(U64_LEN)
.write(account_id)
.write(u8::from(Collection::SieveScript))
.write(u8::from(SieveField::Ids))
.write(&[u8::MAX; 8][..])
.finalize(),
},
)
.await
.caused_by(trc::location!())?;
// Increment document id counter
if did_migrate {
server
.store()
.assign_document_ids(
account_id,
Collection::SieveScript,
script_ids.max().map(|id| id as u64).unwrap_or(num_scripts) + 1,
)
.await
.caused_by(trc::location!())?;
Ok(num_scripts)
} else {
Ok(0)
}
}
impl TryFromLegacy for SieveScript {
fn try_from_legacy(legacy: Object<Value>) -> Option<Self> {
let blob_id = legacy.get(&Property::BlobId).as_blob_id()?;
Some(SieveScript {
name: legacy
.get(&Property::Name)
.as_string()
.unwrap_or_default()
.to_string(),
is_active: legacy
.get(&Property::IsActive)
.as_bool()
.unwrap_or_default(),
blob_hash: blob_id.hash.clone(),
size: blob_id.section.as_ref()?.size as u32,
vacation_response: VacationResponse::try_from_legacy(legacy),
})
}
}
impl TryFromLegacy for VacationResponse {
fn try_from_legacy(legacy: Object<Value>) -> Option<Self> {
let vacation = VacationResponse {
from_date: legacy
.get(&Property::FromDate)
.as_date()
.map(|s| s.timestamp() as u64),
to_date: legacy
.get(&Property::ToDate)
.as_date()
.map(|s| s.timestamp() as u64),
subject: legacy
.get(&Property::Name)
.as_string()
.map(|s| s.to_string()),
text_body: legacy
.get(&Property::TextBody)
.as_string()
.map(|s| s.to_string()),
html_body: legacy
.get(&Property::HtmlBody)
.as_string()
.map(|s| s.to_string()),
};
if vacation.from_date.is_some()
|| vacation.to_date.is_some()
|| vacation.subject.is_some()
|| vacation.text_body.is_some()
|| vacation.html_body.is_some()
{
Some(vacation)
} else {
None
}
}
}