Personal-data catalog spec, default D5 (settled 2026-09-28; built after the v0.16.24 import's spam-rules loader landed). msbl.org's EBL is sent a SHA-1 of every email address it's asked about. A new install's first boot now leaves a note, and the rules update, once the bundled rules are in, switches STWT_MSBL_EBL_EMAIL off and forgets the note, so it happens once; the loader keeps that switch through later updates. An existing server has no note and keeps every blocklist as it is. Also fixes the data inventory's DNSBL endpoints: a zone is an expression (`ip_reverse + '.zen.spamhaus.org'`, conditional branches, `hash(email, 'sha1') + '.ebl.msbl.org'`), and the zone names are now the quoted literals that start with a dot, from every branch, rather than the expression's text. Tested: unit test for the zone rule; the compliance system test (no note, no change; the inventory lists ebl.msbl.org, not a hash; with the note the blocklist goes off; the note works once); the system suite; fork checks.
682 lines
25 KiB
Rust
682 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?;
|
|
}
|
|
|
|
// D5: the blocklist sent hashed email addresses starts off; the
|
|
// rules load later, from a task, which acts on this note
|
|
super::spam_rules::mark_new_install(&bp.data_store).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);
|
|
}
|
|
}
|