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,380 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::expr::{
|
||||
self,
|
||||
if_block::{BootstrapExprExt, IfBlock},
|
||||
};
|
||||
use mail_auth::{
|
||||
common::crypto::{Ed25519Key, HashAlgorithm, RsaKey, Sha256, SigningKey},
|
||||
dkim::{Canonicalization, Done},
|
||||
dkim2::{Dkim2Signer, Done as Dkim2Done, Flag},
|
||||
};
|
||||
use mail_parser::decoders::base64::base64_decode;
|
||||
use registry::{
|
||||
schema::{
|
||||
enums::{self, Dkim2Flag, ExpressionConstant},
|
||||
prelude::ObjectType,
|
||||
structs::{Dkim1Signature, DkimSignature, SenderAuth},
|
||||
},
|
||||
types::{ObjectImpl, map::Map},
|
||||
};
|
||||
use rustls_pki_types::{PrivateKeyDer, PrivatePkcs1KeyDer, PrivatePkcs8KeyDer, pem::PemObject};
|
||||
use store::registry::bootstrap::Bootstrap;
|
||||
use utils::cache::CacheItemWeight;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MailAuthConfig {
|
||||
pub dkim: DkimAuthConfig,
|
||||
pub arc: ArcAuthConfig,
|
||||
pub spf: SpfAuthConfig,
|
||||
pub dmarc: DmarcAuthConfig,
|
||||
pub iprev: IpRevAuthConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DkimAuthConfig {
|
||||
pub verify: IfBlock,
|
||||
pub sign: IfBlock,
|
||||
pub strict: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ArcAuthConfig {
|
||||
pub verify: IfBlock,
|
||||
//pub seal: IfBlock,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SpfAuthConfig {
|
||||
pub verify_ehlo: IfBlock,
|
||||
pub verify_mail_from: IfBlock,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DmarcAuthConfig {
|
||||
pub verify: IfBlock,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IpRevAuthConfig {
|
||||
pub verify: IfBlock,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub enum VerifyStrategy {
|
||||
#[default]
|
||||
Relaxed,
|
||||
Strict,
|
||||
Disable,
|
||||
}
|
||||
|
||||
pub enum Dkim1Signer {
|
||||
RsaSha256(mail_auth::dkim::DkimSigner<RsaKey<Sha256>, Done>),
|
||||
Ed25519Sha256(mail_auth::dkim::DkimSigner<Ed25519Key, Done>),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DkimSigners {
|
||||
pub dkim1: Vec<Dkim1Signer>,
|
||||
pub dkim2: Option<Dkim2Signer<Dkim2Done>>,
|
||||
}
|
||||
|
||||
impl MailAuthConfig {
|
||||
pub async fn parse(bp: &mut Bootstrap) -> Self {
|
||||
let auth = bp.setting_infallible::<SenderAuth>().await;
|
||||
|
||||
MailAuthConfig {
|
||||
dkim: DkimAuthConfig {
|
||||
verify: bp
|
||||
.compile_expr(ObjectType::SenderAuth.singleton(), &auth.ctx_dkim_verify()),
|
||||
sign: bp.compile_expr(
|
||||
ObjectType::SenderAuth.singleton(),
|
||||
&auth.ctx_dkim_sign_domain(),
|
||||
),
|
||||
strict: auth.dkim_strict,
|
||||
},
|
||||
arc: ArcAuthConfig {
|
||||
verify: bp.compile_expr(ObjectType::SenderAuth.singleton(), &auth.ctx_arc_verify()),
|
||||
//seal: bp.compile_expr(ObjectType::SenderAuth.singleton(), &auth.ctx_arc_seal_domain()),
|
||||
},
|
||||
spf: SpfAuthConfig {
|
||||
verify_ehlo: bp.compile_expr(
|
||||
ObjectType::SenderAuth.singleton(),
|
||||
&auth.ctx_spf_ehlo_verify(),
|
||||
),
|
||||
verify_mail_from: bp.compile_expr(
|
||||
ObjectType::SenderAuth.singleton(),
|
||||
&auth.ctx_spf_from_verify(),
|
||||
),
|
||||
},
|
||||
dmarc: DmarcAuthConfig {
|
||||
verify: bp
|
||||
.compile_expr(ObjectType::SenderAuth.singleton(), &auth.ctx_dmarc_verify()),
|
||||
},
|
||||
iprev: IpRevAuthConfig {
|
||||
verify: bp.compile_expr(
|
||||
ObjectType::SenderAuth.singleton(),
|
||||
&auth.ctx_reverse_ip_verify(),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DkimSigners {
|
||||
pub async fn insert(&mut self, domain: String, signature: DkimSignature) -> trc::Result<()> {
|
||||
let mut errors = vec![];
|
||||
if !signature.validate(&mut errors) {
|
||||
return Err(trc::DkimEvent::BuildError
|
||||
.reason("DKIM signature validation failed")
|
||||
.details(
|
||||
errors
|
||||
.into_iter()
|
||||
.map(|v| trc::Value::from(v.to_string()))
|
||||
.collect::<Vec<_>>(),
|
||||
));
|
||||
}
|
||||
|
||||
match signature {
|
||||
DkimSignature::Dkim1Ed25519Sha256(signature) => {
|
||||
let private_key = signature
|
||||
.private_key
|
||||
.secret()
|
||||
.await
|
||||
.map_err(|err| trc::DkimEvent::BuildError.reason(err))?;
|
||||
let private_key = simple_pem_parse(&private_key).ok_or_else(|| {
|
||||
trc::DkimEvent::BuildError
|
||||
.reason("Failed to parse ED25519 private key PEM")
|
||||
.details("Invalid PEM format")
|
||||
})?;
|
||||
let key =
|
||||
Ed25519Key::from_pkcs8_maybe_unchecked_der(&private_key).map_err(|err| {
|
||||
trc::DkimEvent::BuildError
|
||||
.reason(err)
|
||||
.details("Failed to build ED25519 key")
|
||||
})?;
|
||||
|
||||
self.dkim1
|
||||
.push(Dkim1Signer::Ed25519Sha256(build_dkim1_signer(
|
||||
domain, signature, key,
|
||||
)));
|
||||
}
|
||||
DkimSignature::Dkim1RsaSha256(signature) => {
|
||||
let private_key = signature
|
||||
.private_key
|
||||
.secret()
|
||||
.await
|
||||
.map_err(|err| trc::DkimEvent::BuildError.reason(err))?;
|
||||
let key = rsa_key_parse(private_key.as_bytes())?;
|
||||
|
||||
self.dkim1.push(Dkim1Signer::RsaSha256(build_dkim1_signer(
|
||||
domain, signature, key,
|
||||
)));
|
||||
}
|
||||
DkimSignature::Dkim2Ed25519Sha256(signature) => {
|
||||
let private_key = signature
|
||||
.private_key
|
||||
.secret()
|
||||
.await
|
||||
.map_err(|err| trc::DkimEvent::BuildError.reason(err))?;
|
||||
let private_key = simple_pem_parse(&private_key).ok_or_else(|| {
|
||||
trc::DkimEvent::BuildError
|
||||
.reason("Failed to parse ED25519 private key PEM")
|
||||
.details("Invalid PEM format")
|
||||
})?;
|
||||
let key =
|
||||
Ed25519Key::from_pkcs8_maybe_unchecked_der(&private_key).map_err(|err| {
|
||||
trc::DkimEvent::BuildError
|
||||
.reason(err)
|
||||
.details("Failed to build ED25519 key")
|
||||
})?;
|
||||
|
||||
self.dkim2 = Some(match self.dkim2.take() {
|
||||
None => Dkim2Signer::from_key(key)
|
||||
.domain(domain)
|
||||
.selector(signature.selector)
|
||||
.flags(map_dkim2_flags(signature.flags)),
|
||||
Some(signer) => signer
|
||||
.additional_key(key, signature.selector)
|
||||
.flags(map_dkim2_flags(signature.flags)),
|
||||
});
|
||||
}
|
||||
DkimSignature::Dkim2RsaSha256(signature) => {
|
||||
let private_key = signature
|
||||
.private_key
|
||||
.secret()
|
||||
.await
|
||||
.map_err(|err| trc::DkimEvent::BuildError.reason(err))?;
|
||||
let key = rsa_key_parse(private_key.as_bytes())?;
|
||||
|
||||
self.dkim2 = Some(match self.dkim2.take() {
|
||||
None => Dkim2Signer::from_key(key)
|
||||
.domain(domain)
|
||||
.selector(signature.selector)
|
||||
.flags(map_dkim2_flags(signature.flags)),
|
||||
Some(signer) => signer
|
||||
.additional_key(key, signature.selector)
|
||||
.flags(map_dkim2_flags(signature.flags)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn map_dkim2_flags(flags: Map<enums::Dkim2Flag>) -> impl Iterator<Item = Flag> {
|
||||
flags.into_inner().into_iter().map(|flag| match flag {
|
||||
Dkim2Flag::Donotmodify => Flag::DoNotModify,
|
||||
Dkim2Flag::Donotexplode => Flag::DoNotExplode,
|
||||
Dkim2Flag::Feedback => Flag::Feedback,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rsa_key_parse(private_key: &[u8]) -> trc::Result<RsaKey<Sha256>> {
|
||||
PrivatePkcs1KeyDer::from_pem_slice(private_key)
|
||||
.map(PrivateKeyDer::Pkcs1)
|
||||
.or_else(|_| PrivatePkcs8KeyDer::from_pem_slice(private_key).map(PrivateKeyDer::Pkcs8))
|
||||
.map_err(|err| {
|
||||
trc::DkimEvent::BuildError
|
||||
.reason(err)
|
||||
.details("Failed to build RSA key")
|
||||
})
|
||||
.and_then(|key| {
|
||||
RsaKey::<Sha256>::from_key_der(key).map_err(|err| {
|
||||
trc::DkimEvent::BuildError
|
||||
.reason(err)
|
||||
.details("Failed to build RSA key")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn simple_pem_parse(contents: &str) -> Option<Vec<u8>> {
|
||||
let mut contents = contents.as_bytes().iter().copied();
|
||||
let mut base64 = vec![];
|
||||
|
||||
'outer: while let Some(ch) = contents.next() {
|
||||
if !ch.is_ascii_whitespace() {
|
||||
if ch == b'-' {
|
||||
for ch in contents.by_ref() {
|
||||
if ch == b'\n' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
base64.push(ch);
|
||||
}
|
||||
|
||||
for ch in contents.by_ref() {
|
||||
if ch == b'-' {
|
||||
break 'outer;
|
||||
} else if !ch.is_ascii_whitespace() {
|
||||
base64.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base64_decode(&base64)
|
||||
}
|
||||
|
||||
fn build_dkim1_signer<T: SigningKey>(
|
||||
domain: String,
|
||||
signature: Dkim1Signature,
|
||||
key: T,
|
||||
) -> mail_auth::dkim::DkimSigner<T, Done> {
|
||||
let mut signer = mail_auth::dkim::DkimSigner::from_key(key)
|
||||
.domain(domain)
|
||||
.selector(signature.selector)
|
||||
.headers(signature.headers)
|
||||
.reporting(signature.report);
|
||||
|
||||
match signature.canonicalization {
|
||||
enums::DkimCanonicalization::RelaxedRelaxed => {
|
||||
signer = signer
|
||||
.body_canonicalization(Canonicalization::Relaxed)
|
||||
.header_canonicalization(Canonicalization::Relaxed);
|
||||
}
|
||||
enums::DkimCanonicalization::SimpleSimple => {
|
||||
signer = signer
|
||||
.body_canonicalization(Canonicalization::Simple)
|
||||
.header_canonicalization(Canonicalization::Simple);
|
||||
}
|
||||
enums::DkimCanonicalization::RelaxedSimple => {
|
||||
signer = signer
|
||||
.body_canonicalization(Canonicalization::Simple)
|
||||
.header_canonicalization(Canonicalization::Relaxed);
|
||||
}
|
||||
enums::DkimCanonicalization::SimpleRelaxed => {
|
||||
signer = signer
|
||||
.body_canonicalization(Canonicalization::Relaxed)
|
||||
.header_canonicalization(Canonicalization::Simple);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(expire) = signature.expire {
|
||||
signer = signer.expiration(expire.into_inner().as_secs());
|
||||
}
|
||||
|
||||
if let Some(auid) = signature.auid {
|
||||
signer = signer.agent_user_identifier(auid);
|
||||
}
|
||||
|
||||
if let Some(atps) = signature.third_party {
|
||||
signer = signer.atps(atps);
|
||||
}
|
||||
|
||||
if let Some(atpsh) = signature.third_party_hash {
|
||||
signer = signer.atpsh(match atpsh {
|
||||
enums::DkimHash::Sha256 => HashAlgorithm::Sha256,
|
||||
enums::DkimHash::Sha1 => HashAlgorithm::Sha1,
|
||||
});
|
||||
}
|
||||
signer
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<expr::Variable<'x>> for VerifyStrategy {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: expr::Variable<'x>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
expr::Variable::Constant(c) => match c {
|
||||
ExpressionConstant::Relaxed => Ok(VerifyStrategy::Relaxed),
|
||||
ExpressionConstant::Strict => Ok(VerifyStrategy::Strict),
|
||||
ExpressionConstant::Disable => Ok(VerifyStrategy::Disable),
|
||||
_ => Err(()),
|
||||
},
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VerifyStrategy {
|
||||
#[inline(always)]
|
||||
pub fn verify(&self) -> bool {
|
||||
matches!(self, VerifyStrategy::Strict | VerifyStrategy::Relaxed)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_strict(&self) -> bool {
|
||||
matches!(self, VerifyStrategy::Strict)
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for Dkim1Signer {
|
||||
fn weight(&self) -> u64 {
|
||||
std::mem::size_of::<Self>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for DkimSigners {
|
||||
fn weight(&self) -> u64 {
|
||||
(std::mem::size_of::<Self>()
|
||||
+ self.dkim1.len() * std::mem::size_of::<Dkim1Signer>()
|
||||
+ std::mem::size_of::<Dkim2Signer<Dkim2Done>>()) as u64
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod auth;
|
||||
pub mod queue;
|
||||
pub mod report;
|
||||
pub mod resolver;
|
||||
pub mod session;
|
||||
|
||||
use self::{
|
||||
auth::MailAuthConfig, queue::QueueConfig, report::ReportConfig, resolver::Resolvers,
|
||||
session::SessionConfig,
|
||||
};
|
||||
use crate::{config::smtp::queue::RequireOptional, expr::if_block::IfBlock};
|
||||
use registry::{
|
||||
schema::{properties::ObjectType, structs::Rate},
|
||||
types::id::ObjectId,
|
||||
};
|
||||
use store::registry::bootstrap::Bootstrap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpConfig {
|
||||
pub session: SessionConfig,
|
||||
pub queue: QueueConfig,
|
||||
pub resolvers: Resolvers,
|
||||
pub mail_auth: MailAuthConfig,
|
||||
pub report: ReportConfig,
|
||||
pub mta_sts_client: reqwest::Client,
|
||||
pub tls_report_client: reqwest::Client,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
//#[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))]
|
||||
pub struct QueueRateLimiter {
|
||||
pub id: ObjectId,
|
||||
pub expr: IfBlock,
|
||||
pub keys: u16,
|
||||
pub rate: Rate,
|
||||
}
|
||||
|
||||
pub const THROTTLE_RCPT: u16 = 1 << 0;
|
||||
pub const THROTTLE_RCPT_DOMAIN: u16 = 1 << 1;
|
||||
pub const THROTTLE_SENDER: u16 = 1 << 2;
|
||||
pub const THROTTLE_SENDER_DOMAIN: u16 = 1 << 3;
|
||||
pub const THROTTLE_AUTH_AS: u16 = 1 << 4;
|
||||
pub const THROTTLE_LISTENER: u16 = 1 << 5;
|
||||
pub const THROTTLE_MX: u16 = 1 << 6;
|
||||
pub const THROTTLE_REMOTE_IP: u16 = 1 << 7;
|
||||
pub const THROTTLE_LOCAL_IP: u16 = 1 << 8;
|
||||
pub const THROTTLE_HELO_DOMAIN: u16 = 1 << 9;
|
||||
|
||||
impl SmtpConfig {
|
||||
pub async fn parse(bp: &mut Bootstrap) -> Self {
|
||||
let config = Self {
|
||||
session: SessionConfig::parse(bp).await,
|
||||
queue: QueueConfig::parse(bp).await,
|
||||
resolvers: Resolvers::parse(bp).await,
|
||||
mail_auth: MailAuthConfig::parse(bp).await,
|
||||
report: ReportConfig::parse(bp).await,
|
||||
mta_sts_client: utils::http::http_client_builder(false)
|
||||
.pool_max_idle_per_host(0)
|
||||
.user_agent(crate::USER_AGENT)
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.unwrap_or_default(),
|
||||
tls_report_client: utils::http::unpooled_http_client(false),
|
||||
};
|
||||
|
||||
if !config.resolvers.dnssec_available
|
||||
&& (config.queue.tls_strategy.is_empty()
|
||||
|| config
|
||||
.queue
|
||||
.tls_strategy
|
||||
.values()
|
||||
.any(|t| !matches!(t.dane, RequireOptional::Disable)))
|
||||
{
|
||||
bp.build_warning(
|
||||
ObjectType::DnsResolver.singleton(),
|
||||
concat!(
|
||||
"The configured DNS resolver cannot validate DNSSEC. ",
|
||||
"DANE has been disabled to avoid deferring mail. ",
|
||||
"Ensure the resolver is DNSSEC-capable and reachable over TCP."
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,801 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
config::server::ServerProtocol,
|
||||
expr::{
|
||||
if_block::{BootstrapExprExt, IfBlock},
|
||||
*,
|
||||
},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use directory::Credentials;
|
||||
use mail_auth::IpLookupStrategy;
|
||||
use registry::schema::{
|
||||
enums::{self, ExpressionConstant, ExpressionVariable, MtaRequiredOrOptional},
|
||||
prelude::ObjectType,
|
||||
structs::{
|
||||
DsnReportSettings, MtaConnectionStrategy, MtaDeliveryExpiration, MtaDeliverySchedule,
|
||||
MtaDeliveryScheduleIntervalsOrDefault, MtaInboundThrottle, MtaOutboundStrategy,
|
||||
MtaOutboundThrottle, MtaQueueQuota, MtaRoute, MtaTlsStrategy, MtaVirtualQueue,
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
hash::{Hash, Hasher},
|
||||
net::IpAddr,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
rkyv::Serialize,
|
||||
rkyv::Deserialize,
|
||||
rkyv::Archive,
|
||||
serde::Deserialize,
|
||||
)]
|
||||
#[rkyv(derive(Debug, Clone, Copy, PartialEq), compare(PartialEq))]
|
||||
#[repr(transparent)]
|
||||
pub struct QueueName([u8; 8]);
|
||||
|
||||
pub const DEFAULT_QUEUE_NAME: QueueName = QueueName([b'd', b'e', b'f', b'a', b'u', b'l', b't', 0]);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct QueueConfig {
|
||||
// Strategy resolver
|
||||
pub route: IfBlock,
|
||||
pub queue: IfBlock,
|
||||
pub connection: IfBlock,
|
||||
pub tls: IfBlock,
|
||||
|
||||
// DSN
|
||||
pub dsn: Dsn,
|
||||
|
||||
// Rate limits
|
||||
pub inbound_limiters: QueueRateLimiters,
|
||||
pub outbound_limiters: QueueRateLimiters,
|
||||
pub quota: QueueQuotas,
|
||||
|
||||
// Strategies
|
||||
pub queue_strategy: AHashMap<String, QueueStrategy>,
|
||||
pub connection_strategy: AHashMap<String, ConnectionStrategy>,
|
||||
pub routing_strategy: AHashMap<String, RoutingStrategy>,
|
||||
pub tls_strategy: AHashMap<String, TlsStrategy>,
|
||||
pub virtual_queues: AHashMap<QueueName, VirtualQueue>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Eq, Debug)]
|
||||
pub enum RoutingStrategy {
|
||||
Local,
|
||||
Mx(MxConfig),
|
||||
Relay(RelayConfig),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MxConfig {
|
||||
pub max_mx: usize,
|
||||
pub max_multi_homed: usize,
|
||||
pub ip_lookup_strategy: IpLookupStrategy,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Dsn {
|
||||
pub name: IfBlock,
|
||||
pub address: IfBlock,
|
||||
pub sign: IfBlock,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VirtualQueue {
|
||||
pub threads: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct QueueStrategy {
|
||||
pub retry: Vec<u64>,
|
||||
pub notify: Vec<u64>,
|
||||
pub expiry: QueueExpiry,
|
||||
pub virtual_queue: QueueName,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
rkyv::Serialize,
|
||||
rkyv::Deserialize,
|
||||
rkyv::Archive,
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
serde::Deserialize,
|
||||
)]
|
||||
pub enum QueueExpiry {
|
||||
Ttl(u64),
|
||||
Attempts(u32),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TlsStrategy {
|
||||
pub dane: RequireOptional,
|
||||
pub mta_sts: RequireOptional,
|
||||
pub tls: RequireOptional,
|
||||
pub allow_invalid_certs: bool,
|
||||
|
||||
pub timeout_tls: Duration,
|
||||
pub timeout_mta_sts: Duration,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConnectionStrategy {
|
||||
pub source_ipv4: Vec<IpAndHost>,
|
||||
pub source_ipv6: Vec<IpAndHost>,
|
||||
pub ehlo_hostname: Option<String>,
|
||||
|
||||
pub timeout_connect: Duration,
|
||||
pub timeout_greeting: Duration,
|
||||
pub timeout_ehlo: Duration,
|
||||
pub timeout_mail: Duration,
|
||||
pub timeout_rcpt: Duration,
|
||||
pub timeout_data: Duration,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IpAndHost {
|
||||
pub ip: IpAddr,
|
||||
pub host: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct QueueRateLimiters {
|
||||
pub sender: Vec<QueueRateLimiter>,
|
||||
pub rcpt: Vec<QueueRateLimiter>,
|
||||
pub remote: Vec<QueueRateLimiter>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct QueueQuotas {
|
||||
pub sender: Vec<QueueQuota>,
|
||||
pub rcpt: Vec<QueueQuota>,
|
||||
pub rcpt_domain: Vec<QueueQuota>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct QueueQuota {
|
||||
pub id: ObjectId,
|
||||
pub expr: IfBlock,
|
||||
pub keys: u16,
|
||||
pub size: Option<u64>,
|
||||
pub messages: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||
pub struct RelayConfig {
|
||||
pub address: HostOrIp<Box<str>, IpStr>,
|
||||
pub port: u16,
|
||||
pub protocol: ServerProtocol,
|
||||
pub auth: Option<Credentials>,
|
||||
pub tls_implicit: bool,
|
||||
pub tls_allow_invalid_certs: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub enum HostOrIp<N, I> {
|
||||
Host(N),
|
||||
Ip(I),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct IpStr {
|
||||
pub ip: IpAddr,
|
||||
pub ip_str: Box<str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub enum RequireOptional {
|
||||
#[default]
|
||||
Optional,
|
||||
Require,
|
||||
Disable,
|
||||
}
|
||||
|
||||
impl QueueConfig {
|
||||
pub async fn parse(bp: &mut Bootstrap) -> Self {
|
||||
let st = bp.setting_infallible::<MtaOutboundStrategy>().await;
|
||||
let dsn = bp.setting_infallible::<DsnReportSettings>().await;
|
||||
|
||||
let mut queue = QueueConfig {
|
||||
route: bp.compile_expr(ObjectType::MtaOutboundStrategy.singleton(), &st.ctx_route()),
|
||||
queue: bp.compile_expr(
|
||||
ObjectType::MtaOutboundStrategy.singleton(),
|
||||
&st.ctx_schedule(),
|
||||
),
|
||||
connection: bp.compile_expr(
|
||||
ObjectType::MtaOutboundStrategy.singleton(),
|
||||
&st.ctx_connection(),
|
||||
),
|
||||
tls: bp.compile_expr(ObjectType::MtaOutboundStrategy.singleton(), &st.ctx_tls()),
|
||||
dsn: Dsn {
|
||||
name: bp.compile_expr(
|
||||
ObjectType::DsnReportSettings.singleton(),
|
||||
&dsn.ctx_from_name(),
|
||||
),
|
||||
address: bp.compile_expr(
|
||||
ObjectType::DsnReportSettings.singleton(),
|
||||
&dsn.ctx_from_address(),
|
||||
),
|
||||
sign: bp.compile_expr(
|
||||
ObjectType::DsnReportSettings.singleton(),
|
||||
&dsn.ctx_dkim_sign_domain(),
|
||||
),
|
||||
},
|
||||
inbound_limiters: QueueRateLimiters::parse_inbound(bp).await,
|
||||
outbound_limiters: QueueRateLimiters::parse_outbound(bp).await,
|
||||
quota: QueueQuotas::parse(bp).await,
|
||||
queue_strategy: Default::default(),
|
||||
connection_strategy: Default::default(),
|
||||
routing_strategy: Default::default(),
|
||||
tls_strategy: Default::default(),
|
||||
virtual_queues: Default::default(),
|
||||
};
|
||||
|
||||
// Parse virtual queues
|
||||
let mut queue_id_to_name = AHashMap::new();
|
||||
for obj in bp.list_infallible::<MtaVirtualQueue>().await {
|
||||
if let Some(queue_name) = QueueName::new(&obj.object.name) {
|
||||
queue_id_to_name.insert(obj.id.id(), queue_name);
|
||||
queue.virtual_queues.insert(
|
||||
queue_name,
|
||||
VirtualQueue {
|
||||
threads: obj.object.threads_per_node as usize,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse queue strategies
|
||||
for obj in bp.list_infallible::<MtaDeliverySchedule>().await {
|
||||
let virtual_queue = if let Some(name) = queue_id_to_name.get(&obj.object.queue_id) {
|
||||
*name
|
||||
} else {
|
||||
bp.build_error(
|
||||
obj.id,
|
||||
format!("Virtual queue ID '{}' does not exist.", obj.object.queue_id),
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
queue.queue_strategy.insert(
|
||||
obj.object.name,
|
||||
QueueStrategy {
|
||||
retry: match obj.object.retry {
|
||||
MtaDeliveryScheduleIntervalsOrDefault::Default => vec![
|
||||
2 * 60,
|
||||
5 * 60,
|
||||
10 * 60,
|
||||
15 * 60,
|
||||
30 * 60,
|
||||
60 * 60,
|
||||
2 * 60 * 60,
|
||||
24 * 60 * 60,
|
||||
3 * 24 * 60 * 60,
|
||||
],
|
||||
MtaDeliveryScheduleIntervalsOrDefault::Custom(intervals) => intervals
|
||||
.intervals
|
||||
.into_iter()
|
||||
.map(|d| d.duration.as_secs())
|
||||
.collect(),
|
||||
},
|
||||
notify: match obj.object.notify {
|
||||
MtaDeliveryScheduleIntervalsOrDefault::Default => {
|
||||
vec![24 * 60 * 60, 3 * 24 * 60 * 60]
|
||||
}
|
||||
MtaDeliveryScheduleIntervalsOrDefault::Custom(intervals) => intervals
|
||||
.intervals
|
||||
.into_iter()
|
||||
.map(|d| d.duration.as_secs())
|
||||
.collect(),
|
||||
},
|
||||
expiry: match obj.object.expiry {
|
||||
MtaDeliveryExpiration::Ttl(exp) => {
|
||||
QueueExpiry::Ttl(exp.expire.into_inner().as_secs())
|
||||
}
|
||||
MtaDeliveryExpiration::Attempts(exp) => {
|
||||
QueueExpiry::Attempts(exp.max_attempts as u32)
|
||||
}
|
||||
},
|
||||
virtual_queue,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Parse connection strategies
|
||||
for obj in bp.list_infallible::<MtaConnectionStrategy>().await {
|
||||
let mut source_ipv4 = Vec::new();
|
||||
let mut source_ipv6 = Vec::new();
|
||||
|
||||
for ip_host in obj.object.source_ips {
|
||||
let ip_host = IpAndHost {
|
||||
ip: ip_host.source_ip.into_inner(),
|
||||
host: ip_host.ehlo_hostname,
|
||||
};
|
||||
if ip_host.ip.is_ipv4() {
|
||||
source_ipv4.push(ip_host);
|
||||
} else {
|
||||
source_ipv6.push(ip_host);
|
||||
}
|
||||
}
|
||||
|
||||
queue.connection_strategy.insert(
|
||||
obj.object.name,
|
||||
ConnectionStrategy {
|
||||
source_ipv4,
|
||||
source_ipv6,
|
||||
ehlo_hostname: obj.object.ehlo_hostname,
|
||||
timeout_connect: obj.object.connect_timeout.into_inner(),
|
||||
timeout_greeting: obj.object.greeting_timeout.into_inner(),
|
||||
timeout_ehlo: obj.object.ehlo_timeout.into_inner(),
|
||||
timeout_mail: obj.object.mail_from_timeout.into_inner(),
|
||||
timeout_rcpt: obj.object.rcpt_to_timeout.into_inner(),
|
||||
timeout_data: obj.object.data_timeout.into_inner(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Parse routing strategies
|
||||
for obj in bp.list_infallible::<MtaRoute>().await {
|
||||
match obj.object {
|
||||
MtaRoute::Mx(route) => {
|
||||
queue.routing_strategy.insert(
|
||||
route.name,
|
||||
RoutingStrategy::Mx(MxConfig {
|
||||
max_mx: route.max_mx_hosts as usize,
|
||||
max_multi_homed: route.max_multihomed as usize,
|
||||
ip_lookup_strategy: match route.ip_lookup_strategy {
|
||||
enums::MtaIpStrategy::V4ThenV6 => IpLookupStrategy::Ipv4thenIpv6,
|
||||
enums::MtaIpStrategy::V6ThenV4 => IpLookupStrategy::Ipv6thenIpv4,
|
||||
enums::MtaIpStrategy::V4Only => IpLookupStrategy::Ipv4Only,
|
||||
enums::MtaIpStrategy::V6Only => IpLookupStrategy::Ipv6Only,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
MtaRoute::Relay(route) => {
|
||||
let secret = route
|
||||
.auth_secret
|
||||
.secret()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
bp.build_error(obj.id, err);
|
||||
})
|
||||
.unwrap_or_default();
|
||||
queue.routing_strategy.insert(
|
||||
route.name,
|
||||
RoutingStrategy::Relay(RelayConfig {
|
||||
address: if let Ok(ip) = route.address.parse() {
|
||||
HostOrIp::Ip(IpStr {
|
||||
ip,
|
||||
ip_str: route.address.into(),
|
||||
})
|
||||
} else {
|
||||
HostOrIp::Host(route.address.into())
|
||||
},
|
||||
port: route.port as u16,
|
||||
protocol: match route.protocol {
|
||||
enums::MtaProtocol::Smtp => ServerProtocol::Smtp,
|
||||
enums::MtaProtocol::Lmtp => ServerProtocol::Lmtp,
|
||||
},
|
||||
auth: route.auth_username.zip(secret).map(|(user, secret)| {
|
||||
Credentials::Basic {
|
||||
username: user,
|
||||
secret: secret.into_owned(),
|
||||
mfa_token: None,
|
||||
}
|
||||
}),
|
||||
tls_implicit: route.implicit_tls,
|
||||
tls_allow_invalid_certs: route.allow_invalid_certs,
|
||||
}),
|
||||
);
|
||||
}
|
||||
MtaRoute::Local(route) => {
|
||||
queue
|
||||
.routing_strategy
|
||||
.insert(route.name, RoutingStrategy::Local);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse TLS strategies
|
||||
for obj in bp.list_infallible::<MtaTlsStrategy>().await {
|
||||
queue.tls_strategy.insert(
|
||||
obj.object.name,
|
||||
TlsStrategy {
|
||||
dane: match obj.object.dane {
|
||||
MtaRequiredOrOptional::Optional => RequireOptional::Optional,
|
||||
MtaRequiredOrOptional::Require => RequireOptional::Require,
|
||||
MtaRequiredOrOptional::Disable => RequireOptional::Disable,
|
||||
},
|
||||
mta_sts: match obj.object.mta_sts {
|
||||
MtaRequiredOrOptional::Optional => RequireOptional::Optional,
|
||||
MtaRequiredOrOptional::Require => RequireOptional::Require,
|
||||
MtaRequiredOrOptional::Disable => RequireOptional::Disable,
|
||||
},
|
||||
tls: match obj.object.start_tls {
|
||||
MtaRequiredOrOptional::Optional => RequireOptional::Optional,
|
||||
MtaRequiredOrOptional::Require => RequireOptional::Require,
|
||||
MtaRequiredOrOptional::Disable => RequireOptional::Disable,
|
||||
},
|
||||
allow_invalid_certs: obj.object.allow_invalid_certs,
|
||||
timeout_tls: obj.object.tls_timeout.into_inner(),
|
||||
timeout_mta_sts: obj.object.mta_sts_timeout.into_inner(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
queue
|
||||
}
|
||||
}
|
||||
|
||||
impl QueueRateLimiters {
|
||||
async fn parse_inbound(bp: &mut Bootstrap) -> QueueRateLimiters {
|
||||
let mut throttle = QueueRateLimiters::default();
|
||||
|
||||
for obj in bp.list_infallible::<MtaInboundThrottle>().await {
|
||||
if !obj.object.enable {
|
||||
continue;
|
||||
}
|
||||
|
||||
let limiter = QueueRateLimiter {
|
||||
expr: bp.compile_expr(obj.id, &obj.object.ctx_match_()),
|
||||
id: obj.id,
|
||||
keys: obj
|
||||
.object
|
||||
.key
|
||||
.iter()
|
||||
.map(|key| match key {
|
||||
enums::MtaInboundThrottleKey::Rcpt => THROTTLE_RCPT,
|
||||
enums::MtaInboundThrottleKey::RcptDomain => THROTTLE_RCPT_DOMAIN,
|
||||
enums::MtaInboundThrottleKey::Sender => THROTTLE_SENDER,
|
||||
enums::MtaInboundThrottleKey::SenderDomain => THROTTLE_SENDER_DOMAIN,
|
||||
enums::MtaInboundThrottleKey::AuthenticatedAs => THROTTLE_AUTH_AS,
|
||||
enums::MtaInboundThrottleKey::Listener => THROTTLE_LISTENER,
|
||||
enums::MtaInboundThrottleKey::RemoteIp => THROTTLE_REMOTE_IP,
|
||||
enums::MtaInboundThrottleKey::LocalIp => THROTTLE_LOCAL_IP,
|
||||
enums::MtaInboundThrottleKey::HeloDomain => THROTTLE_HELO_DOMAIN,
|
||||
})
|
||||
.fold(0, |acc, key| acc | key),
|
||||
rate: obj.object.rate,
|
||||
};
|
||||
|
||||
if (limiter.keys & (THROTTLE_RCPT | THROTTLE_RCPT_DOMAIN)) != 0
|
||||
|| limiter.expr.all_items().any(|c| {
|
||||
matches!(
|
||||
c,
|
||||
ExpressionItem::Variable(
|
||||
ExpressionVariable::Rcpt | ExpressionVariable::RcptDomain
|
||||
)
|
||||
)
|
||||
})
|
||||
{
|
||||
throttle.rcpt.push(limiter);
|
||||
} else if (limiter.keys
|
||||
& (THROTTLE_SENDER
|
||||
| THROTTLE_SENDER_DOMAIN
|
||||
| THROTTLE_HELO_DOMAIN
|
||||
| THROTTLE_AUTH_AS))
|
||||
!= 0
|
||||
|| limiter.expr.all_items().any(|c| {
|
||||
matches!(
|
||||
c,
|
||||
ExpressionItem::Variable(
|
||||
ExpressionVariable::Sender
|
||||
| ExpressionVariable::SenderDomain
|
||||
| ExpressionVariable::HeloDomain
|
||||
| ExpressionVariable::AuthenticatedAs
|
||||
)
|
||||
)
|
||||
})
|
||||
{
|
||||
throttle.sender.push(limiter);
|
||||
} else {
|
||||
throttle.remote.push(limiter);
|
||||
}
|
||||
}
|
||||
|
||||
throttle
|
||||
}
|
||||
|
||||
async fn parse_outbound(bp: &mut Bootstrap) -> QueueRateLimiters {
|
||||
// Parse throttle
|
||||
let mut throttle = QueueRateLimiters::default();
|
||||
|
||||
for obj in bp.list_infallible::<MtaOutboundThrottle>().await {
|
||||
if !obj.object.enable {
|
||||
continue;
|
||||
}
|
||||
|
||||
let limiter = QueueRateLimiter {
|
||||
expr: bp.compile_expr(obj.id, &obj.object.ctx_match_()),
|
||||
id: obj.id,
|
||||
keys: obj
|
||||
.object
|
||||
.key
|
||||
.iter()
|
||||
.map(|key| match key {
|
||||
enums::MtaOutboundThrottleKey::RcptDomain => THROTTLE_RCPT_DOMAIN,
|
||||
enums::MtaOutboundThrottleKey::Sender => THROTTLE_SENDER,
|
||||
enums::MtaOutboundThrottleKey::SenderDomain => THROTTLE_SENDER_DOMAIN,
|
||||
enums::MtaOutboundThrottleKey::Mx => THROTTLE_MX,
|
||||
enums::MtaOutboundThrottleKey::RemoteIp => THROTTLE_REMOTE_IP,
|
||||
enums::MtaOutboundThrottleKey::LocalIp => THROTTLE_LOCAL_IP,
|
||||
})
|
||||
.fold(0, |acc, key| acc | key),
|
||||
rate: obj.object.rate,
|
||||
};
|
||||
if (limiter.keys & (THROTTLE_MX | THROTTLE_REMOTE_IP | THROTTLE_LOCAL_IP)) != 0
|
||||
|| limiter.expr.all_items().any(|c| {
|
||||
matches!(
|
||||
c,
|
||||
ExpressionItem::Variable(
|
||||
ExpressionVariable::Mx
|
||||
| ExpressionVariable::RemoteIp
|
||||
| ExpressionVariable::LocalIp
|
||||
)
|
||||
)
|
||||
})
|
||||
{
|
||||
throttle.remote.push(limiter);
|
||||
} else if (limiter.keys & (THROTTLE_RCPT_DOMAIN)) != 0
|
||||
|| limiter
|
||||
.expr
|
||||
.all_items()
|
||||
.any(|c| matches!(c, ExpressionItem::Variable(ExpressionVariable::RcptDomain)))
|
||||
{
|
||||
throttle.rcpt.push(limiter);
|
||||
} else {
|
||||
throttle.sender.push(limiter);
|
||||
}
|
||||
}
|
||||
|
||||
throttle
|
||||
}
|
||||
}
|
||||
|
||||
impl QueueQuotas {
|
||||
async fn parse(bp: &mut Bootstrap) -> QueueQuotas {
|
||||
let mut capacities = QueueQuotas {
|
||||
sender: Vec::new(),
|
||||
rcpt: Vec::new(),
|
||||
rcpt_domain: Vec::new(),
|
||||
};
|
||||
|
||||
for obj in bp.list_infallible::<MtaQueueQuota>().await {
|
||||
if !obj.object.enable {
|
||||
continue;
|
||||
}
|
||||
|
||||
let quota = QueueQuota {
|
||||
expr: bp.compile_expr(obj.id, &obj.object.ctx_match_()),
|
||||
id: obj.id,
|
||||
keys: obj
|
||||
.object
|
||||
.key
|
||||
.iter()
|
||||
.map(|key| match key {
|
||||
enums::MtaQueueQuotaKey::Rcpt => THROTTLE_RCPT,
|
||||
enums::MtaQueueQuotaKey::RcptDomain => THROTTLE_RCPT_DOMAIN,
|
||||
enums::MtaQueueQuotaKey::Sender => THROTTLE_SENDER,
|
||||
enums::MtaQueueQuotaKey::SenderDomain => THROTTLE_SENDER_DOMAIN,
|
||||
})
|
||||
.fold(0, |acc, key| acc | key),
|
||||
size: obj.object.size,
|
||||
messages: obj.object.messages,
|
||||
};
|
||||
|
||||
if (quota.keys & THROTTLE_RCPT) != 0
|
||||
|| quota
|
||||
.expr
|
||||
.all_items()
|
||||
.any(|c| matches!(c, ExpressionItem::Variable(ExpressionVariable::Rcpt)))
|
||||
{
|
||||
capacities.rcpt.push(quota);
|
||||
} else if (quota.keys & THROTTLE_RCPT_DOMAIN) != 0
|
||||
|| quota
|
||||
.expr
|
||||
.all_items()
|
||||
.any(|c| matches!(c, ExpressionItem::Variable(ExpressionVariable::RcptDomain)))
|
||||
{
|
||||
capacities.rcpt_domain.push(quota);
|
||||
} else {
|
||||
capacities.sender.push(quota);
|
||||
}
|
||||
}
|
||||
|
||||
capacities
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for RequireOptional {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Variable::Constant(ExpressionConstant::Optional) => Ok(RequireOptional::Optional),
|
||||
Variable::Constant(ExpressionConstant::Require) => Ok(RequireOptional::Require),
|
||||
Variable::Constant(ExpressionConstant::Disable) => Ok(RequireOptional::Disable),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for IpLookupStrategy {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Variable::Constant(value) => match value {
|
||||
ExpressionConstant::Ipv4Only => Ok(IpLookupStrategy::Ipv4Only),
|
||||
ExpressionConstant::Ipv6Only => Ok(IpLookupStrategy::Ipv6Only),
|
||||
ExpressionConstant::Ipv6ThenIpv4 => Ok(IpLookupStrategy::Ipv6thenIpv4),
|
||||
ExpressionConstant::Ipv4ThenIpv6 => Ok(IpLookupStrategy::Ipv4thenIpv6),
|
||||
_ => Err(()),
|
||||
},
|
||||
Variable::String(value) => {
|
||||
match value.as_str() {
|
||||
"ipv4_only" => Ok(IpLookupStrategy::Ipv4Only),
|
||||
"ipv6_only" => Ok(IpLookupStrategy::Ipv6Only),
|
||||
//"ipv4_and_ipv6" => IpLookupStrategy::Ipv4AndIpv6,
|
||||
"ipv6_then_ipv4" => Ok(IpLookupStrategy::Ipv6thenIpv4),
|
||||
"ipv4_then_ipv6" => Ok(IpLookupStrategy::Ipv4thenIpv6),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RelayConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RelayConfig")
|
||||
.field("address", &self.address)
|
||||
.field("port", &self.port)
|
||||
.field("protocol", &self.protocol)
|
||||
.field("tls_implicit", &self.tls_implicit)
|
||||
.field("tls_allow_invalid_certs", &self.tls_allow_invalid_certs)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TlsStrategy {
|
||||
#[inline(always)]
|
||||
pub fn try_dane(&self) -> bool {
|
||||
matches!(
|
||||
self.dane,
|
||||
RequireOptional::Require | RequireOptional::Optional
|
||||
)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn try_start_tls(&self) -> bool {
|
||||
matches!(
|
||||
self.tls,
|
||||
RequireOptional::Require | RequireOptional::Optional
|
||||
)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_dane_required(&self) -> bool {
|
||||
matches!(self.dane, RequireOptional::Require)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn try_mta_sts(&self) -> bool {
|
||||
matches!(
|
||||
self.mta_sts,
|
||||
RequireOptional::Require | RequireOptional::Optional
|
||||
)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_mta_sts_required(&self) -> bool {
|
||||
matches!(self.mta_sts, RequireOptional::Require)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_tls_required(&self) -> bool {
|
||||
matches!(self.tls, RequireOptional::Require)
|
||||
|| self.is_dane_required()
|
||||
|| self.is_mta_sts_required()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for MxConfig {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.max_mx.hash(state);
|
||||
self.max_multi_homed.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for MxConfig {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.max_mx == other.max_mx && self.max_multi_homed == other.max_multi_homed
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for MxConfig {}
|
||||
|
||||
impl QueueName {
|
||||
pub fn new(name: impl AsRef<[u8]>) -> Option<Self> {
|
||||
let name_bytes = name.as_ref();
|
||||
if (1..=8).contains(&name_bytes.len()) {
|
||||
let mut bytes = [0; 8];
|
||||
bytes[..name_bytes.len()].copy_from_slice(name_bytes);
|
||||
QueueName(bytes).into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bytes(name: &[u8]) -> Option<Self> {
|
||||
name.try_into().ok().map(|bytes: [u8; 8]| QueueName(bytes))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
std::str::from_utf8(&self.0)
|
||||
.unwrap_or_default()
|
||||
.trim_end_matches('\0')
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> [u8; 8] {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ArchivedQueueName {
|
||||
pub fn as_str(&self) -> &str {
|
||||
std::str::from_utf8(self.0.as_ref())
|
||||
.unwrap_or_default()
|
||||
.trim_end_matches('\0')
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for QueueName {
|
||||
fn default() -> Self {
|
||||
DEFAULT_QUEUE_NAME
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for QueueName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.as_str().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ArchivedQueueName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.as_str().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for QueueName {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::*;
|
||||
use crate::expr::{
|
||||
Variable,
|
||||
if_block::{BootstrapExprExt, IfBlock},
|
||||
};
|
||||
use registry::schema::{
|
||||
enums::ExpressionConstant,
|
||||
prelude::ObjectType,
|
||||
structs::{
|
||||
DataRetention, DkimReportSettings, DmarcReportSettings, ReportSettings, SpfReportSettings,
|
||||
TlsReportSettings,
|
||||
},
|
||||
};
|
||||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ReportConfig {
|
||||
pub submitter: IfBlock,
|
||||
pub analysis: ReportAnalysis,
|
||||
|
||||
pub dkim: Report,
|
||||
pub spf: Report,
|
||||
pub dmarc: Report,
|
||||
pub dmarc_aggregate: AggregateReport,
|
||||
pub tls: AggregateReport,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ReportAnalysis {
|
||||
pub addresses: Vec<AddressMatch>,
|
||||
pub forward: bool,
|
||||
pub store: Option<Duration>,
|
||||
pub max_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AddressMatch {
|
||||
StartsWith(String),
|
||||
EndsWith(String),
|
||||
Equals(String),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AggregateReport {
|
||||
pub name: IfBlock,
|
||||
pub address: IfBlock,
|
||||
pub org_name: IfBlock,
|
||||
pub contact_info: IfBlock,
|
||||
pub send: IfBlock,
|
||||
pub sign: IfBlock,
|
||||
pub max_size: IfBlock,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Report {
|
||||
pub name: IfBlock,
|
||||
pub address: IfBlock,
|
||||
pub subject: IfBlock,
|
||||
pub sign: IfBlock,
|
||||
pub send: IfBlock,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum AggregateFrequency {
|
||||
Hourly,
|
||||
Daily,
|
||||
Weekly,
|
||||
#[default]
|
||||
Never,
|
||||
}
|
||||
|
||||
impl ReportConfig {
|
||||
pub async fn parse(bp: &mut Bootstrap) -> Self {
|
||||
let report = bp.setting_infallible::<ReportSettings>().await;
|
||||
let dkim = bp.setting_infallible::<DkimReportSettings>().await;
|
||||
let spf = bp.setting_infallible::<SpfReportSettings>().await;
|
||||
let dmarc = bp.setting_infallible::<DmarcReportSettings>().await;
|
||||
let tls = bp.setting_infallible::<TlsReportSettings>().await;
|
||||
let dr = bp.setting_infallible::<DataRetention>().await;
|
||||
|
||||
ReportConfig {
|
||||
submitter: bp.compile_expr(
|
||||
ObjectType::ReportSettings.singleton(),
|
||||
&report.ctx_outbound_report_submitter(),
|
||||
),
|
||||
analysis: ReportAnalysis {
|
||||
addresses: report
|
||||
.inbound_report_addresses
|
||||
.iter()
|
||||
.filter_map(|addr| AddressMatch::from_str(addr).ok())
|
||||
.collect(),
|
||||
forward: report.inbound_report_forwarding,
|
||||
store: dr.hold_mta_reports_for.map(|d| d.into_inner()),
|
||||
max_size: std::cmp::max(report.inbound_report_max_size, 1024) as usize,
|
||||
},
|
||||
dkim: Report {
|
||||
name: bp.compile_expr(
|
||||
ObjectType::DkimReportSettings.singleton(),
|
||||
&dkim.ctx_from_name(),
|
||||
),
|
||||
address: bp.compile_expr(
|
||||
ObjectType::DkimReportSettings.singleton(),
|
||||
&dkim.ctx_from_address(),
|
||||
),
|
||||
subject: bp.compile_expr(
|
||||
ObjectType::DkimReportSettings.singleton(),
|
||||
&dkim.ctx_subject(),
|
||||
),
|
||||
sign: bp.compile_expr(
|
||||
ObjectType::DkimReportSettings.singleton(),
|
||||
&dkim.ctx_dkim_sign_domain(),
|
||||
),
|
||||
send: bp.compile_expr(
|
||||
ObjectType::DkimReportSettings.singleton(),
|
||||
&dkim.ctx_send_frequency(),
|
||||
),
|
||||
},
|
||||
spf: Report {
|
||||
name: bp.compile_expr(
|
||||
ObjectType::SpfReportSettings.singleton(),
|
||||
&spf.ctx_from_name(),
|
||||
),
|
||||
address: bp.compile_expr(
|
||||
ObjectType::SpfReportSettings.singleton(),
|
||||
&spf.ctx_from_address(),
|
||||
),
|
||||
subject: bp.compile_expr(
|
||||
ObjectType::SpfReportSettings.singleton(),
|
||||
&spf.ctx_subject(),
|
||||
),
|
||||
sign: bp.compile_expr(
|
||||
ObjectType::SpfReportSettings.singleton(),
|
||||
&spf.ctx_dkim_sign_domain(),
|
||||
),
|
||||
send: bp.compile_expr(
|
||||
ObjectType::SpfReportSettings.singleton(),
|
||||
&spf.ctx_send_frequency(),
|
||||
),
|
||||
},
|
||||
dmarc: Report {
|
||||
name: bp.compile_expr(
|
||||
ObjectType::DmarcReportSettings.singleton(),
|
||||
&dmarc.ctx_failure_from_name(),
|
||||
),
|
||||
address: bp.compile_expr(
|
||||
ObjectType::DmarcReportSettings.singleton(),
|
||||
&dmarc.ctx_failure_from_address(),
|
||||
),
|
||||
subject: bp.compile_expr(
|
||||
ObjectType::DmarcReportSettings.singleton(),
|
||||
&dmarc.ctx_failure_subject(),
|
||||
),
|
||||
sign: bp.compile_expr(
|
||||
ObjectType::DmarcReportSettings.singleton(),
|
||||
&dmarc.ctx_failure_dkim_sign_domain(),
|
||||
),
|
||||
send: bp.compile_expr(
|
||||
ObjectType::DmarcReportSettings.singleton(),
|
||||
&dmarc.ctx_failure_send_frequency(),
|
||||
),
|
||||
},
|
||||
dmarc_aggregate: AggregateReport {
|
||||
name: bp.compile_expr(
|
||||
ObjectType::DmarcReportSettings.singleton(),
|
||||
&dmarc.ctx_aggregate_from_name(),
|
||||
),
|
||||
address: bp.compile_expr(
|
||||
ObjectType::DmarcReportSettings.singleton(),
|
||||
&dmarc.ctx_aggregate_from_address(),
|
||||
),
|
||||
org_name: bp.compile_expr(
|
||||
ObjectType::DmarcReportSettings.singleton(),
|
||||
&dmarc.ctx_aggregate_org_name(),
|
||||
),
|
||||
contact_info: bp.compile_expr(
|
||||
ObjectType::DmarcReportSettings.singleton(),
|
||||
&dmarc.ctx_aggregate_contact_info(),
|
||||
),
|
||||
send: bp.compile_expr(
|
||||
ObjectType::DmarcReportSettings.singleton(),
|
||||
&dmarc.ctx_aggregate_send_frequency(),
|
||||
),
|
||||
sign: bp.compile_expr(
|
||||
ObjectType::DmarcReportSettings.singleton(),
|
||||
&dmarc.ctx_aggregate_dkim_sign_domain(),
|
||||
),
|
||||
max_size: bp.compile_expr(
|
||||
ObjectType::DmarcReportSettings.singleton(),
|
||||
&dmarc.ctx_aggregate_max_report_size(),
|
||||
),
|
||||
},
|
||||
tls: AggregateReport {
|
||||
name: bp.compile_expr(
|
||||
ObjectType::TlsReportSettings.singleton(),
|
||||
&tls.ctx_from_name(),
|
||||
),
|
||||
address: bp.compile_expr(
|
||||
ObjectType::TlsReportSettings.singleton(),
|
||||
&tls.ctx_from_address(),
|
||||
),
|
||||
org_name: bp.compile_expr(
|
||||
ObjectType::TlsReportSettings.singleton(),
|
||||
&tls.ctx_org_name(),
|
||||
),
|
||||
contact_info: bp.compile_expr(
|
||||
ObjectType::TlsReportSettings.singleton(),
|
||||
&tls.ctx_contact_info(),
|
||||
),
|
||||
send: bp.compile_expr(
|
||||
ObjectType::TlsReportSettings.singleton(),
|
||||
&tls.ctx_send_frequency(),
|
||||
),
|
||||
sign: bp.compile_expr(
|
||||
ObjectType::TlsReportSettings.singleton(),
|
||||
&tls.ctx_dkim_sign_domain(),
|
||||
),
|
||||
max_size: bp.compile_expr(
|
||||
ObjectType::TlsReportSettings.singleton(),
|
||||
&tls.ctx_max_report_size(),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for AggregateFrequency {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Variable::Constant(ExpressionConstant::Disable) => Ok(AggregateFrequency::Never),
|
||||
Variable::Constant(ExpressionConstant::Hourly) => Ok(AggregateFrequency::Hourly),
|
||||
Variable::Constant(ExpressionConstant::Daily) => Ok(AggregateFrequency::Daily),
|
||||
Variable::Constant(ExpressionConstant::Weekly) => Ok(AggregateFrequency::Weekly),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReportAnalysis {
|
||||
pub fn is_report_address(&self, address: &str) -> bool {
|
||||
self.addresses.iter().any(|addr_match| match addr_match {
|
||||
AddressMatch::StartsWith(prefix) => address.starts_with(prefix),
|
||||
AddressMatch::EndsWith(suffix) => address.ends_with(suffix),
|
||||
AddressMatch::Equals(value) => address == value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AddressMatch {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
if let Some(value) = value.strip_prefix('*').map(|v| v.trim()) {
|
||||
if !value.is_empty() {
|
||||
return Ok(AddressMatch::EndsWith(value.to_lowercase()));
|
||||
}
|
||||
} else if let Some(value) = value.strip_suffix('*').map(|v| v.trim()) {
|
||||
if !value.is_empty() {
|
||||
return Ok(AddressMatch::StartsWith(value.to_lowercase()));
|
||||
}
|
||||
} else if value.contains('@') {
|
||||
return Ok(AddressMatch::Equals(value.trim().to_lowercase()));
|
||||
}
|
||||
Err(format!("Invalid address match value {:?}.", value,))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use ahash::AHashMap;
|
||||
use mail_auth::{
|
||||
MessageAuthenticator,
|
||||
hickory_resolver::{
|
||||
TokioResolver,
|
||||
config::{
|
||||
CLOUDFLARE, ConnectionConfig, GOOGLE, NameServerConfig, ProtocolConfig, QUAD9,
|
||||
ResolverConfig, ResolverOpts,
|
||||
},
|
||||
net::runtime::TokioRuntimeProvider,
|
||||
system_conf::read_system_conf,
|
||||
},
|
||||
};
|
||||
use registry::schema::{
|
||||
enums::{DnsResolverProtocol, PolicyEnforcement},
|
||||
prelude::ObjectType,
|
||||
structs::{DnsResolver, MtaSts, SystemSettings},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
net::IpAddr,
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
use store::registry::bootstrap::Bootstrap;
|
||||
use utils::cache::CacheItemWeight;
|
||||
|
||||
pub struct Resolvers {
|
||||
pub dns: MessageAuthenticator,
|
||||
pub dnssec: DnssecResolver,
|
||||
pub dnssec_available: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DnssecResolver {
|
||||
pub resolver: TokioResolver,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TlsaMatching {
|
||||
Full,
|
||||
Sha256,
|
||||
Sha512,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TlsaEntry {
|
||||
pub is_end_entity: bool,
|
||||
pub is_spki: bool,
|
||||
pub matching: TlsaMatching,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Tlsa {
|
||||
pub entries: Vec<TlsaEntry>,
|
||||
pub has_end_entities: bool,
|
||||
pub has_intermediates: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Mode {
|
||||
Enforce,
|
||||
Testing,
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum MxPattern {
|
||||
Equals(String),
|
||||
StartsWith(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
|
||||
pub struct Policy {
|
||||
pub id: String,
|
||||
pub mode: Mode,
|
||||
pub mx: Box<[MxPattern]>,
|
||||
pub max_age: u64,
|
||||
}
|
||||
|
||||
impl CacheItemWeight for Tlsa {
|
||||
fn weight(&self) -> u64 {
|
||||
self.entries
|
||||
.iter()
|
||||
.map(|entry| (entry.data.len() + std::mem::size_of::<TlsaEntry>()) as u64)
|
||||
.sum::<u64>()
|
||||
+ std::mem::size_of::<Tlsa>() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheItemWeight for Policy {
|
||||
fn weight(&self) -> u64 {
|
||||
(std::mem::size_of::<Policy>()
|
||||
+ self
|
||||
.mx
|
||||
.iter()
|
||||
.map(|mx| match mx {
|
||||
MxPattern::Equals(t) => t.len(),
|
||||
MxPattern::StartsWith(t) => t.len(),
|
||||
})
|
||||
.sum::<usize>()) as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolvers {
|
||||
pub async fn parse(bp: &mut Bootstrap) -> Self {
|
||||
let mut resolver_config: ResolverConfig;
|
||||
let mut opts = ResolverOpts::default();
|
||||
|
||||
match bp.setting_infallible::<DnsResolver>().await {
|
||||
DnsResolver::System(resolver) => match read_system_conf() {
|
||||
Ok((config, options)) => {
|
||||
resolver_config = config;
|
||||
opts = options;
|
||||
opts.num_concurrent_reqs = resolver.concurrency as usize;
|
||||
opts.timeout = resolver.timeout.into_inner();
|
||||
opts.preserve_intermediates = resolver.preserve_intermediates;
|
||||
opts.try_tcp_on_error = resolver.tcp_on_error;
|
||||
opts.attempts = resolver.attempts as usize;
|
||||
opts.edns0 = resolver.enable_edns;
|
||||
}
|
||||
Err(err) => {
|
||||
bp.build_error(
|
||||
ObjectType::DnsResolver.singleton(),
|
||||
format!("Failed to read system DNS config: {err}"),
|
||||
);
|
||||
resolver_config = ResolverConfig::udp_and_tcp(&CLOUDFLARE);
|
||||
}
|
||||
},
|
||||
DnsResolver::Custom(resolver) => {
|
||||
resolver_config = ResolverConfig::default();
|
||||
let mut nameservers: AHashMap<IpAddr, Vec<ConnectionConfig>> = AHashMap::new();
|
||||
|
||||
for server in resolver.servers {
|
||||
let ip = server.address.into_inner();
|
||||
let port = server.port as u16;
|
||||
let protocol = match server.protocol {
|
||||
DnsResolverProtocol::Udp => ProtocolConfig::Udp,
|
||||
DnsResolverProtocol::Tcp => ProtocolConfig::Tcp,
|
||||
DnsResolverProtocol::Tls => ProtocolConfig::Tls {
|
||||
server_name: Arc::from(server.address.to_string()),
|
||||
},
|
||||
};
|
||||
let mut connection = ConnectionConfig::new(protocol);
|
||||
connection.port = port;
|
||||
nameservers.entry(ip).or_default().push(connection);
|
||||
}
|
||||
|
||||
for (ip, connections) in nameservers {
|
||||
resolver_config.add_name_server(NameServerConfig::new(ip, true, connections));
|
||||
}
|
||||
|
||||
opts.num_concurrent_reqs = resolver.concurrency as usize;
|
||||
opts.timeout = resolver.timeout.into_inner();
|
||||
opts.preserve_intermediates = resolver.preserve_intermediates;
|
||||
opts.try_tcp_on_error = resolver.tcp_on_error;
|
||||
opts.attempts = resolver.attempts as usize;
|
||||
opts.edns0 = resolver.enable_edns;
|
||||
}
|
||||
DnsResolver::Cloudflare(resolver) => {
|
||||
resolver_config = if resolver.use_tls {
|
||||
ResolverConfig::tls(&CLOUDFLARE)
|
||||
} else {
|
||||
ResolverConfig::udp_and_tcp(&CLOUDFLARE)
|
||||
};
|
||||
|
||||
opts.num_concurrent_reqs = resolver.concurrency as usize;
|
||||
opts.timeout = resolver.timeout.into_inner();
|
||||
opts.preserve_intermediates = resolver.preserve_intermediates;
|
||||
opts.try_tcp_on_error = resolver.tcp_on_error;
|
||||
opts.attempts = resolver.attempts as usize;
|
||||
opts.edns0 = resolver.enable_edns;
|
||||
}
|
||||
DnsResolver::Quad9(resolver) => {
|
||||
resolver_config = if resolver.use_tls {
|
||||
ResolverConfig::tls(&QUAD9)
|
||||
} else {
|
||||
ResolverConfig::udp_and_tcp(&QUAD9)
|
||||
};
|
||||
opts.num_concurrent_reqs = resolver.concurrency as usize;
|
||||
opts.timeout = resolver.timeout.into_inner();
|
||||
opts.preserve_intermediates = resolver.preserve_intermediates;
|
||||
opts.try_tcp_on_error = resolver.tcp_on_error;
|
||||
opts.attempts = resolver.attempts as usize;
|
||||
opts.edns0 = resolver.enable_edns;
|
||||
}
|
||||
DnsResolver::Google(resolver) => {
|
||||
resolver_config = ResolverConfig::udp_and_tcp(&GOOGLE);
|
||||
opts.num_concurrent_reqs = resolver.concurrency as usize;
|
||||
opts.timeout = resolver.timeout.into_inner();
|
||||
opts.preserve_intermediates = resolver.preserve_intermediates;
|
||||
opts.try_tcp_on_error = resolver.tcp_on_error;
|
||||
opts.attempts = resolver.attempts as usize;
|
||||
opts.edns0 = resolver.enable_edns;
|
||||
}
|
||||
}
|
||||
|
||||
// We already have a cache, so disable the built-in cache
|
||||
opts.cache_size = 0;
|
||||
|
||||
// Prepare DNSSEC resolver options
|
||||
let config_dnssec = resolver_config.clone();
|
||||
let mut opts_dnssec = opts.clone();
|
||||
opts_dnssec.validate = true;
|
||||
|
||||
let dnssec = DnssecResolver {
|
||||
resolver: TokioResolver::builder_with_config(
|
||||
config_dnssec,
|
||||
TokioRuntimeProvider::default(),
|
||||
)
|
||||
.with_options(opts_dnssec)
|
||||
.build()
|
||||
.expect("Failed to build DNSSEC resolver"),
|
||||
};
|
||||
|
||||
Resolvers {
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
dnssec_available: ensure_dnssec(&resolver_config, &dnssec.resolver).await,
|
||||
#[cfg(feature = "test_mode")]
|
||||
dnssec_available: true,
|
||||
dns: MessageAuthenticator::new(resolver_config, opts).unwrap(),
|
||||
dnssec,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
async fn ensure_dnssec(config: &ResolverConfig, resolver: &TokioResolver) -> bool {
|
||||
config.name_servers().iter().any(|name_server| {
|
||||
name_server
|
||||
.connections
|
||||
.iter()
|
||||
.any(|connection| !matches!(connection.protocol, ProtocolConfig::Udp))
|
||||
}) && resolver
|
||||
.lookup(
|
||||
hickory_proto::rr::Name::root(),
|
||||
hickory_proto::rr::RecordType::DNSKEY,
|
||||
)
|
||||
.await
|
||||
.is_ok_and(|lookup| {
|
||||
lookup
|
||||
.answers()
|
||||
.iter()
|
||||
.any(|record| record.proof.is_secure())
|
||||
})
|
||||
}
|
||||
|
||||
impl Policy {
|
||||
pub async fn try_parse(bp: &mut Bootstrap) -> Option<Self> {
|
||||
let mta = bp.setting_infallible::<MtaSts>().await;
|
||||
|
||||
if matches!(mta.mode, PolicyEnforcement::Disable) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut mx_hosts = mta.mx_hosts.into_inner();
|
||||
|
||||
if mx_hosts.is_empty() {
|
||||
let settings = bp.setting_infallible::<SystemSettings>().await;
|
||||
let default_host = settings.default_hostname.as_str();
|
||||
mx_hosts = settings
|
||||
.mail_exchangers
|
||||
.iter()
|
||||
.map(|mx| mx.hostname.as_deref().unwrap_or(default_host).to_string())
|
||||
.collect();
|
||||
}
|
||||
|
||||
if !mx_hosts.is_empty() {
|
||||
mx_hosts.sort_unstable();
|
||||
mx_hosts.dedup();
|
||||
|
||||
let mut policy = Policy {
|
||||
id: Default::default(),
|
||||
mode: match mta.mode {
|
||||
PolicyEnforcement::Enforce => Mode::Enforce,
|
||||
PolicyEnforcement::Testing => Mode::Testing,
|
||||
PolicyEnforcement::Disable => Mode::None,
|
||||
},
|
||||
mx: mx_hosts
|
||||
.into_iter()
|
||||
.map(|mx| {
|
||||
if let Some(mx) = mx.strip_prefix("*.") {
|
||||
MxPattern::StartsWith(mx.to_string())
|
||||
} else {
|
||||
MxPattern::Equals(mx)
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
max_age: mta.max_age.into_inner().as_secs(),
|
||||
};
|
||||
|
||||
policy.id = policy.hash().to_string();
|
||||
|
||||
Some(policy)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn hash(&self) -> u64 {
|
||||
let mut s = DefaultHasher::new();
|
||||
self.mode.hash(&mut s);
|
||||
self.max_age.hash(&mut s);
|
||||
self.mx.hash(&mut s);
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Mode {
|
||||
type Err = String;
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"enforce" => Ok(Self::Enforce),
|
||||
"testing" | "test" => Ok(Self::Testing),
|
||||
"none" => Ok(Self::None),
|
||||
_ => Err(format!("Invalid mode value {value:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Resolvers {
|
||||
fn default() -> Self {
|
||||
let (config, opts) = match read_system_conf() {
|
||||
Ok(conf) => conf,
|
||||
Err(_) => (
|
||||
ResolverConfig::udp_and_tcp(&CLOUDFLARE),
|
||||
ResolverOpts::default(),
|
||||
),
|
||||
};
|
||||
|
||||
let config_dnssec = config.clone();
|
||||
let mut opts_dnssec = opts.clone();
|
||||
opts_dnssec.validate = true;
|
||||
|
||||
Self {
|
||||
dns: MessageAuthenticator::new(config, opts).expect("Failed to build DNS resolver"),
|
||||
dnssec: DnssecResolver {
|
||||
resolver: TokioResolver::builder_with_config(
|
||||
config_dnssec,
|
||||
TokioRuntimeProvider::default(),
|
||||
)
|
||||
.with_options(opts_dnssec)
|
||||
.build()
|
||||
.expect("Failed to build DNSSEC resolver"),
|
||||
},
|
||||
dnssec_available: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Policy {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("version: STSv1\r\n")?;
|
||||
f.write_str("mode: ")?;
|
||||
match self.mode {
|
||||
Mode::Enforce => f.write_str("enforce")?,
|
||||
Mode::Testing => f.write_str("testing")?,
|
||||
Mode::None => f.write_str("none")?,
|
||||
}
|
||||
f.write_str("\r\nmax_age: ")?;
|
||||
self.max_age.fmt(f)?;
|
||||
f.write_str("\r\n")?;
|
||||
|
||||
for mx in &self.mx {
|
||||
f.write_str("mx: ")?;
|
||||
mx.fmt(f)?;
|
||||
f.write_str("\r\n")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MxPattern {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MxPattern::Equals(mx) => f.write_str(mx),
|
||||
MxPattern::StartsWith(mx) => {
|
||||
f.write_str("*.")?;
|
||||
f.write_str(mx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Resolvers {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
dns: self.dns.clone(),
|
||||
dnssec: self.dnssec.clone(),
|
||||
dnssec_available: self.dnssec_available,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use self::resolver::Policy;
|
||||
use super::*;
|
||||
use crate::expr::{
|
||||
Variable,
|
||||
if_block::{BootstrapExprExt, IfBlock},
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
use hyper::HeaderMap;
|
||||
use registry::schema::{
|
||||
enums::{self, ExpressionConstant, MtaStage},
|
||||
prelude::ObjectType,
|
||||
structs::{
|
||||
MtaExtensions, MtaHook, MtaInboundSession, MtaMilter, MtaStageAuth, MtaStageConnect,
|
||||
MtaStageData, MtaStageEhlo, MtaStageMail, MtaStageRcpt,
|
||||
},
|
||||
};
|
||||
use smtp_proto::*;
|
||||
use std::{
|
||||
net::{SocketAddr, ToSocketAddrs},
|
||||
str::FromStr,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SessionConfig {
|
||||
pub timeout: IfBlock,
|
||||
pub duration: IfBlock,
|
||||
pub transfer_limit: IfBlock,
|
||||
|
||||
pub connect: Connect,
|
||||
pub ehlo: Ehlo,
|
||||
pub auth: Auth,
|
||||
pub mail: Mail,
|
||||
pub rcpt: Rcpt,
|
||||
pub data: Data,
|
||||
pub extensions: Extensions,
|
||||
pub mta_sts_policy: Option<Policy>,
|
||||
|
||||
pub milters: Vec<Milter>,
|
||||
pub hooks: Vec<MTAHook>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Connect {
|
||||
pub hostname: IfBlock,
|
||||
pub script: IfBlock,
|
||||
pub greeting: IfBlock,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Ehlo {
|
||||
pub script: IfBlock,
|
||||
pub require: IfBlock,
|
||||
pub reject_non_fqdn: IfBlock,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Extensions {
|
||||
pub pipelining: IfBlock,
|
||||
pub chunking: IfBlock,
|
||||
pub requiretls: IfBlock,
|
||||
pub dsn: IfBlock,
|
||||
pub vrfy: IfBlock,
|
||||
pub expn: IfBlock,
|
||||
pub no_soliciting: IfBlock,
|
||||
pub future_release: IfBlock,
|
||||
pub deliver_by: IfBlock,
|
||||
pub mt_priority: IfBlock,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Auth {
|
||||
pub mechanisms: IfBlock,
|
||||
pub require: IfBlock,
|
||||
pub must_match_sender: IfBlock,
|
||||
pub errors_max: IfBlock,
|
||||
pub errors_wait: IfBlock,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Mail {
|
||||
pub script: IfBlock,
|
||||
pub rewrite: IfBlock,
|
||||
pub is_allowed: IfBlock,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Rcpt {
|
||||
pub script: IfBlock,
|
||||
pub relay: IfBlock,
|
||||
pub rewrite: IfBlock,
|
||||
pub errors_max: IfBlock,
|
||||
pub errors_wait: IfBlock,
|
||||
pub max_recipients: IfBlock,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub enum AddressMapping {
|
||||
Enable,
|
||||
Custom(IfBlock),
|
||||
#[default]
|
||||
Disable,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Data {
|
||||
pub script: IfBlock,
|
||||
pub spam_filter: IfBlock,
|
||||
pub max_messages: IfBlock,
|
||||
pub max_message_size: IfBlock,
|
||||
pub max_received_headers: IfBlock,
|
||||
pub add_received: IfBlock,
|
||||
pub add_received_spf: IfBlock,
|
||||
pub add_return_path: IfBlock,
|
||||
pub add_auth_results: IfBlock,
|
||||
pub add_message_id: IfBlock,
|
||||
pub add_date: IfBlock,
|
||||
pub add_delivered_to: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Milter {
|
||||
pub enable: IfBlock,
|
||||
pub id: ObjectId,
|
||||
pub addrs: Vec<SocketAddr>,
|
||||
pub hostname: String,
|
||||
pub port: u16,
|
||||
pub timeout_connect: Duration,
|
||||
pub timeout_command: Duration,
|
||||
pub timeout_data: Duration,
|
||||
pub tls: bool,
|
||||
pub tls_allow_invalid_certs: bool,
|
||||
pub tempfail_on_error: bool,
|
||||
pub max_frame_len: usize,
|
||||
pub protocol_version: MilterVersion,
|
||||
pub flags_actions: Option<u32>,
|
||||
pub flags_protocol: Option<u32>,
|
||||
pub run_on_stage: AHashSet<Stage>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum MilterVersion {
|
||||
V2,
|
||||
V6,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MTAHook {
|
||||
pub enable: IfBlock,
|
||||
pub id: ObjectId,
|
||||
pub url: String,
|
||||
pub timeout: Duration,
|
||||
pub headers: HeaderMap,
|
||||
pub tls_allow_invalid_certs: bool,
|
||||
pub tempfail_on_error: bool,
|
||||
pub run_on_stage: AHashSet<Stage>,
|
||||
pub max_response_size: usize,
|
||||
pub client: reqwest::Client,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Stage {
|
||||
Connect,
|
||||
Ehlo,
|
||||
Auth,
|
||||
Mail,
|
||||
Rcpt,
|
||||
Data,
|
||||
}
|
||||
|
||||
impl SessionConfig {
|
||||
pub async fn parse(bp: &mut Bootstrap) -> Self {
|
||||
let session = bp.setting_infallible::<MtaInboundSession>().await;
|
||||
let connect = bp.setting_infallible::<MtaStageConnect>().await;
|
||||
let auth = bp.setting_infallible::<MtaStageAuth>().await;
|
||||
let ehlo = bp.setting_infallible::<MtaStageEhlo>().await;
|
||||
let mail = bp.setting_infallible::<MtaStageMail>().await;
|
||||
let rcpt = bp.setting_infallible::<MtaStageRcpt>().await;
|
||||
let data = bp.setting_infallible::<MtaStageData>().await;
|
||||
let ext = bp.setting_infallible::<MtaExtensions>().await;
|
||||
|
||||
let mut hooks = Vec::new();
|
||||
|
||||
for hook in bp.list_infallible::<MtaHook>().await {
|
||||
let id = hook.id;
|
||||
let hook = hook.object;
|
||||
let enable = bp.compile_expr(id, &hook.ctx_enable());
|
||||
let headers = match hook
|
||||
.http_auth
|
||||
.build_headers(hook.http_headers, "application/json".into())
|
||||
.await
|
||||
{
|
||||
Ok(headers) => headers,
|
||||
Err(err) => {
|
||||
bp.build_error(id, format!("Unable to build HTTP headers: {}", err));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
hooks.push(MTAHook {
|
||||
enable,
|
||||
id,
|
||||
url: hook.url,
|
||||
timeout: hook.timeout.into_inner(),
|
||||
headers,
|
||||
tls_allow_invalid_certs: hook.allow_invalid_certs,
|
||||
tempfail_on_error: hook.temp_fail_on_error,
|
||||
run_on_stage: hook.stages.into_iter().map(Stage::from).collect(),
|
||||
max_response_size: hook.max_response_size as usize,
|
||||
client: utils::http::http_client_builder(hook.allow_invalid_certs)
|
||||
.build()
|
||||
.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
|
||||
SessionConfig {
|
||||
timeout: bp.compile_expr(
|
||||
ObjectType::MtaInboundSession.singleton(),
|
||||
&session.ctx_timeout(),
|
||||
),
|
||||
duration: bp.compile_expr(
|
||||
ObjectType::MtaInboundSession.singleton(),
|
||||
&session.ctx_max_duration(),
|
||||
),
|
||||
transfer_limit: bp.compile_expr(
|
||||
ObjectType::MtaInboundSession.singleton(),
|
||||
&session.ctx_transfer_limit(),
|
||||
),
|
||||
connect: Connect {
|
||||
hostname: bp.compile_expr(
|
||||
ObjectType::MtaStageConnect.singleton(),
|
||||
&connect.ctx_hostname(),
|
||||
),
|
||||
script: bp.compile_expr(
|
||||
ObjectType::MtaStageConnect.singleton(),
|
||||
&connect.ctx_script(),
|
||||
),
|
||||
greeting: bp.compile_expr(
|
||||
ObjectType::MtaStageConnect.singleton(),
|
||||
&connect.ctx_smtp_greeting(),
|
||||
),
|
||||
},
|
||||
ehlo: Ehlo {
|
||||
script: bp.compile_expr(ObjectType::MtaStageEhlo.singleton(), &ehlo.ctx_script()),
|
||||
require: bp.compile_expr(ObjectType::MtaStageEhlo.singleton(), &ehlo.ctx_require()),
|
||||
reject_non_fqdn: bp.compile_expr(
|
||||
ObjectType::MtaStageEhlo.singleton(),
|
||||
&ehlo.ctx_reject_non_fqdn(),
|
||||
),
|
||||
},
|
||||
auth: Auth {
|
||||
mechanisms: bp.compile_expr(
|
||||
ObjectType::MtaStageAuth.singleton(),
|
||||
&auth.ctx_sasl_mechanisms(),
|
||||
),
|
||||
require: bp.compile_expr(ObjectType::MtaStageAuth.singleton(), &auth.ctx_require()),
|
||||
must_match_sender: bp.compile_expr(
|
||||
ObjectType::MtaStageAuth.singleton(),
|
||||
&auth.ctx_must_match_sender(),
|
||||
),
|
||||
errors_max: bp.compile_expr(
|
||||
ObjectType::MtaStageAuth.singleton(),
|
||||
&auth.ctx_max_failures(),
|
||||
),
|
||||
errors_wait: bp.compile_expr(
|
||||
ObjectType::MtaStageAuth.singleton(),
|
||||
&auth.ctx_wait_on_fail(),
|
||||
),
|
||||
},
|
||||
mail: Mail {
|
||||
script: bp.compile_expr(ObjectType::MtaStageMail.singleton(), &mail.ctx_script()),
|
||||
rewrite: bp.compile_expr(ObjectType::MtaStageMail.singleton(), &mail.ctx_rewrite()),
|
||||
is_allowed: bp.compile_expr(
|
||||
ObjectType::MtaStageMail.singleton(),
|
||||
&mail.ctx_is_sender_allowed(),
|
||||
),
|
||||
},
|
||||
rcpt: Rcpt {
|
||||
script: bp.compile_expr(ObjectType::MtaStageRcpt.singleton(), &rcpt.ctx_script()),
|
||||
relay: bp.compile_expr(
|
||||
ObjectType::MtaStageRcpt.singleton(),
|
||||
&rcpt.ctx_allow_relaying(),
|
||||
),
|
||||
rewrite: bp.compile_expr(ObjectType::MtaStageRcpt.singleton(), &rcpt.ctx_rewrite()),
|
||||
errors_max: bp.compile_expr(
|
||||
ObjectType::MtaStageRcpt.singleton(),
|
||||
&rcpt.ctx_max_failures(),
|
||||
),
|
||||
errors_wait: bp.compile_expr(
|
||||
ObjectType::MtaStageRcpt.singleton(),
|
||||
&rcpt.ctx_wait_on_fail(),
|
||||
),
|
||||
max_recipients: bp.compile_expr(
|
||||
ObjectType::MtaStageRcpt.singleton(),
|
||||
&rcpt.ctx_max_recipients(),
|
||||
),
|
||||
},
|
||||
data: Data {
|
||||
script: bp.compile_expr(ObjectType::MtaStageData.singleton(), &data.ctx_script()),
|
||||
spam_filter: bp.compile_expr(
|
||||
ObjectType::MtaStageData.singleton(),
|
||||
&data.ctx_enable_spam_filter(),
|
||||
),
|
||||
max_messages: bp.compile_expr(
|
||||
ObjectType::MtaStageData.singleton(),
|
||||
&data.ctx_max_messages(),
|
||||
),
|
||||
max_message_size: bp.compile_expr(
|
||||
ObjectType::MtaStageData.singleton(),
|
||||
&data.ctx_max_message_size(),
|
||||
),
|
||||
max_received_headers: bp.compile_expr(
|
||||
ObjectType::MtaStageData.singleton(),
|
||||
&data.ctx_max_received_headers(),
|
||||
),
|
||||
add_received: bp.compile_expr(
|
||||
ObjectType::MtaStageData.singleton(),
|
||||
&data.ctx_add_received_header(),
|
||||
),
|
||||
add_received_spf: bp.compile_expr(
|
||||
ObjectType::MtaStageData.singleton(),
|
||||
&data.ctx_add_received_spf_header(),
|
||||
),
|
||||
add_return_path: bp.compile_expr(
|
||||
ObjectType::MtaStageData.singleton(),
|
||||
&data.ctx_add_return_path_header(),
|
||||
),
|
||||
add_auth_results: bp.compile_expr(
|
||||
ObjectType::MtaStageData.singleton(),
|
||||
&data.ctx_add_auth_results_header(),
|
||||
),
|
||||
add_message_id: bp.compile_expr(
|
||||
ObjectType::MtaStageData.singleton(),
|
||||
&data.ctx_add_message_id_header(),
|
||||
),
|
||||
add_date: bp.compile_expr(
|
||||
ObjectType::MtaStageData.singleton(),
|
||||
&data.ctx_add_date_header(),
|
||||
),
|
||||
add_delivered_to: data.add_delivered_to_header,
|
||||
},
|
||||
extensions: Extensions {
|
||||
pipelining: bp
|
||||
.compile_expr(ObjectType::MtaExtensions.singleton(), &ext.ctx_pipelining()),
|
||||
chunking: bp
|
||||
.compile_expr(ObjectType::MtaExtensions.singleton(), &ext.ctx_chunking()),
|
||||
requiretls: bp.compile_expr(
|
||||
ObjectType::MtaExtensions.singleton(),
|
||||
&ext.ctx_require_tls(),
|
||||
),
|
||||
dsn: bp.compile_expr(ObjectType::MtaExtensions.singleton(), &ext.ctx_dsn()),
|
||||
vrfy: bp.compile_expr(ObjectType::MtaExtensions.singleton(), &ext.ctx_vrfy()),
|
||||
expn: bp.compile_expr(ObjectType::MtaExtensions.singleton(), &ext.ctx_expn()),
|
||||
no_soliciting: bp.compile_expr(
|
||||
ObjectType::MtaExtensions.singleton(),
|
||||
&ext.ctx_no_soliciting(),
|
||||
),
|
||||
future_release: bp.compile_expr(
|
||||
ObjectType::MtaExtensions.singleton(),
|
||||
&ext.ctx_future_release(),
|
||||
),
|
||||
deliver_by: bp
|
||||
.compile_expr(ObjectType::MtaExtensions.singleton(), &ext.ctx_deliver_by()),
|
||||
mt_priority: bp.compile_expr(
|
||||
ObjectType::MtaExtensions.singleton(),
|
||||
&ext.ctx_mt_priority(),
|
||||
),
|
||||
},
|
||||
mta_sts_policy: Policy::try_parse(bp).await,
|
||||
milters: bp
|
||||
.list_infallible::<MtaMilter>()
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|milter| {
|
||||
let id = milter.id;
|
||||
let milter = milter.object;
|
||||
|
||||
Some(Milter {
|
||||
enable: bp.compile_expr(id, &milter.ctx_enable()),
|
||||
id,
|
||||
addrs: format!("{}:{}", milter.hostname, milter.port)
|
||||
.to_socket_addrs()
|
||||
.map_err(|err| {
|
||||
bp.build_error(
|
||||
id,
|
||||
format!(
|
||||
"Unable to resolve milter hostname {}: {}",
|
||||
milter.hostname, err
|
||||
),
|
||||
)
|
||||
})
|
||||
.ok()?
|
||||
.collect(),
|
||||
hostname: milter.hostname,
|
||||
port: milter.port as u16,
|
||||
timeout_connect: milter.timeout_connect.into_inner(),
|
||||
timeout_command: milter.timeout_command.into_inner(),
|
||||
timeout_data: milter.timeout_data.into_inner(),
|
||||
tls: milter.use_tls,
|
||||
tls_allow_invalid_certs: milter.allow_invalid_certs,
|
||||
tempfail_on_error: milter.temp_fail_on_error,
|
||||
max_frame_len: milter.max_response_size as usize,
|
||||
protocol_version: match milter.protocol_version {
|
||||
enums::MilterVersion::V2 => MilterVersion::V2,
|
||||
enums::MilterVersion::V6 => MilterVersion::V6,
|
||||
},
|
||||
flags_actions: milter.flags_action.map(|v| v as u32),
|
||||
flags_protocol: milter.flags_protocol.map(|v| v as u32),
|
||||
run_on_stage: milter.stages.into_iter().map(Stage::from).collect(),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
hooks,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Mechanism(u64);
|
||||
|
||||
impl FromStr for Mechanism {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Mechanism(match value.to_ascii_uppercase().as_str() {
|
||||
"LOGIN" => AUTH_LOGIN,
|
||||
"PLAIN" => AUTH_PLAIN,
|
||||
"XOAUTH2" => AUTH_XOAUTH2,
|
||||
"OAUTHBEARER" => AUTH_OAUTHBEARER,
|
||||
/*"SCRAM-SHA-256-PLUS" => AUTH_SCRAM_SHA_256_PLUS,
|
||||
"SCRAM-SHA-256" => AUTH_SCRAM_SHA_256,
|
||||
"SCRAM-SHA-1-PLUS" => AUTH_SCRAM_SHA_1_PLUS,
|
||||
"SCRAM-SHA-1" => AUTH_SCRAM_SHA_1,
|
||||
"XOAUTH" => AUTH_XOAUTH,
|
||||
"9798-M-DSA-SHA1" => AUTH_9798_M_DSA_SHA1,
|
||||
"9798-M-ECDSA-SHA1" => AUTH_9798_M_ECDSA_SHA1,
|
||||
"9798-M-RSA-SHA1-ENC" => AUTH_9798_M_RSA_SHA1_ENC,
|
||||
"9798-U-DSA-SHA1" => AUTH_9798_U_DSA_SHA1,
|
||||
"9798-U-ECDSA-SHA1" => AUTH_9798_U_ECDSA_SHA1,
|
||||
"9798-U-RSA-SHA1-ENC" => AUTH_9798_U_RSA_SHA1_ENC,
|
||||
"EAP-AES128" => AUTH_EAP_AES128,
|
||||
"EAP-AES128-PLUS" => AUTH_EAP_AES128_PLUS,
|
||||
"ECDH-X25519-CHALLENGE" => AUTH_ECDH_X25519_CHALLENGE,
|
||||
"ECDSA-NIST256P-CHALLENGE" => AUTH_ECDSA_NIST256P_CHALLENGE,
|
||||
"EXTERNAL" => AUTH_EXTERNAL,
|
||||
"GS2-KRB5" => AUTH_GS2_KRB5,
|
||||
"GS2-KRB5-PLUS" => AUTH_GS2_KRB5_PLUS,
|
||||
"GSS-SPNEGO" => AUTH_GSS_SPNEGO,
|
||||
"GSSAPI" => AUTH_GSSAPI,
|
||||
"KERBEROS_V4" => AUTH_KERBEROS_V4,
|
||||
"KERBEROS_V5" => AUTH_KERBEROS_V5,
|
||||
"NMAS-SAMBA-AUTH" => AUTH_NMAS_SAMBA_AUTH,
|
||||
"NMAS_AUTHEN" => AUTH_NMAS_AUTHEN,
|
||||
"NMAS_LOGIN" => AUTH_NMAS_LOGIN,
|
||||
"NTLM" => AUTH_NTLM,
|
||||
"OAUTH10A" => AUTH_OAUTH10A,
|
||||
"OPENID20" => AUTH_OPENID20,
|
||||
"OTP" => AUTH_OTP,
|
||||
"SAML20" => AUTH_SAML20,
|
||||
"SECURID" => AUTH_SECURID,
|
||||
"SKEY" => AUTH_SKEY,
|
||||
"SPNEGO" => AUTH_SPNEGO,
|
||||
"SPNEGO-PLUS" => AUTH_SPNEGO_PLUS,
|
||||
"SXOVER-PLUS" => AUTH_SXOVER_PLUS,
|
||||
"CRAM-MD5" => AUTH_CRAM_MD5,
|
||||
"DIGEST-MD5" => AUTH_DIGEST_MD5,
|
||||
"ANONYMOUS" => AUTH_ANONYMOUS,*/
|
||||
_ => return Err(format!("Unsupported mechanism {:?}.", value)),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for Mechanism {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Variable::Constant(value) => Mechanism::try_from(value),
|
||||
Variable::Array(items) => {
|
||||
let mut mechanism = 0;
|
||||
|
||||
for item in items {
|
||||
match item {
|
||||
Variable::Constant(value) => mechanism |= Mechanism::try_from(value)?.0,
|
||||
_ => return Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Mechanism(mechanism))
|
||||
}
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ExpressionConstant> for Mechanism {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: ExpressionConstant) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
ExpressionConstant::Login => Ok(Mechanism(AUTH_LOGIN)),
|
||||
ExpressionConstant::Plain => Ok(Mechanism(AUTH_PLAIN)),
|
||||
ExpressionConstant::Xoauth2 => Ok(Mechanism(AUTH_XOAUTH2)),
|
||||
ExpressionConstant::Oauthbearer => Ok(Mechanism(AUTH_OAUTHBEARER)),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Mechanism> for u64 {
|
||||
fn from(value: Mechanism) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for Mechanism {
|
||||
fn from(value: u64) -> Self {
|
||||
Mechanism(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'x> TryFrom<Variable<'x>> for MtPriority {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Variable<'x>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Variable::Constant(value) => match value {
|
||||
ExpressionConstant::Mixer => Ok(MtPriority::Mixer),
|
||||
ExpressionConstant::Stanag4406 => Ok(MtPriority::Stanag4406),
|
||||
ExpressionConstant::Nsep => Ok(MtPriority::Nsep),
|
||||
_ => Err(()),
|
||||
},
|
||||
Variable::String(value) => {
|
||||
let value = value.as_str();
|
||||
if value.eq_ignore_ascii_case("MIXER") {
|
||||
Ok(MtPriority::Mixer)
|
||||
} else if value.eq_ignore_ascii_case("STANAG4406") {
|
||||
Ok(MtPriority::Stanag4406)
|
||||
} else if value.eq_ignore_ascii_case("NSEP") {
|
||||
Ok(MtPriority::Nsep)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MtaStage> for Stage {
|
||||
fn from(value: MtaStage) -> Self {
|
||||
match value {
|
||||
MtaStage::Connect => Stage::Connect,
|
||||
MtaStage::Ehlo => Stage::Ehlo,
|
||||
MtaStage::Auth => Stage::Auth,
|
||||
MtaStage::Mail => Stage::Mail,
|
||||
MtaStage::Rcpt => Stage::Rcpt,
|
||||
MtaStage::Data => Stage::Data,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user