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,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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user