Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,555 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
expr::if_block::{BootstrapExprExt, IfBlock},
|
||||
network::{
|
||||
autoconfig::pacc::{
|
||||
Authentication, Configuration, HttpServer, Info, Logo, OAuthPublic, Protocols,
|
||||
Provider, TextServer,
|
||||
},
|
||||
security::Security,
|
||||
},
|
||||
};
|
||||
use mail_builder::mime::make_boundary;
|
||||
use registry::schema::{
|
||||
enums::{AcmeChallengeType, ClusterTaskType, ProviderInfo, ServiceProtocol},
|
||||
prelude::{ObjectType, Property},
|
||||
structs::{
|
||||
self, AcmeProvider, Asn, ClusterTaskGroup, HttpForm, MailExchanger, Rate, Service,
|
||||
SystemSettings, TaskManager,
|
||||
},
|
||||
};
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Network {
|
||||
pub node_id: u64,
|
||||
pub roles: ClusterRoles,
|
||||
pub server_name: String,
|
||||
pub security: Security,
|
||||
pub http: Http,
|
||||
pub contact_form: Option<ContactForm>,
|
||||
pub asn_geo_lookup: AsnGeoLookupConfig,
|
||||
pub task_manager: TaskManager,
|
||||
pub has_acme_tls_challenge: bool,
|
||||
pub has_acme_http_challenge: bool,
|
||||
pub info: NetworkInfo,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NetworkInfo {
|
||||
pub pacc: Pacc,
|
||||
pub mxs: Vec<MailExchanger>,
|
||||
pub services: VecMap<ServiceProtocol, Service>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Pacc {
|
||||
pub prefix: String,
|
||||
pub suffix: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Http {
|
||||
pub rate_authenticated: Option<Rate>,
|
||||
pub rate_anonymous: Option<Rate>,
|
||||
pub url_https: String,
|
||||
pub allowed_endpoint: IfBlock,
|
||||
pub response_headers: Vec<(hyper::header::HeaderName, hyper::header::HeaderValue)>,
|
||||
pub use_forwarded: bool,
|
||||
pub redirect_root: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ContactForm {
|
||||
pub rcpt_to: Vec<String>,
|
||||
pub max_size: usize,
|
||||
pub rate: Option<Rate>,
|
||||
pub validate_domain: bool,
|
||||
pub from_email: FieldOrDefault,
|
||||
pub from_subject: FieldOrDefault,
|
||||
pub from_name: FieldOrDefault,
|
||||
pub field_honey_pot: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClusterRoles {
|
||||
pub store_maintenance: bool,
|
||||
pub account_maintenance: bool,
|
||||
pub push_notifications: bool,
|
||||
pub search_indexing: bool,
|
||||
pub spam_training: bool,
|
||||
pub metrics_calculate: bool,
|
||||
pub metrics_push: bool,
|
||||
pub outbound_mta: bool,
|
||||
pub task_scheduler: bool,
|
||||
pub task_manager: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub enum AsnGeoLookupConfig {
|
||||
Resource {
|
||||
expires: Duration,
|
||||
timeout: Duration,
|
||||
max_size: usize,
|
||||
headers: HeaderMap,
|
||||
asn_resources: Vec<String>,
|
||||
geo_resources: Vec<String>,
|
||||
},
|
||||
Dns {
|
||||
zone_ipv4: String,
|
||||
zone_ipv6: String,
|
||||
separator: String,
|
||||
index_asn: usize,
|
||||
index_asn_name: Option<usize>,
|
||||
index_country: Option<usize>,
|
||||
},
|
||||
#[default]
|
||||
Disabled,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FieldOrDefault {
|
||||
pub field: Option<String>,
|
||||
pub default: String,
|
||||
}
|
||||
|
||||
impl ContactForm {
|
||||
pub async fn parse(bp: &mut Bootstrap) -> Option<Self> {
|
||||
let form = bp.setting_infallible::<HttpForm>().await;
|
||||
|
||||
if !form.enable {
|
||||
return None;
|
||||
} else if form.deliver_to.is_empty() {
|
||||
bp.build_error(
|
||||
ObjectType::HttpForm.singleton(),
|
||||
"Contact form is enabled but no recipient addresses are configured",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ContactForm {
|
||||
rcpt_to: form.deliver_to.into_inner(),
|
||||
max_size: form.max_size as usize,
|
||||
validate_domain: form.validate_domain,
|
||||
from_email: FieldOrDefault {
|
||||
field: form.field_email,
|
||||
default: form.default_from_address,
|
||||
},
|
||||
from_subject: FieldOrDefault {
|
||||
field: form.field_subject,
|
||||
default: form.default_subject,
|
||||
},
|
||||
from_name: FieldOrDefault {
|
||||
field: form.field_name,
|
||||
default: form.default_name,
|
||||
},
|
||||
field_honey_pot: form.field_honey_pot,
|
||||
rate: form.rate_limit,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Network {
|
||||
pub async fn parse(bp: &mut Bootstrap) -> Self {
|
||||
let system = bp.setting_infallible::<SystemSettings>().await;
|
||||
let mut has_acme_tls_challenge = false;
|
||||
let mut has_acme_http_challenge = false;
|
||||
let mut has_acme_challenges = false;
|
||||
|
||||
for provider in bp.list_infallible::<AcmeProvider>().await {
|
||||
match provider.object.challenge_type {
|
||||
AcmeChallengeType::Http01 => has_acme_http_challenge = true,
|
||||
AcmeChallengeType::TlsAlpn01 => has_acme_tls_challenge = true,
|
||||
_ => {}
|
||||
}
|
||||
has_acme_challenges = true;
|
||||
}
|
||||
|
||||
if !has_acme_challenges {
|
||||
// Assume this is an initial deployment and optimistically set both to true
|
||||
// to avoid requiring a reload after ACME providers are added
|
||||
has_acme_http_challenge = true;
|
||||
has_acme_tls_challenge = true;
|
||||
}
|
||||
|
||||
const SPLIT_HERE: &str = "$$__SPLIT_HERE__$$";
|
||||
let mut pacc = Configuration {
|
||||
protocols: Protocols::default(),
|
||||
authentication: Some(Authentication {
|
||||
oauth_public: Some(OAuthPublic {
|
||||
issuer: SPLIT_HERE.to_string(),
|
||||
}),
|
||||
password: true,
|
||||
}),
|
||||
info: Info {
|
||||
provider: Provider {
|
||||
name: "Stalwart".into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
let default_hostname = if !system.default_hostname.is_empty() {
|
||||
system.default_hostname.as_str()
|
||||
} else {
|
||||
bp.registry.local_hostname()
|
||||
};
|
||||
let mut http_host = default_hostname.to_string();
|
||||
for (service, details) in &system.services {
|
||||
let hostname = details.hostname.as_deref().unwrap_or(default_hostname);
|
||||
|
||||
match service {
|
||||
ServiceProtocol::Jmap => {
|
||||
if hostname != http_host {
|
||||
http_host = hostname.to_string();
|
||||
}
|
||||
pacc.protocols.jmap = HttpServer {
|
||||
url: format!("https://{hostname}/jmap/session",),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
ServiceProtocol::Caldav => {
|
||||
pacc.protocols.caldav = HttpServer {
|
||||
url: format!("https://{hostname}/dav/cal/",),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
ServiceProtocol::Carddav => {
|
||||
pacc.protocols.carddav = HttpServer {
|
||||
url: format!("https://{hostname}/dav/card/",),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
ServiceProtocol::Webdav => {
|
||||
pacc.protocols.webdav = HttpServer {
|
||||
url: format!("https://{hostname}/dav/file/",),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
ServiceProtocol::Imap => {
|
||||
pacc.protocols.imap = TextServer {
|
||||
host: hostname.to_string(),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
ServiceProtocol::Pop3 => {
|
||||
pacc.protocols.pop3 = TextServer {
|
||||
host: hostname.to_string(),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
ServiceProtocol::Smtp => {
|
||||
pacc.protocols.smtp = TextServer {
|
||||
host: hostname.to_string(),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
ServiceProtocol::Managesieve => {
|
||||
pacc.protocols.managesieve = TextServer {
|
||||
host: hostname.to_string(),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (tag, text) in system.provider_info {
|
||||
match tag {
|
||||
ProviderInfo::ProviderName => pacc.info.provider.name = text,
|
||||
ProviderInfo::ProviderShortName => pacc.info.provider.short_name = Some(text),
|
||||
ProviderInfo::UserDocumentation => {
|
||||
pacc.info.help.get_or_insert_default().documentation = Some(text)
|
||||
}
|
||||
ProviderInfo::DeveloperDocumentation => {
|
||||
pacc.info.help.get_or_insert_default().developer = Some(text)
|
||||
}
|
||||
ProviderInfo::ContactUri => {
|
||||
pacc.info
|
||||
.help
|
||||
.get_or_insert_default()
|
||||
.contact
|
||||
.get_or_insert_default()
|
||||
.push(text);
|
||||
}
|
||||
ProviderInfo::LogoUrl => {
|
||||
let logo = pacc.info.provider.logo.get_or_insert_default();
|
||||
if logo.is_empty() {
|
||||
logo.push(Logo {
|
||||
url: text,
|
||||
..Default::default()
|
||||
});
|
||||
} else {
|
||||
logo[0].url = text;
|
||||
}
|
||||
}
|
||||
ProviderInfo::LogoWidth => {
|
||||
let logo = pacc.info.provider.logo.get_or_insert_default();
|
||||
if logo.is_empty() {
|
||||
logo.push(Logo {
|
||||
width: text.parse().ok(),
|
||||
..Default::default()
|
||||
});
|
||||
} else {
|
||||
logo[0].width = text.parse().ok();
|
||||
}
|
||||
}
|
||||
ProviderInfo::LogoHeight => {
|
||||
let logo = pacc.info.provider.logo.get_or_insert_default();
|
||||
if logo.is_empty() {
|
||||
logo.push(Logo {
|
||||
height: text.parse().ok(),
|
||||
..Default::default()
|
||||
});
|
||||
} else {
|
||||
logo[0].height = text.parse().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (prefix, suffix) = serde_json::to_string(&pacc)
|
||||
.unwrap_or_default()
|
||||
.rsplit_once(SPLIT_HERE)
|
||||
.map(|(prefix, suffix)| (prefix.to_string(), suffix.to_string()))
|
||||
.unwrap();
|
||||
let mut network = Network {
|
||||
node_id: bp.node_id() as u64,
|
||||
server_name: default_hostname.to_string(),
|
||||
security: Security::parse(bp).await,
|
||||
contact_form: ContactForm::parse(bp).await,
|
||||
asn_geo_lookup: AsnGeoLookupConfig::parse(bp).await.unwrap_or_default(),
|
||||
roles: ClusterRoles::default(),
|
||||
http: Http::parse(bp, &http_host).await,
|
||||
task_manager: bp.setting_infallible::<TaskManager>().await,
|
||||
has_acme_tls_challenge,
|
||||
has_acme_http_challenge,
|
||||
info: NetworkInfo {
|
||||
mxs: system.mail_exchangers.into_iter().collect(),
|
||||
services: system.services,
|
||||
pacc: Pacc { prefix, suffix },
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(role) = &bp.role {
|
||||
match &role.tasks {
|
||||
ClusterTaskGroup::EnableAll => {}
|
||||
ClusterTaskGroup::DisableAll => {
|
||||
for network_role in network.roles.all_mut() {
|
||||
*network_role = false;
|
||||
}
|
||||
}
|
||||
ClusterTaskGroup::EnableSome(group) => {
|
||||
for network_role in network.roles.all_mut() {
|
||||
*network_role = false;
|
||||
}
|
||||
for task_type in group.task_types.iter() {
|
||||
network.roles.set_role(*task_type, true);
|
||||
}
|
||||
}
|
||||
ClusterTaskGroup::DisableSome(group) => {
|
||||
for task_type in group.task_types.iter() {
|
||||
network.roles.set_role(*task_type, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
network
|
||||
}
|
||||
|
||||
pub fn message_id(&self) -> String {
|
||||
format!("{}@{}", make_boundary("."), self.server_name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Http {
|
||||
#[cfg_attr(
|
||||
any(feature = "dev_mode", feature = "test_mode"),
|
||||
allow(unused_variables)
|
||||
)]
|
||||
pub async fn parse(bp: &mut Bootstrap, server_name: &str) -> Self {
|
||||
let http = bp.setting_infallible::<structs::Http>().await;
|
||||
|
||||
// Parse HTTP headers
|
||||
let mut http_headers = http
|
||||
.response_headers
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
Ok((
|
||||
hyper::header::HeaderName::from_str(k.trim()).map_err(|err| {
|
||||
format!("Invalid header found in property \"http.headers\": {}", err)
|
||||
})?,
|
||||
hyper::header::HeaderValue::from_str(v.trim()).map_err(|err| {
|
||||
format!("Invalid header found in property \"http.headers\": {}", err)
|
||||
})?,
|
||||
))
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()
|
||||
.map_err(|e| {
|
||||
bp.build_error(
|
||||
ObjectType::Http.singleton(),
|
||||
format!("Failed to parse HTTP headers: {}", e),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Add permissive CORS headers
|
||||
#[cfg(feature = "dev_mode")]
|
||||
let use_permissive_cors = true;
|
||||
|
||||
#[cfg(not(feature = "dev_mode"))]
|
||||
let use_permissive_cors = http.use_permissive_cors || bp.registry.is_recovery_mode();
|
||||
|
||||
if use_permissive_cors {
|
||||
http_headers.push((
|
||||
hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
hyper::header::HeaderValue::from_static("*"),
|
||||
));
|
||||
http_headers.push((
|
||||
hyper::header::ACCESS_CONTROL_ALLOW_HEADERS,
|
||||
hyper::header::HeaderValue::from_static(
|
||||
"Authorization, Content-Type, Accept, X-Requested-With",
|
||||
),
|
||||
));
|
||||
http_headers.push((
|
||||
hyper::header::ACCESS_CONTROL_ALLOW_METHODS,
|
||||
hyper::header::HeaderValue::from_static(
|
||||
"POST, GET, PATCH, PUT, DELETE, HEAD, OPTIONS",
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// Add HTTP Strict Transport Security
|
||||
if http.enable_hsts {
|
||||
http_headers.push((
|
||||
hyper::header::STRICT_TRANSPORT_SECURITY,
|
||||
hyper::header::HeaderValue::from_static("max-age=31536000; includeSubDomains"),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "dev_mode", feature = "test_mode"))]
|
||||
let server_name = "127.0.0.1";
|
||||
|
||||
Http {
|
||||
url_https: if !bp.registry.is_recovery_mode() {
|
||||
if let Some(url) = bp.registry.public_url() {
|
||||
url.to_string()
|
||||
} else {
|
||||
format!("https://{server_name}")
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
allowed_endpoint: if bp.registry.is_recovery_mode() {
|
||||
IfBlock::empty(ObjectType::Http.singleton(), Property::AllowedEndpoints)
|
||||
} else {
|
||||
bp.compile_expr(ObjectType::Http.singleton(), &http.ctx_allowed_endpoints())
|
||||
},
|
||||
rate_authenticated: if bp.registry.is_recovery_mode() {
|
||||
None
|
||||
} else {
|
||||
http.rate_limit_authenticated
|
||||
},
|
||||
rate_anonymous: if bp.registry.is_recovery_mode() {
|
||||
None
|
||||
} else {
|
||||
http.rate_limit_anonymous
|
||||
},
|
||||
response_headers: http_headers,
|
||||
use_forwarded: http.use_x_forwarded,
|
||||
redirect_root: http.redirect_root,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsnGeoLookupConfig {
|
||||
pub async fn parse(bp: &mut Bootstrap) -> Option<Self> {
|
||||
match bp.setting_infallible::<Asn>().await {
|
||||
Asn::Resource(asn) => Some(AsnGeoLookupConfig::Resource {
|
||||
expires: asn.expires.into_inner(),
|
||||
timeout: asn.timeout.into_inner(),
|
||||
max_size: asn.max_size as usize,
|
||||
headers: asn
|
||||
.http_auth
|
||||
.build_headers(asn.http_headers, None)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
bp.build_error(
|
||||
ObjectType::Asn.singleton(),
|
||||
format!("Unable to build HTTP headers: {}", err),
|
||||
)
|
||||
})
|
||||
.ok()?,
|
||||
asn_resources: asn.asn_urls.into_inner(),
|
||||
geo_resources: asn.geo_urls.into_inner(),
|
||||
}),
|
||||
Asn::Dns(asn) => Some(AsnGeoLookupConfig::Dns {
|
||||
zone_ipv4: asn.zone_ip_v4,
|
||||
zone_ipv6: asn.zone_ip_v6,
|
||||
separator: asn.separator,
|
||||
index_asn: asn.index_asn as usize,
|
||||
index_asn_name: asn.index_asn_name.map(|v| v as usize),
|
||||
index_country: asn.index_country.map(|v| v as usize),
|
||||
}),
|
||||
Asn::Disabled => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClusterRoles {
|
||||
fn all_mut(&mut self) -> impl Iterator<Item = &mut bool> {
|
||||
[
|
||||
&mut self.store_maintenance,
|
||||
&mut self.account_maintenance,
|
||||
&mut self.push_notifications,
|
||||
&mut self.search_indexing,
|
||||
&mut self.spam_training,
|
||||
&mut self.outbound_mta,
|
||||
&mut self.task_manager,
|
||||
&mut self.task_scheduler,
|
||||
&mut self.metrics_calculate,
|
||||
&mut self.metrics_push,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
fn set_role(&mut self, role: ClusterTaskType, enabled: bool) {
|
||||
match role {
|
||||
ClusterTaskType::StoreMaintenance => self.store_maintenance = enabled,
|
||||
ClusterTaskType::AccountMaintenance => self.account_maintenance = enabled,
|
||||
ClusterTaskType::PushNotifications => self.push_notifications = enabled,
|
||||
ClusterTaskType::SearchIndexing => self.search_indexing = enabled,
|
||||
ClusterTaskType::SpamClassifierTraining => self.spam_training = enabled,
|
||||
ClusterTaskType::MetricsCalculate => self.metrics_calculate = enabled,
|
||||
ClusterTaskType::MetricsPush => self.metrics_push = enabled,
|
||||
ClusterTaskType::OutboundMta => self.outbound_mta = enabled,
|
||||
ClusterTaskType::TaskQueueProcessing => self.task_manager = enabled,
|
||||
ClusterTaskType::TaskScheduler => self.task_scheduler = enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ClusterRoles {
|
||||
fn default() -> Self {
|
||||
ClusterRoles {
|
||||
store_maintenance: true,
|
||||
account_maintenance: true,
|
||||
push_notifications: true,
|
||||
search_indexing: true,
|
||||
spam_training: true,
|
||||
metrics_calculate: true,
|
||||
metrics_push: true,
|
||||
outbound_mta: true,
|
||||
task_manager: true,
|
||||
task_scheduler: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user