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
+315
View File
@@ -0,0 +1,315 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{inbound::auth::SaslToken, queue::QueueId};
use common::{
Inner, Server,
auth::AccountInfo,
config::smtp::auth::VerifyStrategy,
network::{ServerInstance, asn::AsnGeoLookupResult},
};
use mail_auth::{IprevOutput, SpfOutput};
use smtp_proto::request::receiver::{
BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, LineReceiver, RequestReceiver,
};
use std::{
hash::Hash,
net::IpAddr,
sync::Arc,
time::{Duration, Instant},
};
use tokio::io::{AsyncRead, AsyncWrite};
use utils::DomainPart;
pub mod params;
pub mod throttle;
#[derive(Clone)]
pub struct SmtpSessionManager {
pub inner: Arc<Inner>,
}
impl SmtpSessionManager {
pub fn new(inner: Arc<Inner>) -> Self {
Self { inner }
}
}
pub enum State {
Request(RequestReceiver),
Bdat(BdatReceiver),
Data(DataReceiver),
Sasl(LineReceiver<SaslToken>),
SkipData(DummyDataReceiver, &'static [u8]),
RequestTooLarge(DummyLineReceiver),
Accepted(QueueId),
None,
}
pub struct Session<T: AsyncWrite + AsyncRead> {
pub hostname: String,
pub state: State,
pub instance: Arc<ServerInstance>,
pub server: Server,
pub stream: T,
pub data: SessionData,
pub params: SessionParameters,
}
pub struct SessionData {
pub session_id: u64,
pub local_ip: IpAddr,
pub local_ip_str: String,
pub local_port: u16,
pub remote_ip: IpAddr,
pub remote_ip_str: String,
pub remote_port: u16,
pub asn_geo_data: AsnGeoLookupResult,
pub helo_domain: String,
pub mail_from: Option<SessionAddress>,
pub rcpt_to: Vec<SessionAddress>,
pub rcpt_errors: usize,
pub rcpt_oks: usize,
pub message: Vec<u8>,
pub authenticated_as: Option<AccountInfo>,
pub auth_errors: usize,
pub priority: i16,
pub delivery_by: i64,
pub future_release: u64,
pub valid_until: Instant,
pub bytes_left: usize,
pub messages_sent: usize,
pub iprev: Option<IprevOutput>,
pub spf_ehlo: Option<SpfOutput>,
pub spf_mail_from: Option<SpfOutput>,
pub dnsbl_error: Option<Vec<u8>>,
}
#[derive(Clone, Debug)]
pub struct SessionAddress {
pub address: String,
pub address_lcase: String,
pub domain: String,
pub flags: u64,
pub dsn_info: Option<String>,
}
#[derive(Debug, Default)]
pub struct SessionParameters {
// Global parameters
pub timeout: Duration,
// Ehlo parameters
pub ehlo_require: bool,
pub ehlo_reject_non_fqdn: bool,
// Auth parameters
pub auth_require: bool,
pub auth_errors_max: usize,
pub auth_errors_wait: Duration,
// Rcpt parameters
pub rcpt_errors_max: usize,
pub rcpt_errors_wait: Duration,
pub rcpt_max: usize,
pub rcpt_dsn: bool,
pub can_expn: bool,
pub can_vrfy: bool,
pub max_message_size: usize,
// Mail authentication parameters
pub iprev: VerifyStrategy,
pub spf_ehlo: VerifyStrategy,
pub spf_mail_from: VerifyStrategy,
}
impl SessionData {
pub fn new(
local_ip: IpAddr,
local_port: u16,
remote_ip: IpAddr,
remote_port: u16,
asn_geo_data: AsnGeoLookupResult,
session_id: u64,
) -> Self {
SessionData {
session_id,
local_ip,
local_port,
remote_ip,
local_ip_str: local_ip.to_string(),
remote_ip_str: remote_ip.to_string(),
remote_port,
asn_geo_data,
helo_domain: String::new(),
mail_from: None,
rcpt_to: Vec::new(),
authenticated_as: None,
priority: 0,
valid_until: Instant::now(),
rcpt_errors: 0,
rcpt_oks: 0,
message: Vec::with_capacity(0),
auth_errors: 0,
messages_sent: 0,
bytes_left: 0,
delivery_by: 0,
future_release: 0,
iprev: None,
spf_ehlo: None,
spf_mail_from: None,
dnsbl_error: None,
}
}
}
impl Default for State {
fn default() -> Self {
State::Request(RequestReceiver::default())
}
}
impl PartialEq for SessionAddress {
fn eq(&self, other: &Self) -> bool {
self.address_lcase == other.address_lcase
}
}
impl Eq for SessionAddress {}
impl Hash for SessionAddress {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.address_lcase.hash(state);
}
}
impl Ord for SessionAddress {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.domain.cmp(&other.domain) {
std::cmp::Ordering::Equal => self.address_lcase.cmp(&other.address_lcase),
order => order,
}
}
}
impl PartialOrd for SessionAddress {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Session<common::network::stream::NullIo> {
pub fn local(
server: Server,
instance: std::sync::Arc<ServerInstance>,
data: SessionData,
) -> Self {
Session {
hostname: "localhost".into(),
state: State::None,
instance,
server,
stream: common::network::stream::NullIo::default(),
data,
params: SessionParameters {
timeout: Default::default(),
ehlo_require: Default::default(),
ehlo_reject_non_fqdn: Default::default(),
auth_require: Default::default(),
auth_errors_max: Default::default(),
auth_errors_wait: Default::default(),
rcpt_errors_max: Default::default(),
rcpt_errors_wait: Default::default(),
rcpt_max: Default::default(),
rcpt_dsn: Default::default(),
max_message_size: Default::default(),
iprev: VerifyStrategy::Disable,
spf_ehlo: VerifyStrategy::Disable,
spf_mail_from: VerifyStrategy::Disable,
can_expn: false,
can_vrfy: false,
},
}
}
pub fn has_failed(&mut self) -> Option<String> {
if self.stream.tx_buf.first().is_none_or(|&c| c == b'2') {
self.stream.tx_buf.clear();
None
} else {
let response = std::str::from_utf8(&self.stream.tx_buf)
.unwrap()
.trim()
.into();
self.stream.tx_buf.clear();
Some(response)
}
}
}
impl SessionData {
pub fn local(
authenticated_as: AccountInfo,
mail_from: Option<SessionAddress>,
rcpt_to: Vec<SessionAddress>,
message: Vec<u8>,
session_id: u64,
) -> Self {
SessionData {
local_ip: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
remote_ip: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
local_ip_str: "127.0.0.1".into(),
remote_ip_str: "127.0.0.1".into(),
remote_port: 0,
local_port: 0,
session_id,
asn_geo_data: AsnGeoLookupResult::default(),
helo_domain: "localhost".into(),
mail_from,
rcpt_to,
rcpt_errors: 0,
rcpt_oks: 0,
message,
authenticated_as: Some(authenticated_as),
auth_errors: 0,
priority: 0,
delivery_by: 0,
future_release: 0,
valid_until: Instant::now(),
bytes_left: 0,
messages_sent: 0,
iprev: None,
spf_ehlo: None,
spf_mail_from: None,
dnsbl_error: None,
}
}
}
impl SessionAddress {
pub fn new(address: String) -> Self {
let address_lcase = address.to_lowercase();
SessionAddress {
domain: address_lcase.domain_part().into(),
address_lcase,
address,
flags: 0,
dsn_info: None,
}
}
pub fn report_address(&self) -> &str {
self.dsn_info
.as_ref()
.and_then(|v| v.strip_prefix("rfc822;"))
.unwrap_or(&self.address_lcase)
}
}
+159
View File
@@ -0,0 +1,159 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::Session;
use common::{config::smtp::auth::VerifyStrategy, network::SessionStream};
use std::time::Duration;
impl<T: SessionStream> Session<T> {
pub async fn eval_session_params(&mut self) {
let c = &self.server.core.smtp.session;
self.data.bytes_left = self
.server
.eval_if(&c.transfer_limit, self, self.data.session_id)
.await
.unwrap_or(250 * 1024 * 1024);
self.data.valid_until += self
.server
.eval_if(&c.duration, self, self.data.session_id)
.await
.unwrap_or_else(|| Duration::from_secs(15 * 60));
self.params.timeout = self
.server
.eval_if(&c.timeout, self, self.data.session_id)
.await
.unwrap_or_else(|| Duration::from_secs(5 * 60));
self.params.spf_ehlo = self
.server
.eval_if(
&self.server.core.smtp.mail_auth.spf.verify_ehlo,
self,
self.data.session_id,
)
.await
.unwrap_or(VerifyStrategy::Relaxed);
self.params.spf_mail_from = self
.server
.eval_if(
&self.server.core.smtp.mail_auth.spf.verify_mail_from,
self,
self.data.session_id,
)
.await
.unwrap_or(VerifyStrategy::Relaxed);
self.params.iprev = self
.server
.eval_if(
&self.server.core.smtp.mail_auth.iprev.verify,
self,
self.data.session_id,
)
.await
.unwrap_or(VerifyStrategy::Relaxed);
// Ehlo parameters
let ec = &self.server.core.smtp.session.ehlo;
self.params.ehlo_require = self
.server
.eval_if(&ec.require, self, self.data.session_id)
.await
.unwrap_or(true);
self.params.ehlo_reject_non_fqdn = self
.server
.eval_if(&ec.reject_non_fqdn, self, self.data.session_id)
.await
.unwrap_or(true);
// Auth parameters
let ac = &self.server.core.smtp.session.auth;
self.params.auth_require = self
.server
.eval_if(&ac.require, self, self.data.session_id)
.await
.unwrap_or(false);
self.params.auth_errors_max = self
.server
.eval_if(&ac.errors_max, self, self.data.session_id)
.await
.unwrap_or(3);
self.params.auth_errors_wait = self
.server
.eval_if(&ac.errors_wait, self, self.data.session_id)
.await
.unwrap_or_else(|| Duration::from_secs(30));
// VRFY/EXPN parameters
let ec = &self.server.core.smtp.session.extensions;
self.params.can_expn = self
.server
.eval_if(&ec.expn, self, self.data.session_id)
.await
.unwrap_or(false);
self.params.can_vrfy = self
.server
.eval_if(&ec.vrfy, self, self.data.session_id)
.await
.unwrap_or(false);
}
pub async fn eval_post_auth_params(&mut self) {
// Refresh VRFY/EXPN parameters
let ec = &self.server.core.smtp.session.extensions;
self.params.can_expn = self
.server
.eval_if(&ec.expn, self, self.data.session_id)
.await
.unwrap_or(false);
self.params.can_vrfy = self
.server
.eval_if(&ec.vrfy, self, self.data.session_id)
.await
.unwrap_or(false);
}
pub async fn eval_rcpt_params(&mut self) {
let rc = &self.server.core.smtp.session.rcpt;
self.params.rcpt_errors_max = self
.server
.eval_if(&rc.errors_max, self, self.data.session_id)
.await
.unwrap_or(10);
self.params.rcpt_errors_wait = self
.server
.eval_if(&rc.errors_wait, self, self.data.session_id)
.await
.unwrap_or_else(|| Duration::from_secs(30));
self.params.rcpt_max = self
.server
.eval_if(&rc.max_recipients, self, self.data.session_id)
.await
.unwrap_or(100);
self.params.rcpt_dsn = self
.server
.eval_if(
&self.server.core.smtp.session.extensions.dsn,
self,
self.data.session_id,
)
.await
.unwrap_or(true);
self.params.max_message_size = match self
.server
.eval_if::<usize, _>(
&self.server.core.smtp.session.data.max_message_size,
self,
self.data.session_id,
)
.await
{
Some(0) => usize::MAX,
Some(max_message_size) => max_message_size,
None => 25 * 1024 * 1024,
};
}
}
+268
View File
@@ -0,0 +1,268 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::Session;
use common::{
KV_RATE_LIMIT_SMTP, ThrottleKey, config::smtp::*, expr::functions::ResolveVariable,
network::SessionStream,
};
use queue::QueueQuota;
use registry::schema::{enums::ExpressionVariable, structs::Rate};
use trc::SmtpEvent;
pub trait NewKey: Sized {
fn new_key(&self, e: &impl ResolveVariable, context: &str) -> ThrottleKey;
}
impl NewKey for QueueQuota {
fn new_key(&self, e: &impl ResolveVariable, _: &str) -> ThrottleKey {
let mut hasher = blake3::Hasher::new();
if (self.keys & THROTTLE_RCPT) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::Rcpt)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_RCPT_DOMAIN) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::RcptDomain)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_SENDER) != 0 {
let sender = e.resolve_variable(ExpressionVariable::Sender).into_string();
hasher.update(
if !sender.is_empty() {
sender.as_ref()
} else {
"<>"
}
.as_bytes(),
);
}
if (self.keys & THROTTLE_SENDER_DOMAIN) != 0 {
let sender_domain = e
.resolve_variable(ExpressionVariable::SenderDomain)
.into_string();
hasher.update(
if !sender_domain.is_empty() {
sender_domain.as_ref()
} else {
"<>"
}
.as_bytes(),
);
}
if let Some(messages) = &self.messages {
hasher.update(&messages.to_ne_bytes()[..]);
}
if let Some(size) = &self.size {
hasher.update(&size.to_ne_bytes()[..]);
}
ThrottleKey {
hash: hasher.finalize().into(),
}
}
}
impl NewKey for QueueRateLimiter {
fn new_key(&self, e: &impl ResolveVariable, context: &str) -> ThrottleKey {
let mut hasher = blake3::Hasher::new();
if (self.keys & THROTTLE_RCPT) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::Rcpt)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_RCPT_DOMAIN) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::RcptDomain)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_SENDER) != 0 {
let sender = e.resolve_variable(ExpressionVariable::Sender).into_string();
hasher.update(
if !sender.is_empty() {
sender.as_ref()
} else {
"<>"
}
.as_bytes(),
);
}
if (self.keys & THROTTLE_SENDER_DOMAIN) != 0 {
let sender_domain = e
.resolve_variable(ExpressionVariable::SenderDomain)
.into_string();
hasher.update(
if !sender_domain.is_empty() {
sender_domain.as_ref()
} else {
"<>"
}
.as_bytes(),
);
}
if (self.keys & THROTTLE_HELO_DOMAIN) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::HeloDomain)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_AUTH_AS) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::AuthenticatedAs)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_LISTENER) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::Listener)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_MX) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::Mx)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_REMOTE_IP) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::RemoteIp)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_LOCAL_IP) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::LocalIp)
.to_string()
.as_bytes(),
);
}
hasher.update(&self.rate.period.as_secs().to_be_bytes()[..]);
hasher.update(&self.rate.count.to_be_bytes()[..]);
hasher.update(context.as_bytes());
ThrottleKey {
hash: hasher.finalize().into(),
}
}
}
impl<T: SessionStream> Session<T> {
pub async fn is_allowed(&mut self) -> bool {
let throttles = if !self.data.rcpt_to.is_empty() {
&self.server.core.smtp.queue.inbound_limiters.rcpt
} else if self.data.mail_from.is_some() {
&self.server.core.smtp.queue.inbound_limiters.sender
} else {
&self.server.core.smtp.queue.inbound_limiters.remote
};
for t in throttles {
if t.expr.is_empty()
|| self
.server
.eval_if(&t.expr, self, self.data.session_id)
.await
.unwrap_or(false)
{
if (t.keys & THROTTLE_RCPT_DOMAIN) != 0 {
let d = self
.data
.rcpt_to
.last()
.map(|r| r.domain.as_str())
.unwrap_or_default();
if self.data.rcpt_to.iter().filter(|p| p.domain == d).count() > 1 {
continue;
}
}
// Build throttle key
let key = t.new_key(self, "inbound");
// Check rate
match self
.server
.in_memory_store()
.is_rate_allowed(KV_RATE_LIMIT_SMTP, key.hash.as_slice(), &t.rate, false)
.await
{
Ok(Some(_)) => {
trc::event!(
Smtp(SmtpEvent::RateLimitExceeded),
SpanId = self.data.session_id,
Id = t.id.to_string(),
Limit = vec![
trc::Value::from(t.rate.count),
trc::Value::from(t.rate.period.into_inner())
],
);
return false;
}
Err(err) => {
trc::error!(
err.span_id(self.data.session_id)
.caused_by(trc::location!())
);
}
_ => (),
}
}
}
true
}
pub async fn throttle_rcpt(&self, rcpt: &str, rate: &Rate, ctx: &str) -> bool {
let mut hasher = blake3::Hasher::new();
hasher.update(rcpt.as_bytes());
hasher.update(ctx.as_bytes());
hasher.update(&rate.period.as_secs().to_ne_bytes()[..]);
hasher.update(&rate.count.to_ne_bytes()[..]);
match self
.server
.in_memory_store()
.is_rate_allowed(
KV_RATE_LIMIT_SMTP,
hasher.finalize().as_bytes(),
rate,
false,
)
.await
{
Ok(None) => true,
Ok(Some(_)) => false,
Err(err) => {
trc::error!(
err.span_id(self.data.session_id)
.caused_by(trc::location!())
);
true
}
}
}
}