/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * 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 + 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::(), }), 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::().await; let mut classifier = bp.setting_infallible::().await; let mut pyzor = bp.setting_infallible::().await; let mut retention = bp.setting_infallible::().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?; 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 // version differs from the one last loaded, which only adds what's // missing: new tags and rules, never a changed score. let rules_url = super::spam_rules::rules_url( bp.registry .object::(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_VERSION); 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); } }