Files
Stalwart/crates/common/src/manager/defaults.rs
2026-04-19 17:31:06 +02:00

534 lines
19 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::auth::permissions::DefaultPermissions;
use aws_lc_rs::{
rand::SystemRandom,
signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair},
};
use registry::{
schema::{
enums::*,
prelude::{ObjectType, SocketAddr},
structs::*,
},
types::{duration::Duration, error::Error, list::List, map::Map},
};
use std::str::FromStr;
use store::{
rand::{Rng, distr::Alphanumeric, rng},
registry::{
bootstrap::Bootstrap,
write::{RegistryWrite, RegistryWriteResult},
},
};
pub const ASN_IPV4: &str = "https://cdn.jsdelivr.net/npm/@ip-location-db/asn/asn-ipv4.csv";
pub const ASN_IPV6: &str = "https://cdn.jsdelivr.net/npm/@ip-location-db/asn/asn-ipv6.csv";
pub const GEO_IPV4: &str = "https://cdn.jsdelivr.net/npm/@ip-location-db/geolite2-geo-whois-asn-country/geolite2-geo-whois-asn-country-ipv4.csv";
pub const GEO_IPV6: &str = "https://cdn.jsdelivr.net/npm/@ip-location-db/geolite2-geo-whois-asn-country/geolite2-geo-whois-asn-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();
#[cfg(not(feature = "test_mode"))]
if (!is_recovery_mode || is_bootstrap_mode)
&& bp.registry.count_object(ObjectType::Application).await? == 0
{
bp.registry
.write(RegistryWrite::insert(
&Application {
auto_update_frequency: Duration::from_millis(30 * 24 * 60 * 60 * 1000),
description: "Stalwart Web Interface".to_string(),
enabled: true,
#[cfg(not(feature = "dev_mode"))]
resource_url:
"https://github.com/stalwartlabs/webui/releases/latest/download/webui.zip"
.into(),
#[cfg(feature = "dev_mode")]
resource_url: "file:///Users/me/code/webui/.ignore/webui.zip".into(),
unpack_directory: None,
url_prefix: Map::new(vec!["/admin".into(), "/account".into()]),
}
.into(),
))
.await?;
}
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(());
}
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::Custom(Default::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: "default".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?;
}
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?;
}
}
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: "stalwart.log".into(),
rotate: LogRotateFrequency::Daily,
path: "/var/log/stalwart".into(),
..Default::default()
})
.into(),
))
.await?;
}
#[cfg(not(feature = "test_mode"))]
{
use store::write::BatchBuilder;
use types::id::Id;
if bp.registry.count_object(ObjectType::SpamRule).await? == 0
&& bp
.registry
.object::<SpamSettings>(Id::singleton())
.await?
.is_none_or(|spam| spam.spam_filter_rules_url.is_some())
{
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(())
}