Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "migration"
version = "0.16.22"
edition = "2024"
[dependencies]
store = { path = "../store" }
trc = { path = "../trc" }
types = { path = "../types" }
common = { path = "../common" }
email = { path = "../email" }
spam-filter = { path = "../spam-filter" }
registry = { path = "../registry" }
tokio = { version = "1.53", features = ["net", "macros"] }
[features]
test_mode = []
enterprise = []
[dev-dependencies]
tokio = { version = "1.53", features = ["full"] }
[lints]
workspace = true
+29
View File
@@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use store::{
Store, U64_LEN,
write::{AnyKey, key::KeySerializer},
};
use trc::AddContext;
pub async fn destroy_subspace(store: &Store, subspace: u8) -> trc::Result<()> {
store
.delete_range(
AnyKey {
subspace,
key: KeySerializer::new(U64_LEN).write(0u8).finalize(),
},
AnyKey {
subspace,
key: KeySerializer::new(U64_LEN)
.write(&[u8::MAX; 64][..])
.finalize(),
},
)
.await
.caused_by(trc::location!())
}
+136
View File
@@ -0,0 +1,136 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
#![warn(clippy::large_futures)]
use crate::v016::migrate_v0_16;
use common::{DATABASE_SCHEMA_VERSION, Server};
use store::{
IterateParams, SUBSPACE_PROPERTY, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_REPORT_IN,
SUBSPACE_REPORT_OUT, SerializeInfallible,
write::{AnyClass, AnyKey, BatchBuilder, ValueClass},
};
use trc::AddContext;
pub mod destroy;
pub mod v016;
pub async fn try_migrate(server: &Server) -> trc::Result<()> {
match server
.store()
.get_value::<u32>(AnyKey {
subspace: SUBSPACE_PROPERTY,
key: vec![0u8],
})
.await
.caused_by(trc::location!())?
{
Some(DATABASE_SCHEMA_VERSION) => {
if !std::env::var("DANGER_FORCE_MIGRATE").is_ok_and(|v| v == "1") {
return Ok(());
}
}
Some(0..=4) => {
abort(concat!(
"You must first upgrade to version 0.15, please read ",
"https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md"
));
}
Some(5) => {
if !server.registry().is_recovery_mode() {
abort(concat!(
"Upgrading to version 0.16 is a multi-step process, please read ",
"https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md"
));
}
}
Some(version) => {
panic!(
"Unknown database schema version, expected {} or below, found {}",
DATABASE_SCHEMA_VERSION, version
);
}
_ => {
if is_new_install(server).await.caused_by(trc::location!())? {
write_schema_version(server).await?;
return Ok(());
} else {
abort(concat!(
"You must first upgrade to version 0.15, please read ",
"https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md"
));
}
}
}
migrate_v0_16(server).await?;
write_schema_version(server).await
}
async fn write_schema_version(server: &Server) -> trc::Result<()> {
let mut batch = BatchBuilder::new();
batch.set(
ValueClass::Any(AnyClass {
subspace: SUBSPACE_PROPERTY,
key: vec![0u8],
}),
DATABASE_SCHEMA_VERSION.serialize(),
);
server
.store()
.write(batch.build_all())
.await
.caused_by(trc::location!())?;
Ok(())
}
fn abort(message: &str) -> ! {
eprintln!("Migration aborted: {message}");
panic!("Migration aborted: {message}");
}
async fn is_new_install(server: &Server) -> trc::Result<bool> {
for subspace in [
SUBSPACE_QUEUE_MESSAGE,
SUBSPACE_REPORT_IN,
SUBSPACE_REPORT_OUT,
SUBSPACE_PROPERTY,
] {
let mut has_data = false;
server
.store()
.iterate(
IterateParams::new(
AnyKey {
subspace,
key: vec![0u8],
},
AnyKey {
subspace,
key: vec![u8::MAX; 16],
},
)
.no_values(),
|_, _| {
has_data = true;
Ok(false)
},
)
.await
.caused_by(trc::location!())?;
if has_data {
return Ok(false);
}
}
Ok(true)
}
+299
View File
@@ -0,0 +1,299 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::destroy::destroy_subspace;
use common::{Server, manager::SPAM_TRAINER_KEY};
use registry::{
schema::{
prelude::{ObjectType, Property},
structs::{ArchivedEmail, ArchivedItem},
},
types::{EnumImpl, ObjectImpl, datetime::UTCDateTime, id::ObjectId},
};
use spam_filter::modules::classifier::SpamTrainer;
use store::{
Deserialize, IterateParams, SUBSPACE_BLOB_LINK, SUBSPACE_DIRECTORY, SUBSPACE_QUOTA,
SUBSPACE_REPORT_IN, SUBSPACE_REPORT_OUT, SUBSPACE_TASK_QUEUE, SUBSPACE_TELEMETRY_METRIC,
SUBSPACE_TELEMETRY_SPAN, Serialize, SerializeInfallible, U32_LEN, U64_LEN,
search::{SearchField, SearchFilter, SearchQuery},
write::{
AlignedBytes, AnyClass, AnyKey, Archive, Archiver, BatchBuilder, RegistryClass,
SearchIndex, ValueClass, key::DeserializeBigEndian, now,
},
};
use trc::AddContext;
use types::{blob::BlobId, blob_hash::BLOB_HASH_LEN};
const LEGACY_SUBSPACE_BLOB_EXTRA: u8 = b'j'; // Now SUBSPACE_DELETED_ITEMS
const LEGACY_SUBSPACE_BITMAP_ID: u8 = b'b'; // Now SUBSPACE_REGISTRY_IDX
const LEGACY_SUBSPACE_SETTINGS: u8 = b's'; // Now SUBSPACE_REGISTRY
const LEGACY_SUBSPACE_FTS_INDEX: u8 = b'g'; // Now SUBSPACE_REGISTRY_PK
const LEGACY_SUBSPACE_TELEMETRY_INDEX: u8 = b'w'; // Now SUBSPACE_SPAM_SAMPLES
pub async fn migrate_v0_16(server: &Server) -> trc::Result<()> {
// Delete tracing index
server
.search_store()
.unindex(
SearchQuery::new(SearchIndex::Tracing)
.with_filter(SearchFilter::lt(SearchField::Id, u64::MAX)),
)
.await
.caused_by(trc::location!())?;
// Delete old quotas
server
.store()
.delete_range(
AnyKey {
subspace: SUBSPACE_QUOTA,
key: vec![0x04],
},
AnyKey {
subspace: SUBSPACE_QUOTA,
key: vec![0x05],
},
)
.await
.caused_by(trc::location!())?;
// Destroy old and incompatible subspaces
for namespace in [
LEGACY_SUBSPACE_BLOB_EXTRA,
LEGACY_SUBSPACE_TELEMETRY_INDEX,
LEGACY_SUBSPACE_SETTINGS,
LEGACY_SUBSPACE_BITMAP_ID,
LEGACY_SUBSPACE_FTS_INDEX,
SUBSPACE_REPORT_IN,
SUBSPACE_REPORT_OUT,
SUBSPACE_DIRECTORY,
SUBSPACE_TELEMETRY_METRIC,
SUBSPACE_TELEMETRY_SPAN,
SUBSPACE_TASK_QUEUE,
] {
destroy_subspace(server.store(), namespace)
.await
.caused_by(trc::location!())?;
}
// Migrate blob links
migrate_blob_links(server).await?;
// Migrate spam model
migrate_spam_model(server).await?;
Ok(())
}
async fn migrate_spam_model(server: &Server) -> trc::Result<()> {
let Some(mut trainer) = server
.blob_store()
.get_blob(SPAM_TRAINER_KEY, 0..usize::MAX)
.await
.and_then(|archive| match archive {
Some(archive) => <Archive<AlignedBytes> as Deserialize>::deserialize(&archive)
.and_then(|archive| archive.deserialize_untrusted::<SpamTrainer>())
.map(Some),
None => Ok(None),
})
.caused_by(trc::location!())?
else {
return Ok(());
};
if trainer.last_id == 0 {
return Ok(());
}
if let Some(config) = &server.core.spam.classifier {
trainer.reservoir.ham.total_seen = std::cmp::min(
config.min_ham_samples + config.reservoir_capacity as u64,
trainer.reservoir.ham.total_seen,
);
trainer.reservoir.spam.total_seen = std::cmp::min(
config.min_spam_samples + config.reservoir_capacity as u64,
trainer.reservoir.spam.total_seen,
);
} else {
trainer.reservoir.ham.total_seen = std::cmp::min(
trainer.reservoir.ham.buffer.len() as u64,
trainer.reservoir.ham.total_seen,
);
trainer.reservoir.spam.total_seen = std::cmp::min(
trainer.reservoir.spam.buffer.len() as u64,
trainer.reservoir.spam.total_seen,
);
}
trainer.reservoir.ham.buffer.clear();
trainer.reservoir.spam.buffer.clear();
trainer.last_id = 0;
server
.blob_store()
.put_blob(
SPAM_TRAINER_KEY,
&Archiver::new(trainer)
.serialize()
.caused_by(trc::location!())?,
server.core.email.compression,
)
.await
.caused_by(trc::location!())?;
Ok(())
}
async fn migrate_blob_links(server: &Server) -> trc::Result<()> {
let mut delete_keys = Vec::new();
let mut archived_items = Vec::new();
let now = now();
server
.store()
.iterate(
IterateParams::new(
AnyKey {
subspace: SUBSPACE_BLOB_LINK,
key: vec![0u8],
},
AnyKey {
subspace: SUBSPACE_BLOB_LINK,
key: vec![u8::MAX; 32],
},
),
|key, value| {
const TEMP_LINK: usize = BLOB_HASH_LEN + U32_LEN + U64_LEN;
const QUOTA_LINK: u8 = 0;
const UNDELETE_LINK: u8 = 1;
const SPAM_SAMPLE_LINK: u8 = 2;
if key.len() == TEMP_LINK && value.len() == 1 {
let until = key.deserialize_be_u64(BLOB_HASH_LEN + U32_LEN)?;
if until > now {
let account_id = key.deserialize_be_u32(BLOB_HASH_LEN)?;
let hash = types::blob_hash::BlobHash::try_from_hash_slice(
key.get(0..BLOB_HASH_LEN).ok_or_else(|| {
trc::Error::corrupted_key(key, None, trc::location!())
})?,
)
.unwrap();
match value.first().copied() {
Some(UNDELETE_LINK) => {
archived_items.push((key.to_vec(), account_id, hash, until));
}
Some(SPAM_SAMPLE_LINK | QUOTA_LINK) => {
delete_keys.push(key.to_vec());
}
_ => {}
}
}
}
Ok(true)
},
)
.await
.caused_by(trc::location!())?;
// Delete spam samples and quota links
let mut batch = BatchBuilder::new();
for key in delete_keys {
batch.clear(ValueClass::Any(AnyClass {
subspace: SUBSPACE_BLOB_LINK,
key,
}));
if batch.is_large_batch() {
server
.store()
.write(batch.build_all())
.await
.caused_by(trc::location!())?;
batch = BatchBuilder::new();
}
}
if !batch.is_empty() {
server
.store()
.write(batch.build_all())
.await
.caused_by(trc::location!())?;
}
// Migrate spam samples
let mut batch = BatchBuilder::new();
let id_gen = &server.inner.data.registry_id_gen;
let mut last_id = 0;
for (key, account_id, blob_hash, until) in archived_items {
let item = ArchivedItem::Email(ArchivedEmail {
account_id: account_id.into(),
blob_id: BlobId::new(blob_hash.clone(), Default::default()),
archived_until: UTCDateTime::from_timestamp(until as i64),
archived_at: UTCDateTime::now(),
from: "Unavailable".to_string(),
received_at: UTCDateTime::now(),
subject: "...".to_string(),
size: 0,
})
.to_pickled_vec();
let object_id = ObjectType::ArchivedItem.to_id();
loop {
let new_id = id_gen.generate();
if new_id != last_id {
last_id = new_id;
break;
} else {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
}
let item_id = last_id;
batch
.set(
ValueClass::Any(AnyClass {
subspace: SUBSPACE_BLOB_LINK,
key,
}),
ObjectId::new(ObjectType::ArchivedItem, item_id.into()).serialize(),
)
.set(
ValueClass::Registry(RegistryClass::Index {
index_id: Property::AccountId.to_id(),
object_id,
item_id,
key: (account_id as u64).serialize(),
}),
vec![],
)
.set(
ValueClass::Registry(RegistryClass::Item { object_id, item_id }),
item,
);
if batch.is_large_batch() {
server
.store()
.write(batch.build_all())
.await
.caused_by(trc::location!())?;
batch = BatchBuilder::new();
}
}
if !batch.is_empty() {
server
.store()
.write(batch.build_all())
.await
.caused_by(trc::location!())?;
}
Ok(())
}