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:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+380
View File
@@ -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
}
}
+92
View File
@@ -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
}
}
+801
View File
@@ -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
}
}
+273
View File
@@ -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,))
}
}
+406
View File
@@ -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,
}
}
}
+567
View File
@@ -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,
}
}
}