Files
inbuxa-server/crates/common/src/manager/defaults.rs
T
jcoffey-dev 7285b3e38a
ci / fork-checks (pull_request) Successful in 15s
ci / build (pull_request) Successful in 7m33s
Merge main (upstream v0.16.24) into feature/compliance-roles
The schema conflicted as a binary file: taken from main and the one
edit here re-applied (sysComplianceGet after sysLegalHoldExport). The
import kept the permission count at 673, so the new id stays 673.

Retested on the merged tree in its own target directory: the
compliance and system suites pass. One earlier system run failed in
purge.rs (an imported blob not found) and didn't recur.
2026-09-28 09:26:27 -07:00

678 lines
25 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::auth::permissions::DefaultPermissions;
use aws_lc_rs::{
rand::SystemRandom,
signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair},
};
use registry::{
schema::{
enums::*,
prelude::{Object, ObjectType, SocketAddr},
structs::*,
},
types::{duration::Duration, error::Error, list::List, map::Map},
};
use std::str::FromStr;
use store::{
rand::{RngExt, distr::Alphanumeric, rng},
registry::{
bootstrap::Bootstrap,
write::{RegistryWrite, RegistryWriteResult},
},
};
pub const ASN_IPV4: &str =
"https://github.com/sapics/ip-location-db/releases/download/latest/origin-asn-ipv4.csv";
pub const ASN_IPV6: &str =
"https://github.com/sapics/ip-location-db/releases/download/latest/origin-asn-ipv6.csv";
pub const GEO_IPV4: &str =
"https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv4.csv";
pub const GEO_IPV6: &str =
"https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv6.csv";
pub trait BootstrapDefaults {
fn insert_safe_defaults(&mut self) -> impl Future<Output = ()> + Send;
}
impl BootstrapDefaults for Bootstrap {
async fn insert_safe_defaults(&mut self) {
if let Err(error) = insert_safe_defaults(self).await {
self.errors.push(Error::Internal {
object_id: None,
error,
});
}
}
}
async fn insert_safe_defaults(bp: &mut Bootstrap) -> trc::Result<()> {
let is_recovery_mode = bp.registry.is_recovery_mode();
let is_bootstrap_mode = bp.registry.is_bootstrap_mode();
// inbuxa: no web interface is installed on the mail host, and nothing is
// downloaded for one (docs/spec/SPEC.md §5.3). Administration is INBUXA
// Admin and webmail is ihasmail, both deployed separately. An install
// upgraded from Stalwart keeps any web application it already has.
if is_bootstrap_mode {
#[cfg(not(any(feature = "dev_mode", feature = "test_mode")))]
if bp.registry.count_object(ObjectType::SystemSettings).await? == 0 {
bp.registry
.write(RegistryWrite::insert(
&SystemSettings {
default_hostname: bp.registry.local_hostname().to_string(),
..Default::default()
}
.into(),
))
.await?;
}
return Ok(());
}
if is_recovery_mode {
return Ok(());
}
// inbuxa: registration is required (contract C-5), so the first-party
// front ends are registered on every start (C-6)
super::first_party::ensure_first_party_clients(bp).await?;
if bp.registry.count_object(ObjectType::MtaQueueQuota).await? == 0 {
bp.registry
.write(RegistryWrite::insert(
&MtaQueueQuota {
description: "Global queue quota".to_string().into(),
enable: true,
messages: 100000.into(),
size: 10737418240.into(),
..Default::default()
}
.into(),
))
.await?;
}
if bp
.registry
.count_object(ObjectType::MtaInboundThrottle)
.await?
== 0
{
for object in [
MtaInboundThrottle {
description: "Sender IP throttle".to_string(),
enable: true,
key: Map::new(vec![MtaInboundThrottleKey::RemoteIp]),
rate: Rate {
count: 5,
period: Duration::from_millis(1000),
},
..Default::default()
},
MtaInboundThrottle {
description: "Sender address to recipient throttle".to_string(),
enable: true,
key: Map::new(vec![
MtaInboundThrottleKey::SenderDomain,
MtaInboundThrottleKey::Rcpt,
]),
rate: Rate {
count: 25,
period: Duration::from_millis(60 * 60 * 1000),
},
..Default::default()
},
] {
bp.registry
.write(RegistryWrite::insert(&object.into()))
.await?;
}
}
if bp
.registry
.count_object(ObjectType::MtaVirtualQueue)
.await?
== 0
&& bp
.registry
.count_object(ObjectType::MtaDeliverySchedule)
.await?
== 0
{
for (id, object) in [
MtaVirtualQueue {
description: "Local delivery queue".to_string().into(),
name: "local".into(),
threads_per_node: 25,
},
MtaVirtualQueue {
description: "Remote delivery queue".to_string().into(),
name: "remote".into(),
threads_per_node: 50,
},
MtaVirtualQueue {
description: "Delivery Status Notification delivery queue"
.to_string()
.into(),
name: "dsn".into(),
threads_per_node: 5,
},
MtaVirtualQueue {
description: "DMARC and TLS report delivery queue".to_string().into(),
name: "report".into(),
threads_per_node: 5,
},
]
.into_iter()
.enumerate()
{
bp.registry
.write(RegistryWrite::insert_with_id(
(id as u64).into(),
&object.into(),
))
.await?;
}
for (id, object) in [
MtaDeliverySchedule {
name: "local".into(),
description: "Local delivery schedule".to_string().into(),
expiry: MtaDeliveryExpiration::Ttl(MtaDeliveryExpirationTtl {
expire: Duration::from_millis(3 * 24 * 60 * 60 * 1000),
}),
notify: MtaDeliveryScheduleIntervalsOrDefault::Default,
retry: MtaDeliveryScheduleIntervalsOrDefault::Default,
queue_id: 0u64.into(),
},
MtaDeliverySchedule {
name: "remote".into(),
description: "Remote delivery schedule".to_string().into(),
expiry: MtaDeliveryExpiration::Ttl(MtaDeliveryExpirationTtl {
expire: Duration::from_millis(3 * 24 * 60 * 60 * 1000),
}),
notify: MtaDeliveryScheduleIntervalsOrDefault::Default,
retry: MtaDeliveryScheduleIntervalsOrDefault::Default,
queue_id: 1u64.into(),
},
MtaDeliverySchedule {
name: "dsn".into(),
description: "Delivery Status Notification delivery schedule"
.to_string()
.into(),
expiry: MtaDeliveryExpiration::Attempts(MtaDeliveryExpirationAttempts {
max_attempts: 10,
}),
notify: MtaDeliveryScheduleIntervalsOrDefault::Default,
retry: MtaDeliveryScheduleIntervalsOrDefault::Custom(
MtaDeliveryScheduleIntervals {
intervals: List::from_iter([
MtaDeliveryScheduleInterval {
duration: Duration::from_millis(15 * 60 * 1000),
},
MtaDeliveryScheduleInterval {
duration: Duration::from_millis(30 * 60 * 1000),
},
MtaDeliveryScheduleInterval {
duration: Duration::from_millis(60 * 60 * 1000),
},
MtaDeliveryScheduleInterval {
duration: Duration::from_millis(2 * 60 * 60 * 1000),
},
]),
},
),
queue_id: 2u64.into(),
},
MtaDeliverySchedule {
name: "report".into(),
description: "DMARC and TLS report delivery schedule".to_string().into(),
expiry: MtaDeliveryExpiration::Attempts(MtaDeliveryExpirationAttempts {
max_attempts: 8,
}),
notify: MtaDeliveryScheduleIntervalsOrDefault::Default,
retry: MtaDeliveryScheduleIntervalsOrDefault::Custom(
MtaDeliveryScheduleIntervals {
intervals: List::from_iter([
MtaDeliveryScheduleInterval {
duration: Duration::from_millis(30 * 60 * 1000),
},
MtaDeliveryScheduleInterval {
duration: Duration::from_millis(60 * 60 * 1000),
},
MtaDeliveryScheduleInterval {
duration: Duration::from_millis(2 * 60 * 60 * 1000),
},
]),
},
),
queue_id: 3u64.into(),
},
]
.into_iter()
.enumerate()
{
bp.registry
.write(RegistryWrite::insert_with_id(
(id as u64).into(),
&object.into(),
))
.await?;
}
}
if bp.registry.count_object(ObjectType::MtaTlsStrategy).await? == 0 {
for object in [
MtaTlsStrategy {
name: "invalid-tls".into(),
description: "Allow invalid TLS certificates".to_string().into(),
allow_invalid_certs: true,
..Default::default()
},
MtaTlsStrategy {
name: "default".into(),
description: "Default TLS settings".to_string().into(),
allow_invalid_certs: false,
..Default::default()
},
] {
bp.registry
.write(RegistryWrite::insert(&object.into()))
.await?;
}
}
if bp.registry.count_object(ObjectType::MtaRoute).await? == 0 {
for object in [
MtaRoute::Mx(MtaRouteMx {
description: "MX delivery route".to_string().into(),
ip_lookup_strategy: MtaIpStrategy::V4ThenV6,
max_multihomed: 2,
max_mx_hosts: 2,
name: "mx".into(),
}),
MtaRoute::Local(MtaRouteCommon {
description: "Local delivery route".to_string().into(),
name: "local".into(),
}),
] {
bp.registry
.write(RegistryWrite::insert(&object.into()))
.await?;
}
}
if bp
.registry
.count_object(ObjectType::MtaConnectionStrategy)
.await?
== 0
{
bp.registry
.write(RegistryWrite::insert(
&MtaConnectionStrategy {
name: "default".into(),
description: "Default connection strategy".to_string().into(),
..Default::default()
}
.into(),
))
.await?;
}
if bp.registry.count_object(ObjectType::OidcProvider).await? == 0 {
let pkcs8_doc =
EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &SystemRandom::new())
.map_err(|err| {
trc::EventType::Server(trc::ServerEvent::Startup)
.into_err()
.reason(err)
.caused_by(trc::location!())
})?;
let signature_pem = pem::encode(&pem::Pem::new("PRIVATE KEY", pkcs8_doc.as_ref()));
bp.registry
.write(RegistryWrite::insert(
&OidcProvider {
encryption_key: SecretKey::Value(SecretKeyValue {
secret: rng()
.sample_iter(Alphanumeric)
.take(64)
.map(char::from)
.collect::<String>(),
}),
signature_key: SecretText::Text(SecretTextValue {
secret: signature_pem,
}),
signature_algorithm: JwtSignatureAlgorithm::Es256,
..Default::default()
}
.into(),
))
.await?;
// Generate a Web Push VAPID signing key (RFC 9749)
if bp.registry.count_object(ObjectType::Jmap).await? == 0 {
match crate::network::webpush::generate_pkcs8_pem() {
Ok(web_push_pem) => {
bp.registry
.write(RegistryWrite::insert(
&Jmap {
web_push_key: SecretTextOptional::Text(SecretTextValue {
secret: web_push_pem,
}),
..Default::default()
}
.into(),
))
.await?;
}
Err(err) => {
trc::event!(
Server(trc::ServerEvent::Startup),
Details = "Failed to generate Web Push VAPID key",
Reason = err
);
}
}
}
}
// inbuxa: personal-data catalog, defaults D2, D3, D4 and D6 (settled
// 2026-09-28): privacy-leaning values, for new installs only. A server
// with roles is not new, and keeps its settings whether saved or left at
// the default. Each singleton is read, changed and written back whole, so
// anything already in it stays.
#[cfg(not(feature = "test_mode"))]
if bp.registry.count_object(ObjectType::Role).await? == 0 {
let mut security = bp.setting_infallible::<Security>().await;
let mut classifier = bp.setting_infallible::<SpamClassifier>().await;
let mut pyzor = bp.setting_infallible::<SpamPyzor>().await;
let mut retention = bp.setting_infallible::<DataRetention>().await;
new_install_privacy_defaults(&mut security, &mut classifier, &mut pyzor, &mut retention);
for object in [
Object::from(security),
classifier.into(),
pyzor.into(),
retention.into(),
] {
bp.registry.write(RegistryWrite::insert(&object)).await?;
}
// D1: rotated log files are kept 30 days (a fork-owned setting,
// since x:TracerLog is also stored inside x:Bootstrap)
use inbuxa_features::security::log_files;
if !log_files::is_set(&bp.data_store).await? {
log_files::set(
&bp.data_store,
&log_files::LogSettings {
keep_for_days: Some(log_files::NEW_INSTALL_KEEP_DAYS),
},
)
.await?;
}
}
if bp.registry.count_object(ObjectType::Role).await? == 0 {
let permissions = DefaultPermissions::default();
let mut role_ids = Vec::with_capacity(4);
for role in [
Role {
description: "User".into(),
enabled_permissions: Map::new(permissions.user),
..Default::default()
},
Role {
description: "Group".into(),
enabled_permissions: Map::new(permissions.group),
..Default::default()
},
Role {
description: "Tenant Administrator".into(),
enabled_permissions: Map::new(permissions.tenant),
..Default::default()
},
Role {
description: "System Administrator".into(),
enabled_permissions: Map::new(permissions.superuser),
..Default::default()
},
] {
match bp
.registry
.write(RegistryWrite::insert(&role.into()))
.await?
{
RegistryWriteResult::Success(id) => role_ids.push(id),
err => {
bp.build_error(
ObjectType::Role.singleton(),
format!("Failed to insert default role: {err}"),
);
}
}
}
if bp.registry.count_object(ObjectType::Authentication).await? == 0 && role_ids.len() == 4 {
bp.registry
.write(RegistryWrite::insert(
&Authentication {
default_user_role_ids: Map::new(vec![role_ids[0]]),
default_group_role_ids: Map::new(vec![role_ids[1]]),
default_tenant_role_ids: Map::new(vec![role_ids[2], role_ids[0]]),
default_admin_role_ids: Map::new(vec![role_ids[3], role_ids[0]]),
..Default::default()
}
.into(),
))
.await?;
}
}
// inbuxa: administrator roles stored before a permission existed get it once
super::granted_permissions::grant_new_admin_permissions(bp).await?;
// inbuxa: personal-data catalog: the compliance roles, once per server
super::compliance_roles::ensure_compliance_roles(&bp.registry, &bp.data_store).await?;
if bp
.registry
.count_object(ObjectType::NetworkListener)
.await?
== 0
{
for (protocol, name, port, tls_implicit) in [
(NetworkListenerProtocol::Smtp, "smtp", 25, false),
(NetworkListenerProtocol::Smtp, "submissions", 465, true),
(NetworkListenerProtocol::Imap, "imaps", 993, true),
(NetworkListenerProtocol::Pop3, "pop3s", 995, true),
(NetworkListenerProtocol::ManageSieve, "sieve", 4190, false),
(NetworkListenerProtocol::Http, "https", 443, true),
(NetworkListenerProtocol::Http, "http", 8080, false),
] {
bp.registry
.write(RegistryWrite::insert(
&NetworkListener {
bind: Map::new(vec![
SocketAddr::from_str(&format!("[::]:{port}")).unwrap(),
]),
name: name.to_string(),
protocol,
use_tls: true,
tls_implicit,
..Default::default()
}
.into(),
))
.await?;
}
}
#[cfg(not(any(feature = "dev_mode", feature = "test_mode")))]
if bp.registry.count_object(ObjectType::Asn).await? == 0 {
bp.registry
.write(RegistryWrite::insert(
&Asn::Resource(AsnResource {
asn_urls: Map::new(vec![ASN_IPV4.into(), ASN_IPV6.into()]),
geo_urls: Map::new(vec![GEO_IPV4.into(), GEO_IPV6.into()]),
max_size: 104857600,
expires: Duration::from_millis(24 * 60 * 60 * 1000),
timeout: Duration::from_millis(5 * 60 * 1000),
..Default::default()
})
.into(),
))
.await?;
}
#[cfg(not(feature = "test_mode"))]
if bp.registry.count_object(ObjectType::TracingStore).await? == 0 {
bp.registry
.write(RegistryWrite::insert(&TracingStore::Default.into()))
.await?;
}
#[cfg(not(feature = "test_mode"))]
if bp.registry.count_object(ObjectType::MetricsStore).await? == 0 {
bp.registry
.write(RegistryWrite::insert(&MetricsStore::Default.into()))
.await?;
}
if bp.registry.count_object(ObjectType::Tracer).await? == 0 {
bp.registry
.write(RegistryWrite::insert(
&Tracer::Log(TracerLog {
enable: true,
ansi: false,
prefix: "inbuxa.log".into(),
rotate: LogRotateFrequency::Daily,
path: "/var/log/inbuxa".into(),
..Default::default()
})
.into(),
))
.await?;
}
#[cfg(not(feature = "test_mode"))]
{
use store::write::BatchBuilder;
use types::id::Id;
// inbuxa: rules are always to hand, since a copy ships with the server
// (spam_rules). They load on first boot, and again when the bundled
// rules differ from the ones last loaded: new tags and rules, fixes to
// rules nobody edited, never a changed score or an admin's edit.
let rules_url = super::spam_rules::rules_url(
bp.registry
.object::<SpamSettings>(Id::singleton())
.await?
.and_then(|spam| spam.spam_filter_rules_url),
);
let bundled_is_new = rules_url.is_none()
&& super::spam_rules::applied_version(&bp.data_store)
.await?
.as_deref()
!= Some(super::spam_rules::BUNDLED_SPAM_RULES_APPLIED);
if bp.registry.count_object(ObjectType::SpamRule).await? == 0 || bundled_is_new {
let mut batch = BatchBuilder::new();
batch.schedule_task(Task::SpamFilterMaintenance(TaskSpamFilterMaintenance {
maintenance_type: TaskSpamFilterMaintenanceType::UpdateRules,
status: TaskStatus::now(),
}));
bp.data_store.write(batch.build_all()).await?;
}
}
Ok(())
}
/// inbuxa: the new-install values of defaults D2, D3, D4 and D6 from the
/// personal-data catalog spec. Automatic IP bans expire after 30 days instead
/// of never; spam training samples are kept 90 days instead of 180; Pyzor,
/// which sends a digest of each message's text to a public server, is off;
/// delivery history is kept 14 days instead of 30.
fn new_install_privacy_defaults(
security: &mut Security,
classifier: &mut SpamClassifier,
pyzor: &mut SpamPyzor,
retention: &mut DataRetention,
) {
const DAY: u64 = 24 * 60 * 60 * 1000;
let ban_period = Some(Duration::from_millis(30 * DAY));
security.auth_ban_period = ban_period;
security.abuse_ban_period = ban_period;
security.loiter_ban_period = ban_period;
security.scan_ban_period = ban_period;
classifier.hold_samples_for = Duration::from_millis(90 * DAY);
pyzor.enable = false;
retention.hold_traces_for = Some(Duration::from_millis(14 * DAY));
}
#[cfg(test)]
mod tests {
use super::*;
const DAY: u64 = 24 * 60 * 60 * 1000;
#[test]
fn new_installs_get_the_privacy_defaults() {
let (mut security, mut classifier, mut pyzor, mut retention) = (
Security::default(),
SpamClassifier::default(),
SpamPyzor::default(),
DataRetention::default(),
);
// What an install gets without them: bans that never lift, 180-day
// samples, Pyzor on, 30-day traces.
assert_eq!(security.auth_ban_period, None);
assert!(pyzor.enable);
new_install_privacy_defaults(&mut security, &mut classifier, &mut pyzor, &mut retention);
for period in [
security.auth_ban_period,
security.abuse_ban_period,
security.loiter_ban_period,
security.scan_ban_period,
] {
assert_eq!(period.map(|p| p.into_inner().as_millis() as u64), Some(30 * DAY));
}
assert_eq!(classifier.hold_samples_for.into_inner().as_millis() as u64, 90 * DAY);
assert!(!pyzor.enable);
assert_eq!(
retention.hold_traces_for.map(|p| p.into_inner().as_millis() as u64),
Some(14 * DAY)
);
}
#[test]
fn everything_else_in_the_settings_stays() {
let mut retention = DataRetention {
archive_deleted_items_for: Some(Duration::from_millis(7 * DAY)),
..Default::default()
};
let before = retention.clone();
new_install_privacy_defaults(
&mut Security::default(),
&mut SpamClassifier::default(),
&mut SpamPyzor::default(),
&mut retention,
);
assert_eq!(retention.archive_deleted_items_for, before.archive_deleted_items_for);
assert_eq!(retention.hold_metrics_for, before.hold_metrics_for);
assert_eq!(retention.expunge_trash_after, before.expunge_trash_after);
}
}