534 lines
19 KiB
Rust
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(())
|
|
}
|