Files
inbuxa-server/crates/jmap/src/registry/mapping/domain.rs
T
jcoffey-dev 72f8ddd5e7 Import upstream v0.16.25, stripped
Upstream commit: 3f657330c0f49a015a3a372fb59669b5cccbca6d
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 118 in 50 files
Dangling module declarations removed: 5
Edits turning enterprise off: 25
Third-party code: 14 files, 0 not in THIRD-PARTY.md
Renamed identifiers: 67 in 19 files
Verification: clean

The same Enterprise footprint as v0.16.24. The build check is clean
apart from the expected errors in the rebuilt-feature tests. A
bug-fix release: DKIM rotation, IMAP failed-login answers, DNSBL
multi-code scoring and negative TTLs, Pyzor on short messages, queue
quotas with an empty match, RocksDB info-log rotation, and
Autodiscover schema handling.
2026-10-05 21:15:02 -07:00

229 lines
7.6 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::registry::mapping::{
ObjectResponse, RegistrySetResponse, ValidationResult, principal::validate_tenant_quota,
};
use common::network::{dkim::generate_dkim_selector, dns::update::DnsUpdater};
use jmap_proto::error::set::{SetError, SetErrorType};
use registry::{
schema::{
enums::{AcmeChallengeType, DkimSignatureType, DnsRecordType, TenantStorageQuota},
prelude::{ObjectType, Property},
structs::{
AcmeProvider, CertificateManagement, DkimManagement, DkimManagementProperties,
DnsManagement, DnsServer, Domain, Task, TaskDnsManagement, TaskDomainManagement,
TaskStatus,
},
},
types::map::Map,
};
use types::id::Id;
pub(crate) async fn validate_domain(
set: &RegistrySetResponse<'_>,
domain: &mut Domain,
old_domain: Option<&Domain>,
tasks: &mut Vec<Task>,
) -> ValidationResult {
let response = if old_domain.is_none() {
match validate_tenant_quota(set.server, set.access_token, TenantStorageQuota::MaxDomains)
.await?
{
Ok(response) => response,
Err(err) => {
return Ok(Err(err));
}
}
} else {
ObjectResponse::default()
};
// Validate DKIM selector template
if let DkimManagement::Automatic(DkimManagementProperties {
selector_template, ..
}) = &domain.dkim_management
&& old_domain.is_none_or(|old| {
matches!(
&old.dkim_management,
DkimManagement::Automatic(DkimManagementProperties {
selector_template: old_selector_template,
..
}) if old_selector_template != selector_template
)
})
&& let Err(err) =
generate_dkim_selector(selector_template, DkimSignatureType::Dkim1RsaSha256)
{
return Ok(Err(SetError::invalid_properties()
.with_property(Property::SelectorTemplate)
.with_description(err)));
}
// Validate that names and aliases do not collide with another domain
let registry = set.server.registry();
if old_domain.is_none_or(|old| old.name != domain.name)
&& let Some(existing) = registry
.primary_key(
ObjectType::Domain.into(),
Property::Aliases,
domain.name.as_bytes().to_vec(),
)
.await?
{
return Ok(Err(SetError::new(SetErrorType::PrimaryKeyViolation)
.with_property(Property::Name)
.with_object_id(existing)));
}
for alias in domain.aliases.iter() {
if alias == &domain.name
|| old_domain.is_some_and(|old| old.aliases.contains(alias) || &old.name == alias)
{
continue;
}
for index in [Property::Name, Property::Aliases] {
if let Some(existing) = registry
.primary_key(ObjectType::Domain.into(), index, alias.as_bytes().to_vec())
.await?
{
return Ok(Err(SetError::new(SetErrorType::PrimaryKeyViolation)
.with_property(Property::Aliases)
.with_object_id(existing)));
}
}
}
// Schedule DNS update task
let will_trigger_dkim = matches!(domain.dkim_management, DkimManagement::Automatic(_))
&& old_domain
.is_none_or(|old| !matches!(old.dkim_management, DkimManagement::Automatic(_)));
let will_schedule_dkim = !will_trigger_dkim
&& matches!(domain.dkim_management, DkimManagement::Automatic(_))
&& publishes_dkim(domain)
&& old_domain.is_some_and(|old| !publishes_dkim(old));
let will_trigger_acme = if let DnsManagement::Automatic(details) = &domain.dns_management
&& old_domain.is_none_or(|old| !matches!(old.dns_management, DnsManagement::Automatic(_)))
{
let on_success_renew_certificate = old_domain.is_none()
&& matches!(
domain.certificate_management,
CertificateManagement::Automatic(_)
);
tasks.push(Task::DnsManagement(TaskDnsManagement {
domain_id: Id::default(),
update_records: Map::new(
details
.publish_records
.iter()
.filter(|&&r| r != DnsRecordType::Dkim || !will_trigger_dkim)
.copied()
.collect(),
),
on_success_renew_certificate,
status: TaskStatus::now(),
}));
on_success_renew_certificate
} else {
if will_schedule_dkim {
tasks.push(Task::DnsManagement(TaskDnsManagement {
domain_id: Id::default(),
update_records: Map::new(vec![DnsRecordType::Dkim]),
on_success_renew_certificate: false,
status: TaskStatus::now(),
}));
}
false
};
// Schedule DKIM key rotation task
if will_trigger_dkim || will_schedule_dkim {
tasks.push(Task::DkimManagement(TaskDomainManagement {
domain_id: Id::default(),
status: TaskStatus::now(),
}));
}
// Schedule ACME renewal task if needed
if !will_trigger_acme
&& let CertificateManagement::Automatic(details) = &domain.certificate_management
&& old_domain.is_none_or(|old| {
!matches!(
old.certificate_management,
CertificateManagement::Automatic(_)
)
})
{
let Some(provider) = set
.server
.registry()
.object::<AcmeProvider>(details.acme_provider_id)
.await?
else {
return Ok(Err(SetError::invalid_properties()
.with_property(Property::AcmeProviderId)
.with_description("ACME provider not found")));
};
if matches!(provider.challenge_type, AcmeChallengeType::Dns01)
&& !matches!(domain.dns_management, DnsManagement::Automatic(_))
{
return Ok(Err(SetError::invalid_properties()
.with_property(Property::AcmeProviderId)
.with_description(
"ACME provider requires automatic DNS management",
)));
}
tasks.push(Task::AcmeRenewal(TaskDomainManagement {
domain_id: Id::default(),
status: TaskStatus::now(),
}));
}
Ok(Ok(response))
}
fn publishes_dkim(domain: &Domain) -> bool {
matches!(
&domain.dns_management,
DnsManagement::Automatic(details) if details.publish_records.contains(&DnsRecordType::Dkim)
)
}
pub(crate) async fn validate_dns_server(
set: &RegistrySetResponse<'_>,
dns: &mut DnsServer,
old_dns: Option<&DnsServer>,
) -> ValidationResult {
let response = if old_dns.is_none() {
match validate_tenant_quota(
set.server,
set.access_token,
TenantStorageQuota::MaxDnsServers,
)
.await?
{
Ok(response) => response,
Err(err) => {
return Ok(Err(err));
}
}
} else {
ObjectResponse::default()
};
if old_dns.is_none_or(|old_dns| old_dns != dns)
&& let Err(err) = DnsUpdater::build(dns.clone(), set.server.core.clone()).await
{
return Ok(Err(SetError::invalid_properties()
.with_description(format!("Failed to build DNS server: {err}"))));
}
Ok(Ok(response))
}