Files
inbuxa-server/crates/common/src/config/inner.rs
T
jcoffey-dev 2c684be5c9
ci / fork-checks (pull_request) Successful in 44s
ci / build (pull_request) Successful in 3m21s
Registry writes apply to the running settings without ReloadSettings
A 3-node rehearsal found that saving an MtaDeliverySchedule left it
unknown to the queue ("Queue strategy not found") until someone ran
x:Action ReloadSettings; only Directory and Authentication writes
reloaded (DIR-17). The admin UI has to remember a separate reload after
every save, and a script or API client that doesn't gets a server
running stale settings.

x:<Object>/set now reloads the running settings when it created,
updated or destroyed an object they are built from, and broadcasts the
same RegistryChange::Reload over the coordinator as ReloadSettings, so
every node applies it:

- Settings objects (MTA, spam filter, listeners, tracers, Sieve system
  scripts, cluster roles, directories, ...: the object types the core,
  telemetry, listener and directory builders read) get a full reload.
- Certificates, lookup stores and blocked/allowed IPs get their own
  targeted reloads.
- Accounts, domains, roles and other data read as needed, stores (they
  take a restart) and applications (their own reload action) get none.

Full reloads are coalesced: a write waits for a reload that started
after it was stored and joins one if it can, so a burst of writes, or
a request with many objects, costs one or two reloads, not one each.

The write itself is never undone. When the reload is refused (build
errors in objects that were working, the rule from the previous
commit), the set response says so in a new x:settingsReload field,
{"applied": false, "description": "Saved, but the running settings
were not reloaded. <object>: <error>"}; {"applied": true} otherwise.
The field is absent when the write needs no reload. The description
helper is shared with ReloadSettings' refusal.

Each reload sends the queue a ReloadSettings event, so the SMTP test
harness's read_event, try_read_event and assert_no_events now pass over
those; expect_reload_settings still waits for one.

system::auto_reload::settings_reload_tests (new): an MtaVirtualQueue
and an MtaDeliverySchedule created over JMAP are in the running
settings with no ReloadSettings, and gone once destroyed; eight
concurrent creates all land; a write whose reload fails is stored and
reported applied: false with the error; a domain write carries no
x:settingsReload. On main the new schedule is missing. The cluster
broadcast test (three nodes, PostgreSQL + NATS) now checks that every
node has a schedule created on node 0 without a reload.
2026-09-24 12:30:00 -07:00

258 lines
9.5 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 super::server::tls::build_self_signed_cert;
use crate::{
Caches, Data, DavResource, DavResources, MailboxCache, MessageStoreCache, MessageUidCache,
TlsConnectors,
auth::{AccessTokenInner, AccountCache, DomainCache, MailingListCache, RoleCache, TenantCache},
config::{
mailstore::spamfilter::SpamClassifier,
server::tls::parse_certificates,
smtp::{
auth::DkimSigners,
resolver::{Policy, Tlsa},
},
},
manager::application::WebApplications,
network::security::BlockedIps,
};
use ahash::{AHashMap, AHashSet};
use arc_swap::ArcSwap;
use mail_auth::{MX, Parameters, RecordSet, Txt};
use parking_lot::RwLock;
use registry::schema::{prelude::ObjectType, structs};
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::Arc,
};
use store::{LookupStores, registry::bootstrap::Bootstrap};
use utils::{
UnwrapFailure,
cache::{Cache, CacheWithTtl},
snowflake::{MAX_NODE_ID, SnowflakeIdGenerator},
tls::build_tls_connector,
};
impl Data {
pub async fn parse(bp: &mut Bootstrap) -> Self {
// Parse certificates
let mut certificates = AHashMap::new();
let mut subject_names = AHashSet::new();
parse_certificates(bp, &mut certificates, &mut subject_names).await;
if subject_names.is_empty() {
subject_names.insert("localhost".into());
}
// Build and test snowflake id generator
let node_id = bp.node_id();
if node_id > MAX_NODE_ID {
panic!("Node id {node_id} exceeds {MAX_NODE_ID}, panicking to avoid data corruption");
}
SnowflakeIdGenerator::set_node_id(node_id as u64);
let id_generator = SnowflakeIdGenerator::new();
if !id_generator.is_valid() {
panic!("Invalid system time, panicking to avoid data corruption");
}
// Initialize apps
let applications = WebApplications::new();
applications.reload(bp).await;
let blocked_ips = BlockedIps::parse(bp).await;
let lookup_stores = LookupStores::build(bp).await;
Data {
spam_classifier: ArcSwap::from_pointee(SpamClassifier::default()),
listener_control: Default::default(),
tls_certificates: ArcSwap::from_pointee(certificates),
tls_self_signed_cert: build_self_signed_cert(
subject_names
.into_iter()
.map(Into::into)
.collect::<Vec<_>>(),
)
.or_else(|err| {
bp.build_error(
ObjectType::Certificate.singleton(),
format!("Failed to build self-signed TLS certificate: {err}"),
);
build_self_signed_cert(vec!["localhost".to_string()])
})
.ok()
.map(Arc::new),
lookup_stores: ArcSwap::from_pointee(lookup_stores.stores),
blocked_ips: RwLock::new(blocked_ips),
jmap_id_gen: id_generator.clone(),
queue_id_gen: id_generator.clone(),
registry_id_gen: id_generator.clone(),
span_id_gen: id_generator,
queue_status: true.into(),
settings_reload: Default::default(),
applications,
logos: Default::default(),
smtp_connectors: TlsConnectors::try_new().failed("Failed to build TLS connectors"),
build_errors: Default::default(),
asn_geo_data: Default::default(),
}
}
}
impl Caches {
pub async fn parse(bp: &mut Bootstrap) -> Self {
let cache = bp.setting_infallible::<structs::Cache>().await;
Caches {
access_tokens: Cache::new_single_shard(
cache.access_tokens,
(std::mem::size_of::<AccessTokenInner>() + 255) as u64,
),
http_auth: Cache::new(cache.http_auth, (50 + std::mem::size_of::<u32>()) as u64),
messages: Cache::new_single_shard(
cache.messages,
(std::mem::size_of::<u32>()
+ std::mem::size_of::<Arc<MessageStoreCache>>()
+ (1024 * std::mem::size_of::<MessageUidCache>())
+ (15 * (std::mem::size_of::<MailboxCache>() + 60))) as u64,
),
files: Cache::new_single_shard(
cache.files,
(std::mem::size_of::<DavResources>() + (500 * std::mem::size_of::<DavResource>()))
as u64,
),
events: Cache::new_single_shard(
cache.events,
(std::mem::size_of::<DavResources>() + (500 * std::mem::size_of::<DavResource>()))
as u64,
),
contacts: Cache::new_single_shard(
cache.contacts,
(std::mem::size_of::<DavResources>() + (500 * std::mem::size_of::<DavResource>()))
as u64,
),
scheduling: Cache::new_single_shard(
cache.scheduling,
(std::mem::size_of::<DavResources>() + (500 * std::mem::size_of::<DavResource>()))
as u64,
),
emails: Cache::new(cache.email_addresses, 255u64),
emails_negative: CacheWithTtl::new(
cache.email_addresses_negative,
(std::mem::size_of::<DomainCache>() + 255) as u64,
),
domain_names: Cache::new(
cache.domain_names,
(std::mem::size_of::<DomainCache>() + 255) as u64,
),
domain_names_negative: CacheWithTtl::new(
cache.domain_names_negative,
(std::mem::size_of::<DomainCache>() + 255) as u64,
),
domains: Cache::new(
cache.domains,
(std::mem::size_of::<DomainCache>() + 255) as u64,
),
accounts: Cache::new(
cache.accounts,
(std::mem::size_of::<AccountCache>() + 255) as u64,
),
roles: Cache::new(cache.roles, (std::mem::size_of::<RoleCache>() + 255) as u64),
tenants: Cache::new(
cache.tenants,
(std::mem::size_of::<TenantCache>() + 255) as u64,
),
lists: Cache::new(
cache.mailing_lists,
(std::mem::size_of::<MailingListCache>() + 255) as u64,
),
dkim_signers: Cache::new(
cache.dkim_signatures,
(std::mem::size_of::<DkimSigners>() + 255) as u64,
),
dns_txt: CacheWithTtl::new(cache.dns_txt, (std::mem::size_of::<Txt>() + 255) as u64),
dns_mx: CacheWithTtl::new(cache.dns_mx, ((std::mem::size_of::<MX>() + 255) * 2) as u64),
dns_ptr: CacheWithTtl::new(cache.dns_ptr, (std::mem::size_of::<IpAddr>() + 255) as u64),
dns_ipv4: CacheWithTtl::new(
cache.dns_ipv4,
((std::mem::size_of::<Ipv4Addr>() + 255) * 2) as u64,
),
dns_ipv6: CacheWithTtl::new(
cache.dns_ipv6,
((std::mem::size_of::<Ipv6Addr>() + 255) * 2) as u64,
),
dns_tlsa: CacheWithTtl::new(cache.dns_tlsa, (std::mem::size_of::<Tlsa>() + 255) as u64),
dns_mta_sts: CacheWithTtl::new(
cache.dns_mta_sts,
(std::mem::size_of::<Policy>() + 255) as u64,
),
dns_rbl: CacheWithTtl::new(
cache.dns_rbl,
((std::mem::size_of::<Ipv4Addr>() + 255) * 2) as u64,
),
negative_cache_ttl: cache.negative_ttl.into_inner(),
}
}
#[allow(clippy::type_complexity)]
#[inline(always)]
pub fn build_auth_parameters<T>(
&self,
params: T,
) -> Parameters<
'_,
T,
CacheWithTtl<Box<str>, Txt>,
CacheWithTtl<Box<str>, RecordSet<MX>>,
CacheWithTtl<Box<str>, RecordSet<Ipv4Addr>>,
CacheWithTtl<Box<str>, RecordSet<Ipv6Addr>>,
CacheWithTtl<IpAddr, RecordSet<Box<str>>>,
> {
Parameters {
params,
cache_txt: Some(&self.dns_txt),
cache_mx: Some(&self.dns_mx),
cache_ptr: Some(&self.dns_ptr),
cache_ipv4: Some(&self.dns_ipv4),
cache_ipv6: Some(&self.dns_ipv6),
}
}
}
impl Default for Data {
fn default() -> Self {
Self {
spam_classifier: Default::default(),
listener_control: Default::default(),
tls_certificates: Default::default(),
tls_self_signed_cert: Default::default(),
blocked_ips: Default::default(),
jmap_id_gen: Default::default(),
queue_id_gen: Default::default(),
span_id_gen: Default::default(),
registry_id_gen: Default::default(),
queue_status: true.into(),
settings_reload: Default::default(),
applications: WebApplications::new(),
logos: Default::default(),
smtp_connectors: TlsConnectors::try_new().unwrap(),
build_errors: Default::default(),
asn_geo_data: Default::default(),
lookup_stores: Default::default(),
}
}
}
impl TlsConnectors {
fn try_new() -> Result<Self, String> {
Ok(TlsConnectors {
pki_verify: build_tls_connector(false)?,
dummy_verify: build_tls_connector(true)?,
})
}
}