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
}
}
}
}
+217
View File
@@ -0,0 +1,217 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::core::Session;
use common::{auth::AuthRequest, network::SessionStream};
use directory::Credentials;
use mail_parser::decoders::base64::base64_decode;
use registry::schema::enums::Permission;
use smtp_proto::{AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2, IntoString};
use trc::AuthEvent;
pub struct SaslToken {
mechanism: u64,
credentials: Credentials,
}
impl SaslToken {
pub fn from_mechanism(mechanism: u64) -> Option<SaslToken> {
match mechanism {
AUTH_PLAIN | AUTH_LOGIN => SaslToken {
mechanism,
credentials: Credentials::Basic {
username: String::new(),
secret: String::new(),
mfa_token: None,
},
}
.into(),
AUTH_OAUTHBEARER | AUTH_XOAUTH2 => SaslToken {
mechanism,
credentials: Credentials::Bearer {
username: None,
token: String::new(),
},
}
.into(),
_ => None,
}
}
}
impl<T: SessionStream> Session<T> {
pub async fn handle_sasl_response(
&mut self,
token: &mut SaslToken,
response: &[u8],
) -> Result<bool, ()> {
if response.is_empty() {
match (token.mechanism, &token.credentials) {
(AUTH_PLAIN | AUTH_XOAUTH2 | AUTH_OAUTHBEARER, _) => {
self.write(b"334 \r\n").await?;
return Ok(true);
}
(
AUTH_LOGIN,
Credentials::Basic {
username, secret, ..
},
) if username.is_empty() && secret.is_empty() => {
self.write(b"334 VXNlcm5hbWU6\r\n").await?;
return Ok(true);
}
_ => (),
}
} else if let Some(response) = base64_decode(response) {
match (token.mechanism, &mut token.credentials) {
(AUTH_PLAIN, _) => {
if let Some(credentials) = Credentials::decode_sasl_challenge_plain(&response) {
return self.authenticate(credentials).await;
}
}
(
AUTH_LOGIN,
Credentials::Basic {
username, secret, ..
},
) => {
return if username.is_empty() {
*username = response.into_string();
self.write(b"334 UGFzc3dvcmQ6\r\n").await?;
Ok(true)
} else {
*secret = response.into_string();
self.authenticate(std::mem::replace(
&mut token.credentials,
Credentials::Basic {
username: String::new(),
secret: String::new(),
mfa_token: None,
},
))
.await
};
}
(AUTH_OAUTHBEARER | AUTH_XOAUTH2, _) => {
if let Some(credentials) = Credentials::decode_sasl_challenge_oauth(&response) {
return self.authenticate(credentials).await;
}
}
_ => (),
}
}
self.auth_error(b"500 5.5.6 Invalid challenge.\r\n").await
}
pub async fn authenticate(&mut self, credentials: Credentials) -> Result<bool, ()> {
// Authenticate
let result = self
.server
.authenticate(&AuthRequest::from_credentials(
credentials,
self.data.session_id,
self.data.remote_ip,
))
.await
.and_then(|access_token| access_token.assert_has_permission(Permission::EmailSend));
let result = match result {
Ok(access_token) => self.server.account_info(access_token.account_id()).await,
Err(err) => Err(err),
};
match result {
Ok(account_info) => {
self.data.authenticated_as = account_info.into();
self.eval_post_auth_params().await;
self.write(b"235 2.7.0 Authentication succeeded.\r\n")
.await?;
return Ok(false);
}
Err(err) => {
let reason = *err.as_ref();
trc::error!(err.span_id(self.data.session_id));
match reason {
trc::EventType::Auth(trc::AuthEvent::Failed) => {
return self
.auth_error(b"535 5.7.8 Authentication credentials invalid.\r\n")
.await;
}
trc::EventType::Auth(trc::AuthEvent::TokenExpired) => {
return self.auth_error(b"535 5.7.8 OAuth token expired.\r\n").await;
}
trc::EventType::Auth(trc::AuthEvent::MfaRequired) => {
return self
.auth_error(
concat!(
"334 5.7.8 This account requires multi-factor authentication. ",
"Alternatively, you can use an app password if your account has one.\r\n"
)
.as_bytes(),
)
.await;
}
trc::EventType::Security(trc::SecurityEvent::Unauthorized) => {
self.write(
concat!(
"550 5.7.1 Your account is not authorized ",
"to use this service.\r\n"
)
.as_bytes(),
)
.await?;
return Ok(false);
}
trc::EventType::Security(_) => {
return Err(());
}
_ => (),
}
}
}
self.write(b"454 4.7.0 Temporary authentication failure\r\n")
.await?;
Ok(false)
}
pub async fn auth_error(&mut self, response: &[u8]) -> Result<bool, ()> {
tokio::time::sleep(self.params.auth_errors_wait).await;
self.data.auth_errors += 1;
self.write(response).await?;
if self.data.auth_errors < self.params.auth_errors_max {
Ok(false)
} else {
trc::event!(
Auth(AuthEvent::TooManyAttempts),
SpanId = self.data.session_id,
);
self.write(b"455 4.3.0 Too many authentication errors, disconnecting.\r\n")
.await?;
Err(())
}
}
pub fn authenticated_as(&self) -> Option<&str> {
self.data
.authenticated_as
.as_ref()
.map(|authenticated_as| authenticated_as.name())
}
pub fn is_authenticated(&self) -> bool {
self.data.authenticated_as.is_some()
}
pub fn authenticated_emails(&self) -> &[String] {
self.data.authenticated_as.as_ref().unwrap().addresses()
}
}
File diff suppressed because it is too large Load Diff
+418
View File
@@ -0,0 +1,418 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::queue::{MessageWrapper, Metadata, spool::QueueParams};
use common::{
Server,
config::smtp::auth::{Dkim1Signer, DkimSigners},
expr::{functions::ResolveVariable, if_block::IfBlock},
};
use mail_auth::{
AuthenticatedMessage,
common::headers::HeaderWriter,
dkim2::{Hop, MessageInstance},
};
use mail_parser::{Address, parsers::MessageStream};
use std::{collections::HashSet, sync::Arc};
use utils::sanitize_email;
pub(crate) trait DkimSign: Sync + Send {
fn sign_message(
&self,
message: &mut MessageWrapper,
params: &mut QueueParams<'_, '_>,
) -> impl Future<Output = Option<Vec<u8>>> + Send;
fn eval_signers(
&self,
if_block: &IfBlock,
resolver: &impl ResolveVariable,
session_id: u64,
) -> impl Future<Output = Option<Arc<DkimSigners>>> + Send;
}
impl DkimSign for Server {
async fn sign_message(
&self,
message: &mut MessageWrapper,
params: &mut QueueParams<'_, '_>,
) -> Option<Vec<u8>> {
let signers = params.dkim_signers.as_ref().unwrap();
let raw_message = params.raw_message;
// DKIM1 signing
let mut headers = Vec::with_capacity(64);
for signer in &signers.dkim1 {
let result = match (signer, params.raw_headers) {
(Dkim1Signer::RsaSha256(signer), None) => signer.sign(raw_message),
(Dkim1Signer::Ed25519Sha256(signer), None) => signer.sign(raw_message),
(Dkim1Signer::RsaSha256(signer), Some(headers)) => {
signer.sign_chained([headers, raw_message].iter().copied())
}
(Dkim1Signer::Ed25519Sha256(signer), Some(headers)) => {
signer.sign_chained([headers, raw_message].iter().copied())
}
};
match result {
Ok(signature) => {
signature.write_header(&mut headers);
}
Err(err) => {
trc::error!(
trc::Error::from(err)
.span_id(params.session_id)
.details("Failed to sign message")
.caused_by(trc::location!())
);
}
}
}
// DKIM2 signing
if let Some(signer) = &signers.dkim2
&& let Some(modified) =
AuthenticatedMessage::parse_with_opts(raw_message, params.raw_headers, true)
{
// Generate message instance
let original = params.original_authenticated_message.take().or_else(|| {
params
.original_raw_message
.and_then(AuthenticatedMessage::parse)
});
let instance = MessageInstance::from_message(&modified, original.as_ref());
if let Some(instance) = &instance {
instance.write_header(&mut headers);
}
// Obtain disclosed and undisclosed recipients
let envelopes = message.undisclosed_recipients(&modified);
// Generate DKIM2 signature for disclosed recipients
if !envelopes.disclosed_recipients.is_empty() {
match signer.sign_with_message_instance(
&modified,
instance.as_ref(),
Hop::real(
message.message.return_path.as_ref(),
envelopes.disclosed_recipients,
),
) {
Ok(signature) => {
if envelopes.undisclosed_recipients.is_empty() {
// Happy path: no undisclosed recipients, serialize signature straight to blob
signature.write_header(&mut headers);
} else {
// Undisclosed recipients present, serialize signature to metadata
let mut header = Vec::with_capacity(64);
signature.write_header(&mut header);
params.metadata.push(Metadata::Headers {
value: header.into_boxed_slice(),
id: u64::MAX,
});
}
}
Err(err) => {
trc::error!(
trc::Error::from(err)
.span_id(params.session_id)
.details("Failed to DKIM2 sign message")
);
}
}
}
// Generate DKIM2 signature for undisclosed recipients
for (pos, rcpt) in envelopes.undisclosed_recipients {
match signer.sign_with_message_instance(
&modified,
instance.as_ref(),
Hop::real(message.message.return_path.as_ref(), [rcpt]),
) {
Ok(signature) => {
// Serialize signature to metadata
let mut header = Vec::with_capacity(64);
signature.write_header(&mut header);
params.metadata.push(Metadata::Headers {
value: header.into_boxed_slice(),
id: pos as u64,
});
}
Err(err) => {
trc::error!(
trc::Error::from(err)
.span_id(params.session_id)
.details("Failed to DKIM2 sign message")
);
}
}
}
}
(!headers.is_empty()).then_some(headers)
}
async fn eval_signers(
&self,
if_block: &IfBlock,
resolver: &impl ResolveVariable,
session_id: u64,
) -> Option<Arc<DkimSigners>> {
let sign_with_domain = self
.eval_if::<String, _>(if_block, resolver, session_id)
.await?;
match self.dkim_signers(&sign_with_domain).await {
Ok(signers) => signers,
Err(err) => {
trc::error!(
err.span_id(session_id)
.details("Failed to retrieve DKIM signers")
);
None
}
}
}
}
struct Dkim2Envelopes<'x> {
undisclosed_recipients: Vec<(usize, &'x str)>,
disclosed_recipients: Vec<&'x str>,
}
impl MessageWrapper {
fn undisclosed_recipients<'x>(
&'x self,
message: &AuthenticatedMessage<'_>,
) -> Dkim2Envelopes<'x> {
if self.message.recipients.len() == 1 {
return Dkim2Envelopes {
undisclosed_recipients: Vec::new(),
disclosed_recipients: vec![self.message.recipients[0].address.as_ref()],
};
}
let mut recipients = HashSet::with_capacity(self.message.recipients.len());
for addr in message.headers.iter().filter_map(|(name, value)| {
let name = name.trim_ascii();
if name.len() == 2
&& (name.eq_ignore_ascii_case(b"to") || name.eq_ignore_ascii_case(b"cc"))
{
MessageStream::new(value).parse_address().into_address()
} else {
None
}
}) {
match addr {
Address::List(addrs) => {
recipients.extend(
addrs
.iter()
.filter_map(|a| a.address())
.map(sanitize_or_lower),
);
}
Address::Group(groups) => {
for group in groups {
recipients.extend(
group
.addresses
.iter()
.filter_map(|a| a.address())
.map(sanitize_or_lower),
);
}
}
}
}
let mut undisclosed_recipients = Vec::new();
let mut disclosed_recipients = Vec::new();
for (i, rcpt) in self.message.recipients.iter().enumerate() {
if !recipients.contains(rcpt.address.as_ref())
&& !recipients.contains(&sanitize_or_lower(&rcpt.address))
{
undisclosed_recipients.push((i, rcpt.address.as_ref()));
} else {
disclosed_recipients.push(rcpt.address.as_ref());
}
}
Dkim2Envelopes {
undisclosed_recipients,
disclosed_recipients,
}
}
}
fn sanitize_or_lower(rcpt: &str) -> String {
sanitize_email(rcpt).unwrap_or_else(|| rcpt.to_lowercase())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::queue::{Message, QueueId, Recipient};
use common::config::smtp::queue::QueueName;
use std::net::{IpAddr, Ipv4Addr};
fn wrapper(recipients: &[&str]) -> MessageWrapper {
MessageWrapper {
queue_id: 0 as QueueId,
queue_name: QueueName::default(),
is_multi_queue: false,
span_id: 0,
message: Message {
created: 0,
blob_hash: Default::default(),
return_path: "[email protected]".into(),
recipients: recipients.iter().map(Recipient::new).collect(),
received_from_ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
received_via_port: 0,
flags: 0,
env_id: None,
priority: 0,
size: 0,
metadata: Default::default(),
},
}
}
fn split(headers: &str, recipients: &[&str]) -> (Vec<String>, Vec<String>) {
let raw = format!("{headers}\r\nSubject: test\r\n\r\nbody\r\n");
let auth = AuthenticatedMessage::parse(raw.as_bytes()).expect("parse message");
let message = wrapper(recipients);
let envelopes = message.undisclosed_recipients(&auth);
let mut disclosed = envelopes
.disclosed_recipients
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
let mut undisclosed = envelopes
.undisclosed_recipients
.iter()
.map(|(_, s)| s.to_string())
.collect::<Vec<_>>();
disclosed.sort();
undisclosed.sort();
(disclosed, undisclosed)
}
#[test]
fn single_recipient_is_always_disclosed() {
let (disclosed, undisclosed) = split("To: [email protected]", &["[email protected]"]);
assert_eq!(disclosed, vec!["[email protected]".to_string()]);
assert!(undisclosed.is_empty());
}
#[test]
fn all_recipients_disclosed() {
let (disclosed, undisclosed) = split(
"To: [email protected], [email protected]\r\nCc: [email protected]",
&["[email protected]", "[email protected]", "[email protected]"],
);
assert_eq!(
disclosed,
vec![
"[email protected]".to_string(),
"[email protected]".to_string(),
"[email protected]".to_string(),
]
);
assert!(undisclosed.is_empty());
}
#[test]
fn mixed_disclosed_and_undisclosed() {
let (disclosed, undisclosed) = split(
"To: [email protected]\r\nCc: [email protected]",
&[
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
],
);
assert_eq!(
disclosed,
vec![
"[email protected]".to_string(),
"[email protected]".to_string()
]
);
assert_eq!(
undisclosed,
vec![
"[email protected]".to_string(),
"[email protected]".to_string(),
]
);
}
#[test]
fn no_to_or_cc_header_all_undisclosed() {
let (disclosed, undisclosed) = split(
"From: [email protected]",
&["[email protected]", "[email protected]"],
);
assert!(disclosed.is_empty());
assert_eq!(
undisclosed,
vec![
"[email protected]".to_string(),
"[email protected]".to_string()
]
);
}
#[test]
fn group_addresses_are_disclosed() {
let (disclosed, undisclosed) = split(
"To: Team:[email protected],[email protected];",
&["[email protected]", "[email protected]", "[email protected]"],
);
assert_eq!(
disclosed,
vec![
"[email protected]".to_string(),
"[email protected]".to_string()
]
);
assert_eq!(undisclosed, vec!["[email protected]".to_string()]);
}
#[test]
fn header_address_casing_is_ignored() {
let (disclosed, undisclosed) = split(
"To: [email protected], [email protected]",
&["[email protected]", "[email protected]", "[email protected]"],
);
assert_eq!(
disclosed,
vec![
"[email protected]".to_string(),
"[email protected]".to_string()
]
);
assert_eq!(undisclosed, vec!["[email protected]".to_string()]);
}
#[test]
fn display_names_and_brackets_are_ignored() {
let (disclosed, undisclosed) = split(
"To: \"Alice Doe\" <[email protected]>\r\nCc: Bob <[email protected]>",
&["[email protected]", "[email protected]", "[email protected]"],
);
assert_eq!(
disclosed,
vec![
"[email protected]".to_string(),
"[email protected]".to_string()
]
);
assert_eq!(undisclosed, vec!["[email protected]".to_string()]);
}
}
+292
View File
@@ -0,0 +1,292 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{core::Session, scripts::ScriptResult};
use common::{
config::smtp::session::{Mechanism, Stage},
network::SessionStream,
};
use mail_auth::{
SpfResult,
spf::verify::{HasValidLabels, SpfParameters},
};
use smtp_proto::*;
use std::{
borrow::Cow,
time::{Duration, Instant, SystemTime},
};
use trc::SmtpEvent;
impl<T: SessionStream> Session<T> {
pub async fn handle_ehlo(&mut self, domain: Cow<'_, str>, is_extended: bool) -> Result<(), ()> {
// Set EHLO domain
if domain != self.data.helo_domain {
// Reject non-FQDN EHLO domains - simply checks that the hostname has at least one dot
if self.params.ehlo_reject_non_fqdn && !domain.as_ref().has_valid_labels() {
trc::event!(
Smtp(SmtpEvent::InvalidEhlo),
SpanId = self.data.session_id,
Domain = domain.as_ref().to_string(),
);
return self.write(b"550 5.5.0 Invalid EHLO domain.\r\n").await;
}
trc::event!(
Smtp(SmtpEvent::Ehlo),
SpanId = self.data.session_id,
Domain = domain.as_ref().to_string(),
);
// SPF check
let prev_helo_domain =
std::mem::replace(&mut self.data.helo_domain, domain.into_owned());
if self.params.spf_ehlo.verify() {
let time = Instant::now();
let spf_output = self
.server
.core
.smtp
.resolvers
.dns
.verify_spf(self.server.inner.cache.build_auth_parameters(
SpfParameters::verify_ehlo(
self.data.remote_ip,
&self.data.helo_domain,
&self.hostname,
),
))
.await;
trc::event!(
Smtp(if matches!(spf_output.result(), SpfResult::Pass) {
SmtpEvent::SpfEhloPass
} else {
SmtpEvent::SpfEhloFail
}),
SpanId = self.data.session_id,
Domain = self.data.helo_domain.clone(),
Result = trc::Error::from(&spf_output),
Elapsed = time.elapsed(),
);
if self
.handle_spf(&spf_output, self.params.spf_ehlo.is_strict())
.await?
{
self.data.spf_ehlo = spf_output.into();
} else {
self.data.mail_from = None;
self.data.helo_domain = prev_helo_domain;
return Ok(());
}
}
// Sieve filtering
if let Some((script, script_id)) = self
.server
.eval_if::<String, _>(
&self.server.core.smtp.session.ehlo.script,
self,
self.data.session_id,
)
.await
.and_then(|name| {
self.server
.get_trusted_sieve_script(&name, self.data.session_id)
.map(|s| (s, name))
})
&& let ScriptResult::Reject(message) = self
.run_script(
script_id,
script.clone(),
self.build_script_parameters("ehlo"),
)
.await
{
self.data.mail_from = None;
self.data.helo_domain = prev_helo_domain;
self.data.spf_ehlo = None;
return self.write(message.as_bytes()).await;
}
// Milter filtering
if let Err(message) = self.run_milters(Stage::Ehlo, None, None).await {
self.data.mail_from = None;
self.data.helo_domain = prev_helo_domain;
self.data.spf_ehlo = None;
return self.write(message.message.as_bytes()).await;
}
// MTAHook filtering
if let Err(message) = self.run_mta_hooks(Stage::Ehlo, None, None).await {
self.data.mail_from = None;
self.data.helo_domain = prev_helo_domain;
self.data.spf_ehlo = None;
return self.write(message.message.as_bytes()).await;
}
}
// Reset
if self.data.mail_from.is_some() {
self.reset();
}
if !is_extended {
return self
.write(format!("250 {} you had me at HELO\r\n", self.hostname).as_bytes())
.await;
}
let mut response = EhloResponse::new(self.hostname.as_str());
response.capabilities =
EXT_ENHANCED_STATUS_CODES | EXT_8BIT_MIME | EXT_BINARY_MIME | EXT_SMTP_UTF8;
if !self.stream.is_tls() && self.instance.acceptor.is_tls() {
response.capabilities |= EXT_START_TLS;
}
let ec = &self.server.core.smtp.session.extensions;
let ac = &self.server.core.smtp.session.auth;
let dc = &self.server.core.smtp.session.data;
// Pipelining
if self
.server
.eval_if(&ec.pipelining, self, self.data.session_id)
.await
.unwrap_or(true)
{
response.capabilities |= EXT_PIPELINING;
}
// Chunking
if self
.server
.eval_if(&ec.chunking, self, self.data.session_id)
.await
.unwrap_or(true)
{
response.capabilities |= EXT_CHUNKING;
}
// Address Expansion
if self
.server
.eval_if(&ec.expn, self, self.data.session_id)
.await
.unwrap_or(false)
{
response.capabilities |= EXT_EXPN;
}
// Recipient Verification
if self
.server
.eval_if(&ec.vrfy, self, self.data.session_id)
.await
.unwrap_or(false)
{
response.capabilities |= EXT_VRFY;
}
// Require TLS
if self
.server
.eval_if(&ec.requiretls, self, self.data.session_id)
.await
.unwrap_or(true)
{
response.capabilities |= EXT_REQUIRE_TLS;
}
// DSN
if self
.server
.eval_if(&ec.dsn, self, self.data.session_id)
.await
.unwrap_or(false)
{
response.capabilities |= EXT_DSN;
}
// Authentication
if !self.is_authenticated() {
response.auth_mechanisms = self
.server
.eval_if::<Mechanism, _>(&ac.mechanisms, self, self.data.session_id)
.await
.unwrap_or_default()
.into();
if response.auth_mechanisms != 0 {
response.capabilities |= EXT_AUTH;
}
}
// Future release
if let Some(value) = self
.server
.eval_if::<Duration, _>(&ec.future_release, self, self.data.session_id)
.await
{
response.capabilities |= EXT_FUTURE_RELEASE;
response.future_release_interval = value.as_secs();
response.future_release_datetime = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
+ value.as_secs();
}
// Deliver By
if let Some(value) = self
.server
.eval_if::<Duration, _>(&ec.deliver_by, self, self.data.session_id)
.await
{
response.capabilities |= EXT_DELIVER_BY;
response.deliver_by = value.as_secs();
}
// Priority
if let Some(value) = self
.server
.eval_if::<MtPriority, _>(&ec.mt_priority, self, self.data.session_id)
.await
{
response.capabilities |= EXT_MT_PRIORITY;
response.mt_priority = value;
}
// Size
response.size = self
.server
.eval_if(&dc.max_message_size, self, self.data.session_id)
.await
.unwrap_or(25 * 1024 * 1024);
if response.size > 0 {
response.capabilities |= EXT_SIZE;
}
// No soliciting
if let Some(value) = self
.server
.eval_if::<String, _>(&ec.no_soliciting, self, self.data.session_id)
.await
{
response.capabilities |= EXT_NO_SOLICITING;
response.no_soliciting = if !value.is_empty() {
value.to_string().into()
} else {
None
};
}
// Generate response
let mut buf = Vec::with_capacity(64);
response.write(&mut buf).ok();
self.write(&buf).await
}
}
+46
View File
@@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::config::smtp::session::MTAHook;
use utils::HttpLimitResponse;
use super::{Request, Response};
pub(super) async fn send_mta_hook_request(
mta_hook: &MTAHook,
request: Request,
) -> Result<Response, String> {
let response = mta_hook
.client
.post(&mta_hook.url)
.timeout(mta_hook.timeout)
.headers(mta_hook.headers.clone())
.body(
serde_json::to_string(&request)
.map_err(|err| format!("Failed to serialize Hook request: {}", err))?,
)
.send()
.await
.map_err(|err| format!("Hook request failed: {err}"))?;
if response.status().is_success() {
serde_json::from_slice(
response
.bytes_with_limit(mta_hook.max_response_size)
.await
.map_err(|err| format!("Failed to parse Hook response: {}", err))?
.ok_or_else(|| "Hook response too large".to_string())?
.as_ref(),
)
.map_err(|err| format!("Failed to parse Hook response: {}", err))
} else {
Err(format!(
"Hook request failed with code {}: {}",
response.status().as_u16(),
response.status().canonical_reason().unwrap_or("Unknown")
))
}
}
+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::{Action, Queue, Response, client::send_mta_hook_request};
use crate::{
core::Session,
inbound::{
FilterResponse,
hooks::{
Address, Client, Context, Envelope, Message, Protocol, Request, Sasl, Server, Tls,
},
milter::Modification,
},
queue::QueueId,
};
use ahash::AHashMap;
use common::{
DAEMON_NAME,
config::smtp::session::{MTAHook, Stage},
network::SessionStream,
};
use mail_auth::AuthenticatedMessage;
use std::time::Instant;
use trc::MtaHookEvent;
impl<T: SessionStream> Session<T> {
pub async fn run_mta_hooks(
&self,
stage: Stage,
message: Option<&AuthenticatedMessage<'_>>,
queue_id: Option<QueueId>,
) -> Result<Vec<Modification>, FilterResponse> {
let mta_hooks = &self.server.core.smtp.session.hooks;
if mta_hooks.is_empty() {
return Ok(Vec::new());
}
let mut modifications = Vec::new();
for mta_hook in mta_hooks {
if !mta_hook.run_on_stage.contains(&stage)
|| !self
.server
.eval_if(&mta_hook.enable, self, self.data.session_id)
.await
.unwrap_or(false)
{
continue;
}
let time = Instant::now();
match self.run_mta_hook(stage, mta_hook, message, queue_id).await {
Ok(response) => {
trc::event!(
MtaHook(match response.action {
Action::Accept => MtaHookEvent::ActionAccept,
Action::Discard => MtaHookEvent::ActionDiscard,
Action::Reject => MtaHookEvent::ActionReject,
Action::Quarantine => MtaHookEvent::ActionQuarantine,
}),
SpanId = self.data.session_id,
QueueId = queue_id,
Id = mta_hook.id.to_string(),
Elapsed = time.elapsed(),
);
let mut new_modifications = Vec::with_capacity(response.modifications.len());
for modification in response.modifications {
new_modifications.push(match modification {
super::Modification::ChangeFrom { value, parameters } => {
Modification::ChangeFrom {
sender: value,
args: flatten_parameters(parameters),
}
}
super::Modification::AddRecipient { value, parameters } => {
Modification::AddRcpt {
recipient: value,
args: flatten_parameters(parameters),
}
}
super::Modification::DeleteRecipient { value } => {
Modification::DeleteRcpt { recipient: value }
}
super::Modification::ReplaceContents { value } => {
Modification::ReplaceBody {
value: value.as_bytes().to_vec(),
}
}
super::Modification::AddHeader { name, value } => {
Modification::AddHeader { name, value }
}
super::Modification::InsertHeader { index, name, value } => {
Modification::InsertHeader { index, name, value }
}
super::Modification::ChangeHeader { index, name, value } => {
Modification::ChangeHeader { index, name, value }
}
super::Modification::DeleteHeader { index, name } => {
Modification::ChangeHeader {
index,
name,
value: String::new(),
}
}
});
}
if !modifications.is_empty() {
// The message body can only be replaced once, so we need to remove
// any previous replacements.
if new_modifications
.iter()
.any(|m| matches!(m, Modification::ReplaceBody { .. }))
{
modifications
.retain(|m| !matches!(m, Modification::ReplaceBody { .. }));
}
modifications.extend(new_modifications);
} else {
modifications = new_modifications;
}
let mut message = match response.action {
Action::Accept => continue,
Action::Discard => FilterResponse::accept(),
Action::Reject => FilterResponse::reject(),
Action::Quarantine => {
modifications.push(Modification::AddHeader {
name: "X-Quarantine".into(),
value: "true".into(),
});
FilterResponse::accept()
}
};
if let Some(response) = response.response {
if let (Some(status), Some(text)) = (response.status, response.message) {
if let Some(enhanced) = response.enhanced_status {
message.message = format!("{status} {enhanced} {text}\r\n").into();
} else {
message.message = format!("{status} {text}\r\n").into();
}
}
message.disconnect = response.disconnect;
}
return Err(message);
}
Err(err) => {
trc::event!(
MtaHook(MtaHookEvent::Error),
SpanId = self.data.session_id,
Id = mta_hook.id.to_string(),
Reason = err,
Elapsed = time.elapsed(),
);
if mta_hook.tempfail_on_error {
return Err(FilterResponse::server_failure());
}
}
}
}
Ok(modifications)
}
pub async fn run_mta_hook(
&self,
stage: Stage,
mta_hook: &MTAHook,
message: Option<&AuthenticatedMessage<'_>>,
queue_id: Option<QueueId>,
) -> Result<Response, String> {
// Build request
let (tls_version, tls_cipher) = self.stream.tls_version_and_cipher();
let request = Request {
context: Context {
stage: stage.into(),
client: Client {
ip: self.data.remote_ip.to_string(),
port: self.data.remote_port,
ptr: self
.data
.iprev
.as_ref()
.and_then(|ip_rev| ip_rev.ptr.as_ref())
.and_then(|ptrs| ptrs.first())
.map(|ip| ip.to_string()),
helo: (!self.data.helo_domain.is_empty())
.then(|| self.data.helo_domain.clone()),
active_connections: 1,
},
sasl: self.authenticated_as().map(|name| Sasl {
login: name.into(),
method: None,
}),
tls: (!tls_version.is_empty()).then(|| Tls {
version: tls_version.as_ref().into(),
cipher: tls_cipher.as_ref().into(),
bits: None,
issuer: None,
subject: None,
}),
server: Server {
name: Some(DAEMON_NAME.into()),
port: self.data.local_port,
ip: self.data.local_ip.to_string().into(),
},
queue: queue_id.map(|id| Queue {
id: format!("{:x}", id),
}),
protocol: Protocol { version: 1 },
},
envelope: self.data.mail_from.as_ref().map(|from| Envelope {
from: Address {
address: from.address_lcase.clone(),
parameters: None,
},
to: self
.data
.rcpt_to
.iter()
.map(|to| Address {
address: to.address_lcase.clone(),
parameters: None,
})
.collect(),
}),
message: message.map(|message| Message {
headers: message
.raw_parsed_headers()
.iter()
.map(|(k, v)| {
(
String::from_utf8_lossy(k).into_owned(),
String::from_utf8_lossy(v).into_owned(),
)
})
.collect(),
server_headers: vec![],
contents: String::from_utf8_lossy(message.raw_body()).into_owned(),
size: message.raw_message().len(),
}),
};
send_mta_hook_request(mta_hook, request).await
}
}
fn flatten_parameters(parameters: AHashMap<String, Option<String>>) -> String {
let mut arguments = String::new();
for (key, value) in parameters {
if !arguments.is_empty() {
arguments.push(' ');
}
arguments.push_str(key.as_str());
if let Some(value) = value {
arguments.push('=');
arguments.push_str(value.as_str());
}
}
arguments
}
+207
View File
@@ -0,0 +1,207 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod client;
pub mod message;
use ahash::AHashMap;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Request {
pub context: Context,
#[serde(skip_serializing_if = "Option::is_none")]
pub envelope: Option<Envelope>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<Message>,
}
#[derive(Serialize, Deserialize)]
pub struct Context {
pub stage: Stage,
pub client: Client,
#[serde(skip_serializing_if = "Option::is_none")]
pub sasl: Option<Sasl>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tls: Option<Tls>,
pub server: Server,
#[serde(skip_serializing_if = "Option::is_none")]
pub queue: Option<Queue>,
pub protocol: Protocol,
}
#[derive(Serialize, Deserialize)]
pub struct Sasl {
pub login: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct Client {
pub ip: String,
pub port: u16,
pub ptr: Option<String>,
pub helo: Option<String>,
#[serde(rename = "activeConnections")]
pub active_connections: u32,
}
#[derive(Serialize, Deserialize)]
pub struct Tls {
pub version: String,
pub cipher: String,
#[serde(rename = "cipherBits")]
#[serde(skip_serializing_if = "Option::is_none")]
pub bits: Option<u16>,
#[serde(rename = "certIssuer")]
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer: Option<String>,
#[serde(rename = "certSubject")]
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct Server {
pub name: Option<String>,
pub port: u16,
pub ip: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct Queue {
pub id: String,
}
#[derive(Serialize, Deserialize)]
pub struct Protocol {
pub version: u32,
}
#[derive(Serialize, Deserialize)]
pub enum Stage {
#[serde(rename = "connect")]
Connect,
#[serde(rename = "ehlo")]
Ehlo,
#[serde(rename = "auth")]
Auth,
#[serde(rename = "mail")]
Mail,
#[serde(rename = "rcpt")]
Rcpt,
#[serde(rename = "data")]
Data,
}
#[derive(Serialize, Deserialize)]
pub struct Address {
pub address: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<AHashMap<String, String>>,
}
#[derive(Serialize, Deserialize)]
pub struct Envelope {
pub from: Address,
pub to: Vec<Address>,
}
#[derive(Serialize, Deserialize)]
pub struct Message {
pub headers: Vec<(String, String)>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(rename = "serverHeaders")]
#[serde(default)]
pub server_headers: Vec<(String, String)>,
pub contents: String,
pub size: usize,
}
#[derive(Serialize, Deserialize)]
pub struct Response {
pub action: Action,
#[serde(default)]
pub response: Option<SmtpResponse>,
#[serde(default)]
pub modifications: Vec<Modification>,
}
#[derive(Serialize, Deserialize)]
pub enum Action {
#[serde(rename = "accept")]
Accept,
#[serde(rename = "discard")]
Discard,
#[serde(rename = "reject")]
Reject,
#[serde(rename = "quarantine")]
Quarantine,
}
#[derive(Serialize, Deserialize, Default)]
pub struct SmtpResponse {
#[serde(default)]
pub status: Option<u16>,
#[serde(default)]
pub enhanced_status: Option<String>,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub disconnect: bool,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum Modification {
#[serde(rename = "changeFrom")]
ChangeFrom {
value: String,
#[serde(default)]
parameters: AHashMap<String, Option<String>>,
},
#[serde(rename = "addRecipient")]
AddRecipient {
value: String,
#[serde(default)]
parameters: AHashMap<String, Option<String>>,
},
#[serde(rename = "deleteRecipient")]
DeleteRecipient { value: String },
#[serde(rename = "replaceContents")]
ReplaceContents { value: String },
#[serde(rename = "addHeader")]
AddHeader { name: String, value: String },
#[serde(rename = "insertHeader")]
InsertHeader {
index: u32,
name: String,
value: String,
},
#[serde(rename = "changeHeader")]
ChangeHeader {
index: u32,
name: String,
value: String,
},
#[serde(rename = "deleteHeader")]
DeleteHeader { index: u32, name: String },
}
impl From<common::config::smtp::session::Stage> for Stage {
fn from(value: common::config::smtp::session::Stage) -> Self {
match value {
common::config::smtp::session::Stage::Connect => Stage::Connect,
common::config::smtp::session::Stage::Ehlo => Stage::Ehlo,
common::config::smtp::session::Stage::Auth => Stage::Auth,
common::config::smtp::session::Stage::Mail => Stage::Mail,
common::config::smtp::session::Stage::Rcpt => Stage::Rcpt,
common::config::smtp::session::Stage::Data => Stage::Data,
}
}
}
+607
View File
@@ -0,0 +1,607 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
core::{Session, SessionAddress},
scripts::ScriptResult,
};
use common::{config::smtp::session::Stage, network::SessionStream, scripts::ScriptModification};
use mail_auth::{IprevOutput, IprevResult, SpfOutput, SpfResult, spf::verify::SpfParameters};
use mail_parser::DateTime;
use registry::schema::structs::Rate;
use smtp_proto::{MAIL_BY_NOTIFY, MAIL_BY_RETURN, MAIL_REQUIRETLS, MailFrom, MtPriority};
use std::{
borrow::Cow,
time::{Duration, Instant, SystemTime},
};
use trc::SmtpEvent;
use utils::DomainPart;
impl<T: SessionStream> Session<T> {
pub async fn handle_mail_from(&mut self, from: MailFrom<Cow<'_, str>>) -> Result<(), ()> {
if self.data.helo_domain.is_empty()
&& (self.params.ehlo_require
|| self.params.spf_ehlo.verify()
|| self.params.spf_mail_from.verify())
{
trc::event!(
Smtp(SmtpEvent::DidNotSayEhlo),
SpanId = self.data.session_id,
);
return self
.write(b"503 5.5.1 Polite people say EHLO first.\r\n")
.await;
} else if self.data.mail_from.is_some() {
trc::event!(
Smtp(SmtpEvent::MultipleMailFrom),
SpanId = self.data.session_id,
);
return self
.write(b"503 5.5.1 Multiple MAIL commands not allowed.\r\n")
.await;
} else if self.params.auth_require && !self.is_authenticated() {
trc::event!(
Smtp(SmtpEvent::MailFromUnauthenticated),
SpanId = self.data.session_id,
);
return self
.write(b"503 5.5.1 You must authenticate first.\r\n")
.await;
} else if self.data.iprev.is_none() && self.params.iprev.verify() {
let time = Instant::now();
let iprev = self
.server
.core
.smtp
.resolvers
.dns
.verify_iprev(
self.server
.inner
.cache
.build_auth_parameters(self.data.remote_ip),
)
.await;
trc::event!(
Smtp(if matches!(iprev.result(), IprevResult::Pass) {
SmtpEvent::IprevPass
} else {
SmtpEvent::IprevFail
}),
SpanId = self.data.session_id,
Domain = self.data.helo_domain.clone(),
Result = trc::Error::from(&iprev),
Elapsed = time.elapsed(),
);
self.data.iprev = iprev.into();
}
// In strict mode reject messages from hosts that fail the reverse DNS lookup check
if self.params.iprev.is_strict()
&& !matches!(
&self.data.iprev,
Some(IprevOutput {
result: IprevResult::Pass,
..
})
)
{
let message = if matches!(
&self.data.iprev,
Some(IprevOutput {
result: IprevResult::TempError(_),
..
})
) {
&b"451 4.7.25 Temporary error validating reverse DNS.\r\n"[..]
} else {
&b"550 5.7.25 Reverse DNS validation failed.\r\n"[..]
};
return self.write(message).await;
}
let (address, address_lcase, domain) = if !from.address.is_empty() {
let address_lcase = from.address.to_lowercase_address(true);
let domain = address_lcase.domain_part().into();
(from.address.into_owned(), address_lcase, domain)
} else {
(String::new(), String::new(), String::new())
};
let has_dsn = from.env_id.is_some();
self.data.mail_from = SessionAddress {
address,
address_lcase,
domain,
flags: from.flags,
dsn_info: from.env_id.map(|e| e.into_owned()),
}
.into();
// Check whether the address is allowed
if !self
.server
.eval_if::<bool, _>(
&self.server.core.smtp.session.mail.is_allowed,
self,
self.data.session_id,
)
.await
.unwrap_or(true)
{
let mail_from = self.data.mail_from.take().unwrap();
trc::event!(
Smtp(SmtpEvent::MailFromNotAllowed),
From = mail_from.address_lcase,
SpanId = self.data.session_id,
);
return self
.write(b"550 5.7.1 Sender address not allowed.\r\n")
.await;
}
// Sieve filtering
if let Some((script, script_id)) = self
.server
.eval_if::<String, _>(
&self.server.core.smtp.session.mail.script,
self,
self.data.session_id,
)
.await
.and_then(|name| {
self.server
.get_trusted_sieve_script(&name, self.data.session_id)
.map(|s| (s, name))
})
{
match self
.run_script(
script_id,
script.clone(),
self.build_script_parameters("mail"),
)
.await
{
ScriptResult::Accept { modifications } if !modifications.is_empty() => {
for modification in modifications {
if let ScriptModification::SetEnvelope { name, value } = modification {
self.data.apply_envelope_modification(name, value);
}
}
}
ScriptResult::Reject(message) => {
self.data.mail_from = None;
return self.write(message.as_bytes()).await;
}
_ => (),
}
}
// Milter filtering
if let Err(message) = self.run_milters(Stage::Mail, None, None).await {
self.data.mail_from = None;
return self.write(message.message.as_bytes()).await;
}
// MTAHook filtering
if let Err(message) = self.run_mta_hooks(Stage::Mail, None, None).await {
self.data.mail_from = None;
return self.write(message.message.as_bytes()).await;
}
// Address rewriting
if let Some(new_address) = self
.server
.eval_if::<String, _>(
&self.server.core.smtp.session.mail.rewrite,
self,
self.data.session_id,
)
.await
{
let mail_from = self.data.mail_from.as_mut().unwrap();
trc::event!(
Smtp(SmtpEvent::MailFromRewritten),
SpanId = self.data.session_id,
Details = mail_from.address_lcase.clone(),
From = new_address.clone(),
);
if new_address.contains('@') {
mail_from.address_lcase = new_address.to_lowercase_address(true);
mail_from.domain = mail_from.address_lcase.domain_part().into();
mail_from.address = new_address;
} else if new_address.is_empty() {
mail_from.address_lcase.clear();
mail_from.domain.clear();
mail_from.address.clear();
}
}
// Make sure that the authenticated user is allowed to send from this address
match self.authenticated_as() {
Some(authenticated_as)
if self
.server
.eval_if(
&self.server.core.smtp.session.auth.must_match_sender,
self,
self.data.session_id,
)
.await
.unwrap_or(true) =>
{
let address_lcase = self.data.mail_from.as_ref().unwrap().address_lcase.as_str();
if authenticated_as != address_lcase
&& !self
.authenticated_emails()
.iter()
.any(|e| e == address_lcase)
{
trc::event!(
Smtp(SmtpEvent::MailFromUnauthorized),
SpanId = self.data.session_id,
From = address_lcase.to_string(),
Details = [trc::Value::String(authenticated_as.into())]
.into_iter()
.chain(
self.authenticated_emails()
.iter()
.map(|e| trc::Value::String(e.into()))
)
.collect::<Vec<_>>()
);
self.data.mail_from = None;
return self
.write(b"501 5.5.4 You are not allowed to send from this address.\r\n")
.await;
}
}
_ => (),
}
// Validate parameters
let config = &self.server.core.smtp.session.extensions;
let config_data = &self.server.core.smtp.session.data;
if (from.flags & MAIL_REQUIRETLS) != 0
&& !self
.server
.eval_if(&config.requiretls, self, self.data.session_id)
.await
.unwrap_or(false)
{
trc::event!(
Smtp(SmtpEvent::RequireTlsDisabled),
SpanId = self.data.session_id,
);
self.data.mail_from = None;
return self
.write(b"501 5.5.4 REQUIRETLS has been disabled.\r\n")
.await;
}
if (from.flags & (MAIL_BY_NOTIFY | MAIL_BY_RETURN)) != 0 {
if let Some(duration) = self
.server
.eval_if::<Duration, _>(&config.deliver_by, self, self.data.session_id)
.await
{
if from.by.checked_abs().unwrap_or(0) as u64 <= duration.as_secs()
&& (from.by.is_positive() || (from.flags & MAIL_BY_NOTIFY) != 0)
{
self.data.delivery_by = from.by;
} else {
self.data.mail_from = None;
trc::event!(
Smtp(SmtpEvent::DeliverByInvalid),
SpanId = self.data.session_id,
Details = from.by,
);
return self
.write(
format!(
"501 5.5.4 BY parameter exceeds maximum of {} seconds.\r\n",
duration.as_secs()
)
.as_bytes(),
)
.await;
}
} else {
trc::event!(
Smtp(SmtpEvent::DeliverByDisabled),
SpanId = self.data.session_id,
);
self.data.mail_from = None;
return self
.write(b"501 5.5.4 DELIVERBY extension has been disabled.\r\n")
.await;
}
}
if from.mt_priority != 0 {
if self
.server
.eval_if::<MtPriority, _>(&config.mt_priority, self, self.data.session_id)
.await
.is_some()
{
if (-6..6).contains(&from.mt_priority) {
self.data.priority = from.mt_priority as i16;
} else {
trc::event!(
Smtp(SmtpEvent::MtPriorityInvalid),
SpanId = self.data.session_id,
Details = from.mt_priority,
);
self.data.mail_from = None;
return self.write(b"501 5.5.4 Invalid priority value.\r\n").await;
}
} else {
trc::event!(
Smtp(SmtpEvent::MtPriorityDisabled),
SpanId = self.data.session_id,
);
self.data.mail_from = None;
return self
.write(b"501 5.5.4 MT-PRIORITY extension has been disabled.\r\n")
.await;
}
}
if from.size > 0 {
let max_message_size = self
.server
.eval_if::<usize, _>(&config_data.max_message_size, self, self.data.session_id)
.await
.unwrap_or(25 * 1024 * 1024);
if max_message_size > 0 && from.size > max_message_size {
trc::event!(
Smtp(SmtpEvent::MessageTooLarge),
SpanId = self.data.session_id,
Size = from.size,
Limit = max_message_size,
);
self.data.mail_from = None;
return self
.write(b"552 5.3.4 Message too big for system.\r\n")
.await;
}
}
if from.hold_for != 0 || from.hold_until != 0 {
if from.hold_for != 0 && from.hold_until != 0 {
trc::event!(
Smtp(SmtpEvent::FutureReleaseInvalid),
SpanId = self.data.session_id,
Details = "Both HOLDFOR and HOLDUNTIL were specified",
);
self.data.mail_from = None;
return self
.write(b"501 5.5.4 Only one of HOLDFOR or HOLDUNTIL may be specified.\r\n")
.await;
}
if let Some(max_hold) = self
.server
.eval_if::<Duration, _>(&config.future_release, self, self.data.session_id)
.await
{
let max_hold = max_hold.as_secs();
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let hold_for = if from.hold_for != 0 {
from.hold_for
} else if from.hold_until > now {
from.hold_until - now
} else {
trc::event!(
Smtp(SmtpEvent::FutureReleaseInvalid),
SpanId = self.data.session_id,
Details = from.hold_until,
);
self.data.mail_from = None;
return self
.write(b"501 5.5.4 HOLDUNTIL must be a date and time in the future.\r\n")
.await;
};
if hold_for <= max_hold {
self.data.future_release = hold_for;
} else {
trc::event!(
Smtp(SmtpEvent::FutureReleaseInvalid),
SpanId = self.data.session_id,
Details = hold_for,
);
self.data.mail_from = None;
let response = if from.hold_for != 0 {
format!(
"501 5.5.4 Requested hold time exceeds maximum of {max_hold} seconds.\r\n"
)
} else {
format!(
"501 5.5.4 Requested release time exceeds maximum of {}.\r\n",
DateTime::from_timestamp((now + max_hold) as i64).to_rfc3339()
)
};
return self.write(response.as_bytes()).await;
}
} else {
trc::event!(
Smtp(SmtpEvent::FutureReleaseDisabled),
SpanId = self.data.session_id,
);
self.data.mail_from = None;
return self
.write(b"501 5.5.4 FUTURERELEASE extension has been disabled.\r\n")
.await;
}
}
if has_dsn
&& !self
.server
.eval_if(&config.dsn, self, self.data.session_id)
.await
.unwrap_or(false)
{
trc::event!(Smtp(SmtpEvent::DsnDisabled), SpanId = self.data.session_id,);
self.data.mail_from = None;
return self
.write(b"501 5.5.4 DSN extension has been disabled.\r\n")
.await;
}
if self.is_allowed().await {
// Verify SPF
if self.params.spf_mail_from.verify() {
let time = Instant::now();
let mail_from = self.data.mail_from.as_ref().unwrap();
let spf_output = if !mail_from.address.is_empty() {
self.server
.core
.smtp
.resolvers
.dns
.check_host(self.server.inner.cache.build_auth_parameters(
SpfParameters::new(
self.data.remote_ip,
&mail_from.domain,
&self.data.helo_domain,
&self.hostname,
&mail_from.address_lcase,
),
))
.await
} else {
self.server
.core
.smtp
.resolvers
.dns
.check_host(self.server.inner.cache.build_auth_parameters(
SpfParameters::new(
self.data.remote_ip,
&self.data.helo_domain,
&self.data.helo_domain,
&self.hostname,
&format!("postmaster@{}", self.data.helo_domain),
),
))
.await
};
trc::event!(
Smtp(if matches!(spf_output.result(), SpfResult::Pass) {
SmtpEvent::SpfFromPass
} else {
SmtpEvent::SpfFromFail
}),
SpanId = self.data.session_id,
Domain = self.data.helo_domain.clone(),
From = if !mail_from.address.is_empty() {
mail_from.address.as_str()
} else {
"<>"
}
.to_string(),
Result = trc::Error::from(&spf_output),
Elapsed = time.elapsed(),
);
if self
.handle_spf(&spf_output, self.params.spf_mail_from.is_strict())
.await?
{
self.data.spf_mail_from = spf_output.into();
} else {
self.data.mail_from = None;
return Ok(());
}
}
trc::event!(
Smtp(SmtpEvent::MailFrom),
SpanId = self.data.session_id,
From = self.data.mail_from.as_ref().unwrap().address_lcase.clone(),
);
self.eval_rcpt_params().await;
self.write(b"250 2.1.0 OK\r\n").await
} else {
trc::event!(
Smtp(SmtpEvent::RateLimitExceeded),
SpanId = self.data.session_id,
From = self.data.mail_from.as_ref().unwrap().address_lcase.clone(),
);
self.data.mail_from = None;
self.write(b"452 4.4.5 Rate limit exceeded, try again later.\r\n")
.await
}
}
pub async fn handle_spf(&mut self, spf_output: &SpfOutput, strict: bool) -> Result<bool, ()> {
let result = match spf_output.result() {
SpfResult::Pass => true,
SpfResult::TempError if strict => {
self.write(b"451 4.7.24 Temporary SPF validation error.\r\n")
.await?;
false
}
result => {
if strict {
self.write(
format!("550 5.7.23 SPF validation failed, status: {result}.\r\n")
.as_bytes(),
)
.await?;
false
} else {
true
}
}
};
// Send report
if let (Some(recipient), Some(rate)) = (
spf_output.report_address(),
self.server
.eval_if::<Rate, _>(
&self.server.core.smtp.report.spf.send,
self,
self.data.session_id,
)
.await,
) {
// Do not send SPF auth failures to local domains, as they are likely relay attempts (which are blocked later on)
match self.server.domain(recipient.domain_part()).await {
Ok(Some(_)) => return Ok(result),
Ok(None) => (),
Err(err) => {
trc::error!(
err.caused_by(trc::location!())
.span_id(self.data.session_id)
.details("Failed to lookup local domain")
);
}
}
self.send_spf_report(recipient, &rate, !result, spf_output)
.await;
}
Ok(result)
}
}
+368
View File
@@ -0,0 +1,368 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::config::smtp::session::Milter;
use rustls_pki_types::ServerName;
use tokio::{
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
net::TcpStream,
};
use tokio_rustls::{TlsConnector, client::TlsStream};
use trc::MilterEvent;
use super::{
protocol::{SMFIC_CONNECT, SMFIC_HELO, SMFIC_MAIL, SMFIC_RCPT},
receiver::{FrameResult, Receiver},
*,
};
const MILTER_CHUNK_SIZE: usize = 65535;
impl MilterClient<TcpStream> {
pub async fn connect(config: &Milter, session_id: u64) -> Result<Self> {
tokio::time::timeout(config.timeout_command, async {
let mut last_err = Error::Disconnected;
for addr in &config.addrs {
match TcpStream::connect(addr).await {
Ok(stream) => {
return Ok(MilterClient {
stream,
timeout_cmd: config.timeout_command,
timeout_data: config.timeout_data,
buf: vec![0u8; 8192],
bytes_read: 0,
receiver: Receiver::with_max_frame_len(config.max_frame_len),
options: 0,
version: config.protocol_version,
session_id,
flags_actions: config.flags_actions.unwrap_or(
SMFIF_ADDHDRS
| SMFIF_CHGBODY
| SMFIF_ADDRCPT
| SMFIF_DELRCPT
| SMFIF_CHGHDRS
| SMFIF_QUARANTINE
| SMFIF_CHGFROM
| SMFIF_ADDRCPT_PAR,
),
flags_protocol: config.flags_protocol.unwrap_or(0x42),
id: config.id,
});
}
Err(err) => {
last_err = Error::Io(err);
}
}
}
Err(last_err)
})
.await
.map_err(|_| Error::Timeout)?
}
pub async fn into_tls(
self,
tls_connector: &TlsConnector,
tls_hostname: &str,
) -> Result<MilterClient<TlsStream<TcpStream>>> {
tokio::time::timeout(self.timeout_cmd, async {
Ok(MilterClient {
stream: tls_connector
.connect(
ServerName::try_from(tls_hostname)
.map_err(|_| Error::TLSInvalidName)?
.to_owned(),
self.stream,
)
.await?,
buf: self.buf,
timeout_cmd: self.timeout_cmd,
timeout_data: self.timeout_data,
receiver: self.receiver,
bytes_read: self.bytes_read,
options: self.options,
version: self.version,
session_id: self.session_id,
flags_actions: self.flags_actions,
flags_protocol: self.flags_protocol,
id: self.id,
})
})
.await
.map_err(|_| Error::Timeout)?
}
}
impl<T: AsyncRead + AsyncWrite + Unpin> MilterClient<T> {
pub async fn init(&mut self) -> super::Result<Options> {
self.write(Command::OptionNegotiation(Options {
version: match self.version {
MilterVersion::V2 => 2,
MilterVersion::V6 => 6,
},
actions: self.flags_actions,
protocol: self.flags_protocol,
}))
.await?;
match self.read().await? {
Response::OptionNegotiation(options) => {
self.options = options.protocol;
Ok(options)
}
response => Err(Error::Unexpected(response)),
}
}
pub async fn connection(
&mut self,
hostname: impl AsRef<[u8]>,
remote_ip: IpAddr,
remote_port: u16,
macros: Macros<'_>,
) -> super::Result<Action> {
if !self.has_option(SMFIP_NOCONNECT) {
self.write(Command::Macro {
macros: macros.with_cmd_code(SMFIC_CONNECT),
})
.await?;
self.write(Command::Connect {
hostname: hostname.as_ref(),
port: remote_port,
address: remote_ip,
})
.await?;
if !self.has_option(SMFIP_NR_CONN) {
return self.read().await?.into_action();
}
}
Ok(Action::Accept)
}
pub async fn helo(
&mut self,
hostname: impl AsRef<[u8]>,
macros: Macros<'_>,
) -> super::Result<Action> {
if !self.has_option(SMFIP_NOHELO) {
self.write(Command::Macro {
macros: macros.with_cmd_code(SMFIC_HELO),
})
.await?;
self.write(Command::Helo {
hostname: hostname.as_ref(),
})
.await?;
if !self.has_option(SMFIP_NR_HELO) {
return self.read().await?.into_action();
}
}
Ok(Action::Accept)
}
pub async fn mail_from<A, V>(
&mut self,
addr: A,
params: Option<&[V]>,
macros: Macros<'_>,
) -> super::Result<Action>
where
A: AsRef<[u8]>,
V: AsRef<[u8]>,
{
if !self.has_option(SMFIP_NOMAIL) {
self.write(Command::Macro {
macros: macros.with_cmd_code(SMFIC_MAIL),
})
.await?;
self.write(Command::MailFrom {
sender: addr.as_ref(),
args: params.map(|params| params.iter().map(|value| value.as_ref()).collect()),
})
.await?;
if !self.has_option(SMFIP_NR_MAIL) {
return self.read().await?.into_action();
}
}
Ok(Action::Accept)
}
pub async fn rcpt_to<A, V>(
&mut self,
addr: A,
params: Option<&[V]>,
macros: Macros<'_>,
) -> super::Result<Action>
where
A: AsRef<[u8]>,
V: AsRef<[u8]>,
{
if !self.has_option(SMFIP_NORCPT) {
self.write(Command::Macro {
macros: macros.with_cmd_code(SMFIC_RCPT),
})
.await?;
self.write(Command::Rcpt {
recipient: addr.as_ref(),
args: params.map(|params| params.iter().map(|value| value.as_ref()).collect()),
})
.await?;
if !self.has_option(SMFIP_NR_RCPT) {
return self.read().await?.into_action();
}
}
Ok(Action::Accept)
}
pub async fn headers<I, H, V>(&mut self, headers: I) -> super::Result<Action>
where
I: Iterator<Item = (H, V)>,
H: AsRef<str>,
V: AsRef<str>,
{
if !self.has_option(SMFIP_NOHDRS) {
for (name, value) in headers {
self.write(Command::Header {
name: name.as_ref().trim().as_bytes(),
value: value.as_ref().trim().as_bytes(),
})
.await?;
if !self.has_option(SMFIP_NR_HDR) {
match self.read().await? {
Response::Action(Action::Accept | Action::Continue) => (),
Response::Action(action) => return Ok(action),
response => return Err(Error::Unexpected(response)),
}
}
}
// Write EndOfHeaders
self.write(Command::EndOfHeader).await?;
if !self.has_option(SMFIP_NR_EOH) {
return self.read().await?.into_action();
}
}
Ok(Action::Accept)
}
pub async fn data(&mut self) -> super::Result<Action> {
if matches!(self.version, MilterVersion::V6) && !self.has_option(SMFIP_NODATA) {
self.write(Command::Data).await?;
if !self.has_option(SMFIP_NR_DATA) {
return self.read().await?.into_action();
}
}
Ok(Action::Accept)
}
pub async fn body(&mut self, body: &[u8]) -> super::Result<(Action, Vec<Modification>)> {
if !self.has_option(SMFIP_NOBODY) {
// Write body chunks
for value in body.chunks(MILTER_CHUNK_SIZE) {
self.write(Command::Body { value }).await?;
if !self.has_option(SMFIP_NR_BODY) {
match self.read().await? {
Response::Action(Action::Accept | Action::Continue)
| Response::Progress => (),
Response::Skip => break,
Response::Action(reject) => {
return Ok((reject, Vec::new()));
}
response => return Err(Error::Unexpected(response)),
}
}
}
// Write EndOfBody
self.write(Command::EndOfBody).await?;
// Collect responses
let mut modifications = Vec::new();
loop {
match self.read().await? {
Response::Action(action) => {
return Ok((action, modifications));
}
Response::Modification(modification) => {
modifications.push(modification);
}
Response::Progress => (),
unexpected => {
return Err(Error::Unexpected(unexpected));
}
}
}
} else {
Ok((Action::Accept, vec![]))
}
}
pub async fn abort(&mut self) -> super::Result<()> {
self.write(Command::Abort).await
}
pub async fn quit(&mut self) -> super::Result<()> {
self.write(Command::Quit).await
}
async fn write(&mut self, action: Command<'_>) -> super::Result<()> {
trc::event!(
Milter(MilterEvent::Write),
SpanId = self.session_id,
Id = self.id.to_string(),
Contents = action.to_string(),
);
tokio::time::timeout(self.timeout_cmd, async {
self.stream.write_all(action.serialize().as_ref()).await?;
self.stream.flush().await.map_err(Error::Io)
})
.await
.map_err(|_| Error::Timeout)?
}
async fn read(&mut self) -> super::Result<Response> {
loop {
match self.receiver.read_frame(&self.buf[..self.bytes_read]) {
FrameResult::Frame(frame) => {
if let Some(response) = Response::deserialize(&frame) {
trc::event!(
Milter(MilterEvent::Read),
SpanId = self.session_id,
Id = self.id.to_string(),
Contents = response.to_string(),
);
return Ok(response);
} else {
return Err(Error::FrameInvalid(frame.into_owned()));
}
}
FrameResult::Incomplete => {
self.bytes_read = tokio::time::timeout(self.timeout_data, async {
self.stream.read(&mut self.buf).await.map_err(Error::Io)
})
.await
.map_err(|_| Error::Timeout)??;
if self.bytes_read == 0 {
return Err(Error::Disconnected);
}
}
FrameResult::TooLarge(size) => return Err(Error::FrameTooLarge(size)),
}
}
}
#[inline(always)]
fn has_option(&self, opt: u32) -> bool {
self.options & opt == opt
}
pub fn with_version(mut self, version: MilterVersion) -> Self {
self.version = version;
self
}
}
+184
View File
@@ -0,0 +1,184 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{borrow::Cow, net::IpAddr};
use super::{Macro, Macros};
pub trait IntoMacroValue<'x> {
fn into_macro_value(self) -> Cow<'x, [u8]>;
}
impl<'x> Macros<'x> {
pub fn new() -> Self {
Macros::default()
}
pub fn with_cmd_code(mut self, cmd_code: u8) -> Self {
self.cmdcode = cmd_code;
self
}
pub fn with_macro(mut self, name: &'static [u8], value: impl IntoMacroValue<'x>) -> Self {
self.macros.push(Macro {
name,
value: value.into_macro_value(),
});
self
}
pub fn with_queue_id(self, queue_id: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"i", queue_id)
}
pub fn with_local_hostname(self, my_hostname: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"j", my_hostname)
}
pub fn with_validated_client_name(self, client_name: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"_", client_name)
}
pub fn with_sasl_login_name(self, sasl_login_name: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{auth_authen}", sasl_login_name)
}
pub fn with_sasl_sender(self, sasl_sender: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{auth_author}", sasl_sender)
}
pub fn with_sasl_method(self, sasl_method: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{auth_type}", sasl_method)
}
pub fn with_client_address(self, client_address: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{client_addr}", client_address)
}
pub fn with_client_connections(self, client_connections: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{client_connections}", client_connections)
}
pub fn with_client_name(self, client_name: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{client_name}", client_name)
}
pub fn with_client_port(self, client_port: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{client_port}", client_port)
}
pub fn with_client_ptr(self, client_ptr: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{client_ptr}", client_ptr)
}
pub fn with_cert_issuer(self, cert_issuer: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{cert_issuer}", cert_issuer)
}
pub fn with_cert_subject(self, cert_subject: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{cert_subject}", cert_subject)
}
pub fn with_cipher_bits(self, cipher_bits: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{cipher_bits}", cipher_bits)
}
pub fn with_cipher(self, cipher: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{cipher}", cipher)
}
pub fn with_daemon_address(self, daemon_address: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{daemon_addr}", daemon_address)
}
pub fn with_daemon_name(self, daemon_name: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{daemon_name}", daemon_name)
}
pub fn with_daemon_port(self, daemon_port: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{daemon_port}", daemon_port)
}
pub fn with_mail_address(self, mail_address: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{mail_addr}", mail_address)
}
pub fn with_mail_host(self, mail_host_address: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{mail_host}", mail_host_address)
}
pub fn with_mail_mailer(self, mail_mailer: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{mail_mailer}", mail_mailer)
}
pub fn with_rcpt_address(self, rcpt_address: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{rcpt_addr}", rcpt_address)
}
pub fn with_rcpt_host(self, rcpt_host: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{rcpt_host}", rcpt_host)
}
pub fn with_rcpt_mailer(self, rcpt_mailer: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{rcpt_mailer}", rcpt_mailer)
}
pub fn with_tls_version(self, tls_version: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{tls_version}", tls_version)
}
pub fn with_version(self, version: impl IntoMacroValue<'x>) -> Self {
self.with_macro(b"{v}", version)
}
}
impl<'x> IntoMacroValue<'x> for IpAddr {
fn into_macro_value(self) -> Cow<'x, [u8]> {
Cow::Owned(self.to_string().into_bytes())
}
}
impl<'x> IntoMacroValue<'x> for u16 {
fn into_macro_value(self) -> Cow<'x, [u8]> {
Cow::Owned(self.to_string().into_bytes())
}
}
impl<'x> IntoMacroValue<'x> for &'x [u8] {
fn into_macro_value(self) -> Cow<'x, [u8]> {
Cow::Borrowed(self)
}
}
impl<'x> IntoMacroValue<'x> for &'x str {
fn into_macro_value(self) -> Cow<'x, [u8]> {
Cow::Borrowed(self.as_bytes())
}
}
impl<'x> IntoMacroValue<'x> for &'x String {
fn into_macro_value(self) -> Cow<'x, [u8]> {
Cow::Borrowed(self.as_bytes())
}
}
impl<'x> IntoMacroValue<'x> for String {
fn into_macro_value(self) -> Cow<'x, [u8]> {
Cow::Owned(self.into_bytes())
}
}
impl<'x> IntoMacroValue<'x> for Vec<u8> {
fn into_macro_value(self) -> Cow<'x, [u8]> {
Cow::Owned(self)
}
}
impl<'x> IntoMacroValue<'x> for &'x Vec<u8> {
fn into_macro_value(self) -> Cow<'x, [u8]> {
Cow::Borrowed(self)
}
}
+550
View File
@@ -0,0 +1,550 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{Action, Error, Macros, Modification};
use crate::{
core::{Session, SessionAddress, SessionData},
inbound::{FilterResponse, milter::MilterClient},
queue::QueueId,
};
use common::{
DAEMON_NAME,
config::smtp::session::{Milter, Stage},
network::SessionStream,
};
use mail_auth::AuthenticatedMessage;
use smtp_proto::{IntoString, request::parser::Rfc5321Parser};
use std::{borrow::Cow, time::Instant};
use tokio::io::{AsyncRead, AsyncWrite};
use trc::MilterEvent;
use utils::DomainPart;
enum Rejection {
Action(Action),
Error(Error),
}
impl<T: SessionStream> Session<T> {
pub async fn run_milters(
&self,
stage: Stage,
message: Option<&AuthenticatedMessage<'_>>,
queue_id: Option<QueueId>,
) -> Result<Vec<Modification>, FilterResponse> {
let milters = &self.server.core.smtp.session.milters;
if milters.is_empty() {
return Ok(Vec::new());
}
let mut modifications = Vec::new();
for milter in milters {
if !milter.run_on_stage.contains(&stage)
|| !self
.server
.eval_if(&milter.enable, self, self.data.session_id)
.await
.unwrap_or(false)
{
continue;
}
let time = Instant::now();
match self.connect_and_run(milter, message).await {
Ok(new_modifications) => {
trc::event!(
Milter(MilterEvent::ActionAccept),
SpanId = self.data.session_id,
Id = milter.id.to_string(),
Elapsed = time.elapsed(),
);
if !modifications.is_empty() {
// The message body can only be replaced once, so we need to remove
// any previous replacements.
if new_modifications
.iter()
.any(|m| matches!(m, Modification::ReplaceBody { .. }))
{
modifications
.retain(|m| !matches!(m, Modification::ReplaceBody { .. }));
}
modifications.extend(new_modifications);
} else {
modifications = new_modifications;
}
}
Err(Rejection::Action(action)) => {
trc::event!(
Milter(match &action {
Action::Discard => MilterEvent::ActionDiscard,
Action::Reject => MilterEvent::ActionReject,
Action::TempFail => MilterEvent::ActionTempFail,
Action::ReplyCode { .. } => {
MilterEvent::ActionReplyCode
}
Action::Shutdown => MilterEvent::ActionShutdown,
Action::ConnectionFailure => MilterEvent::ActionConnectionFailure,
Action::Accept | Action::Continue => unreachable!(),
}),
SpanId = self.data.session_id,
QueueId = queue_id,
Id = milter.id.to_string(),
Elapsed = time.elapsed(),
);
return Err(match action {
Action::Discard => FilterResponse::accept(),
Action::Reject => FilterResponse::reject(),
Action::TempFail => FilterResponse::temp_fail(),
Action::ReplyCode { code, text } => {
let mut response = Vec::with_capacity(text.len() + 6);
response.extend_from_slice(code.as_slice());
response.push(b' ');
response.extend_from_slice(text.as_bytes());
if !text.ends_with('\n') {
response.extend_from_slice(b"\r\n");
}
FilterResponse {
message: response.into_string().into(),
disconnect: false,
}
}
Action::Shutdown => FilterResponse::shutdown(),
Action::ConnectionFailure => FilterResponse::default().disconnect(),
Action::Accept | Action::Continue => unreachable!(),
});
}
Err(Rejection::Error(err)) => {
let (code, details) = match err {
Error::Io(details) => {
(MilterEvent::IoError, trc::Value::from(details.to_string()))
}
Error::FrameTooLarge(size) => {
(MilterEvent::FrameTooLarge, trc::Value::from(size))
}
Error::FrameInvalid(bytes) => {
(MilterEvent::FrameInvalid, trc::Value::from(bytes))
}
Error::Unexpected(response) => (
MilterEvent::UnexpectedResponse,
trc::Value::from(response.to_string()),
),
Error::Timeout => (MilterEvent::Timeout, trc::Value::None),
Error::TLSInvalidName => (MilterEvent::TlsInvalidName, trc::Value::None),
Error::Disconnected => (MilterEvent::Disconnected, trc::Value::None),
};
trc::event!(
Milter(code),
SpanId = self.data.session_id,
Id = milter.id.to_string(),
Details = details,
Elapsed = time.elapsed(),
);
if milter.tempfail_on_error {
return Err(FilterResponse::server_failure());
}
}
}
}
Ok(modifications)
}
async fn connect_and_run(
&self,
milter: &Milter,
message: Option<&AuthenticatedMessage<'_>>,
) -> Result<Vec<Modification>, Rejection> {
// Build client
let client = MilterClient::connect(milter, self.data.session_id).await?;
if !milter.tls {
self.run(client, message).await
} else {
self.run(
client
.into_tls(
if !milter.tls_allow_invalid_certs {
&self.server.inner.data.smtp_connectors.pki_verify
} else {
&self.server.inner.data.smtp_connectors.dummy_verify
},
&milter.hostname,
)
.await?,
message,
)
.await
}
}
async fn run<S: AsyncRead + AsyncWrite + Unpin>(
&self,
mut client: MilterClient<S>,
message: Option<&AuthenticatedMessage<'_>>,
) -> Result<Vec<Modification>, Rejection> {
// Option negotiation
client.init().await?;
// Connect stage
let client_ptr = self
.data
.iprev
.as_ref()
.and_then(|ip_rev| ip_rev.ptr.as_ref())
.and_then(|ptrs| ptrs.first())
.map(|s| s.as_ref());
client
.connection(
client_ptr.unwrap_or(self.data.helo_domain.as_str()),
self.data.remote_ip,
self.data.remote_port,
Macros::new()
.with_daemon_name(DAEMON_NAME)
.with_local_hostname(&self.hostname)
.with_client_address(self.data.remote_ip)
.with_client_port(self.data.remote_port)
.with_client_ptr(client_ptr.unwrap_or("unknown")),
)
.await?
.assert_continue()?;
// EHLO/HELO
let (tls_version, tls_cipher) = self.stream.tls_version_and_cipher();
client
.helo(
&self.data.helo_domain,
Macros::new()
.with_cipher(tls_cipher.as_ref())
.with_tls_version(tls_version.as_ref()),
)
.await?
.assert_continue()?;
// Mail from
if let Some(mail_from) = &self.data.mail_from {
let addr = &mail_from.address_lcase;
client
.mail_from(
&format!("<{addr}>"),
None::<&[&str]>,
if let Some(name) = self.authenticated_as() {
Macros::new()
.with_mail_address(addr)
.with_sasl_login_name(name)
} else {
Macros::new().with_mail_address(addr)
},
)
.await?
.assert_continue()?;
// Rcpt to
for rcpt in &self.data.rcpt_to {
client
.rcpt_to(
&format!("<{}>", rcpt.address_lcase),
None::<&[&str]>,
Macros::new().with_rcpt_address(&rcpt.address_lcase),
)
.await?
.assert_continue()?;
}
}
if let Some(message) = message {
// Data
client.data().await?.assert_continue()?;
// Headers
client
.headers(message.raw_parsed_headers().iter().map(|(k, v)| {
(
std::str::from_utf8(k).unwrap_or_default(),
std::str::from_utf8(v).unwrap_or_default(),
)
}))
.await?
.assert_continue()?;
// Message body
let (action, modifications) = client.body(message.raw_message()).await?;
action.assert_continue()?;
// Quit
let _ = client.quit().await;
// Return modifications
Ok(modifications)
} else {
// Quit
let _ = client.quit().await;
Ok(Vec::new())
}
}
}
impl SessionData {
pub fn apply_milter_modifications(
&mut self,
modifications: Vec<Modification>,
message: &AuthenticatedMessage<'_>,
) -> Option<Vec<u8>> {
let mut body = Vec::new();
let mut header_changes = Vec::new();
let mut needs_rewrite = false;
for modification in modifications {
match modification {
Modification::ChangeFrom { sender, mut args } => {
// Change sender
let sender = strip_brackets(&sender);
let address_lcase = sender.to_lowercase();
let mut mail_from = SessionAddress {
domain: address_lcase.domain_part().into(),
address_lcase,
address: sender,
flags: 0,
dsn_info: None,
};
if !args.is_empty() {
args.push('\n');
match Rfc5321Parser::new(&mut args.as_bytes().iter())
.mail_from_parameters(Cow::Borrowed(""))
{
Ok(addr) => {
mail_from.flags = addr.flags;
mail_from.dsn_info = addr.env_id.map(|e| e.into_owned());
}
Err(err) => {
trc::event!(
Milter(MilterEvent::ParseError),
SpanId = self.session_id,
Details = "Failed to parse milter mailFrom parameters",
Reason = err.to_string(),
);
}
}
}
self.mail_from = Some(mail_from);
}
Modification::AddRcpt {
recipient,
mut args,
} => {
// Add recipient
let recipient = strip_brackets(&recipient);
if recipient.contains('@') {
let address_lcase = recipient.to_lowercase();
let mut rcpt = SessionAddress {
domain: address_lcase.domain_part().into(),
address_lcase,
address: recipient,
flags: 0,
dsn_info: None,
};
if !args.is_empty() {
args.push('\n');
match Rfc5321Parser::new(&mut args.as_bytes().iter())
.rcpt_to_parameters(Cow::Borrowed(""))
{
Ok(addr) => {
rcpt.flags = addr.flags;
rcpt.dsn_info = addr.orcpt.map(|e| e.into_owned());
}
Err(err) => {
trc::event!(
Milter(MilterEvent::ParseError),
SpanId = self.session_id,
Details = "Failed to parse milter rcptTo parameters",
Reason = err.to_string(),
);
}
}
}
if !self.rcpt_to.contains(&rcpt) {
self.rcpt_to.push(rcpt);
}
}
}
Modification::DeleteRcpt { recipient } => {
let recipient = strip_brackets(&recipient);
self.rcpt_to.retain(|r| r.address_lcase != recipient);
}
Modification::ReplaceBody { value } => {
body.extend(value);
}
Modification::AddHeader { name, value } => {
header_changes.push((0, name, value, false));
}
Modification::InsertHeader { index, name, value } => {
header_changes.push((index, name, value, false));
needs_rewrite = true;
}
Modification::ChangeHeader { index, name, value } => {
if value.is_empty()
|| message
.raw_parsed_headers()
.iter()
.any(|(n, _)| n.eq_ignore_ascii_case(name.as_bytes()))
{
header_changes.push((index, name, value, true));
needs_rewrite = true;
} else {
header_changes.push((0, name, value, false));
}
}
Modification::Quarantine { reason } => {
header_changes.push((0, "X-Quarantine".into(), reason, false));
}
}
}
// If there are no header changes return
if header_changes.is_empty() {
return if !body.is_empty() {
let mut new_message = Vec::with_capacity(body.len() + message.raw_headers().len());
new_message.extend_from_slice(message.raw_headers());
new_message.extend(body);
Some(new_message)
} else {
None
};
}
let new_body = if !body.is_empty() {
&body[..]
} else {
message.raw_body()
};
if needs_rewrite {
let mut headers = message
.raw_parsed_headers()
.iter()
.map(|(h, v)| (Cow::from(*h), Cow::from(*v)))
.collect::<Vec<_>>();
// Perform changes
for (index, header_name, header_value, is_change) in header_changes {
if is_change {
let mut header_count = 0;
for (pos, (name, value)) in headers.iter_mut().enumerate() {
if name.eq_ignore_ascii_case(header_name.as_bytes()) {
header_count += 1;
if header_count == index {
if !header_value.is_empty() {
*value = Cow::from(header_value.as_bytes().to_vec());
} else {
headers.remove(pos);
}
break;
}
}
}
} else {
let mut header_pos = 0;
if index > 0 {
let mut header_count = 0;
for (pos, (name, _)) in headers.iter().enumerate() {
if name.eq_ignore_ascii_case(header_name.as_bytes()) {
header_pos = pos;
header_count += 1;
if header_count == index {
break;
}
}
}
}
headers.insert(
header_pos,
(
Cow::from(header_name.as_bytes().to_vec()),
Cow::from(header_value.as_bytes().to_vec()),
),
);
}
}
// Write new headers
let mut new_message = Vec::with_capacity(
new_body.len()
+ message.raw_headers().len()
+ headers
.iter()
.map(|(h, v)| h.len() + v.len() + 4)
.sum::<usize>(),
);
for (header, value) in headers {
new_message.extend_from_slice(header.as_ref());
if value.first().is_some_and(|c| c.is_ascii_whitespace()) {
new_message.extend_from_slice(b":");
} else {
new_message.extend_from_slice(b": ");
}
new_message.extend_from_slice(value.as_ref());
if value.last().is_none_or(|c| *c != b'\n') {
new_message.extend_from_slice(b"\r\n");
}
}
new_message.extend_from_slice(b"\r\n");
new_message.extend(new_body);
Some(new_message)
} else {
let mut new_message = Vec::with_capacity(
new_body.len()
+ message.raw_headers().len()
+ header_changes
.iter()
.map(|(_, h, v, _)| h.len() + v.len() + 4)
.sum::<usize>(),
);
for (_, header, value, _) in header_changes {
new_message.extend_from_slice(header.as_bytes());
new_message.extend_from_slice(b": ");
new_message.extend_from_slice(value.as_bytes());
if !value.ends_with('\n') {
new_message.extend_from_slice(b"\r\n");
}
}
new_message.extend_from_slice(message.raw_headers());
new_message.extend(new_body);
Some(new_message)
}
}
}
impl Action {
fn assert_continue(self) -> Result<(), Rejection> {
match self {
Action::Continue | Action::Accept => Ok(()),
action => Err(Rejection::Action(action)),
}
}
}
impl From<Error> for Rejection {
fn from(err: Error) -> Self {
Rejection::Error(err)
}
}
fn strip_brackets(addr: &str) -> String {
let addr = addr.trim();
if let Some(addr) = addr.strip_prefix('<') {
if let Some((addr, _)) = addr.rsplit_once('>') {
addr.trim().into()
} else {
addr.trim().into()
}
} else {
addr.into()
}
}
+488
View File
@@ -0,0 +1,488 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use self::receiver::Receiver;
use common::config::smtp::session::MilterVersion;
use registry::types::id::ObjectId;
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, fmt::Display, net::IpAddr, time::Duration};
use tokio::io::{AsyncRead, AsyncWrite};
pub mod client;
pub mod macros;
pub mod message;
pub mod protocol;
pub mod receiver;
pub struct MilterClient<T: AsyncRead + AsyncWrite> {
stream: T,
buf: Vec<u8>,
bytes_read: usize,
timeout_cmd: Duration,
timeout_data: Duration,
receiver: Receiver,
version: MilterVersion,
options: u32,
flags_actions: u32,
flags_protocol: u32,
id: ObjectId,
session_id: u64,
}
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
FrameTooLarge(usize),
FrameInvalid(Vec<u8>),
Unexpected(Response),
Timeout,
TLSInvalidName,
Disconnected,
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Error::Io(err)
}
}
pub enum Command<'x> {
Abort,
Body {
value: &'x [u8],
},
EndOfBody,
Data,
Connect {
hostname: &'x [u8],
port: u16,
address: IpAddr,
},
Macro {
macros: Macros<'x>,
},
Header {
name: &'x [u8],
value: &'x [u8],
},
EndOfHeader,
Helo {
hostname: &'x [u8],
},
MailFrom {
sender: &'x [u8],
args: Option<Vec<&'x [u8]>>,
},
Rcpt {
recipient: &'x [u8],
args: Option<Vec<&'x [u8]>>,
},
OptionNegotiation(Options),
Quit,
QuitNewConnection,
}
#[derive(Debug)]
pub enum Response {
Action(Action),
Modification(Modification),
Progress,
Skip,
SetSymbols,
OptionNegotiation(Options),
}
#[derive(Debug)]
pub enum Action {
Accept,
Continue,
Discard,
Reject,
TempFail,
ReplyCode { code: [u8; 3], text: String },
Shutdown,
ConnectionFailure,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Modification {
ChangeFrom {
sender: String,
args: String,
},
AddRcpt {
recipient: String,
args: String,
},
DeleteRcpt {
recipient: String,
},
ReplaceBody {
value: Vec<u8>,
},
AddHeader {
name: String,
value: String,
},
InsertHeader {
index: u32,
name: String,
value: String,
},
ChangeHeader {
index: u32,
name: String,
value: String,
},
Quarantine {
reason: String,
},
}
#[derive(Debug)]
pub struct Options {
pub version: u32,
pub actions: u32,
pub protocol: u32,
}
#[derive(Default)]
pub struct Macros<'x> {
cmdcode: u8,
macros: Vec<Macro<'x>>,
}
pub struct Macro<'x> {
name: &'x [u8],
value: Cow<'x, [u8]>,
}
pub const SMFIF_NONE: u32 = 0x00000000; /* no flags */
pub const SMFIF_ADDHDRS: u32 = 0x00000001; /* filter may add headers */
pub const SMFIF_CHGBODY: u32 = 0x00000002; /* filter may replace body */
pub const SMFIF_MODBODY: u32 = SMFIF_CHGBODY; /* backwards compatible */
pub const SMFIF_ADDRCPT: u32 = 0x00000004; /* filter may add recipients */
pub const SMFIF_DELRCPT: u32 = 0x00000008; /* filter may delete recipients */
pub const SMFIF_CHGHDRS: u32 = 0x00000010; /* filter may change/delete headers */
pub const SMFIF_QUARANTINE: u32 = 0x00000020; /* filter may quarantine envelope */
pub const SMFIF_CHGFROM: u32 = 0x00000040; /* filter may change "from" (envelope sender) */
pub const SMFIF_ADDRCPT_PAR: u32 = 0x00000080; /* add recipients incl. args */
pub const SMFIF_SETSYMLIST: u32 = 0x00000100; /* filter can send set of symbols (macros) that it wants */
pub const SMFIP_NOCONNECT: u32 = 0x00000001; /* MTA should not send connect info */
pub const SMFIP_NOHELO: u32 = 0x00000002; /* MTA should not send HELO info */
pub const SMFIP_NOMAIL: u32 = 0x00000004; /* MTA should not send MAIL info */
pub const SMFIP_NORCPT: u32 = 0x00000008; /* MTA should not send RCPT info */
pub const SMFIP_NOBODY: u32 = 0x00000010; /* MTA should not send body */
pub const SMFIP_NOHDRS: u32 = 0x00000020; /* MTA should not send headers */
pub const SMFIP_NOEOH: u32 = 0x00000040; /* MTA should not send EOH */
pub const SMFIP_NR_HDR: u32 = 0x00000080; /* No reply for headers */
pub const SMFIP_NOHREPL: u32 = SMFIP_NR_HDR; /* No reply for headers */
pub const SMFIP_NOUNKNOWN: u32 = 0x00000100; /* MTA should not send unknown commands */
pub const SMFIP_NODATA: u32 = 0x00000200; /* MTA should not send DATA */
pub const SMFIP_SKIP: u32 = 0x00000400; /* MTA understands SMFIS_SKIP */
pub const SMFIP_RCPT_REJ: u32 = 0x00000800; /* MTA should also send rejected RCPTs */
pub const SMFIP_NR_CONN: u32 = 0x00001000; /* No reply for connect */
pub const SMFIP_NR_HELO: u32 = 0x00002000; /* No reply for HELO */
pub const SMFIP_NR_MAIL: u32 = 0x00004000; /* No reply for MAIL */
pub const SMFIP_NR_RCPT: u32 = 0x00008000; /* No reply for RCPT */
pub const SMFIP_NR_DATA: u32 = 0x00010000; /* No reply for DATA */
pub const SMFIP_NR_UNKN: u32 = 0x00020000; /* No reply for UNKN */
pub const SMFIP_NR_EOH: u32 = 0x00040000; /* No reply for eoh */
pub const SMFIP_NR_BODY: u32 = 0x00080000; /* No reply for body chunk */
pub const SMFIP_HDR_LEADSPC: u32 = 0x00100000; /* header value leading space */
pub const SMFIP_MDS_256K: u32 = 0x10000000; /* MILTER_MAX_DATA_SIZE=256K */
pub const SMFIP_MDS_1M: u32 = 0x20000000; /* MILTER_MAX_DATA_SIZE=1M */
pub type Result<T> = std::result::Result<T, Error>;
impl Display for Command<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Command::Abort => write!(f, "ABORT"),
Command::Body { value } => write!(f, "BODY [{} bytes]", value.len()),
Command::EndOfBody => write!(f, "EOB"),
Command::Connect {
hostname,
port,
address,
} => write!(
f,
"CONNECT (host: {:?}, port: {}, address: {})",
std::str::from_utf8(hostname).unwrap_or_default(),
port,
address
),
Command::Macro { macros } => {
write!(f, "MACRO (code: {}, params: ", macros.cmdcode)?;
for macro_ in &macros.macros {
write!(
f,
"({:?}, {:?})",
std::str::from_utf8(macro_.name).unwrap_or_default(),
std::str::from_utf8(macro_.value.as_ref()).unwrap_or_default()
)?;
}
write!(f, ")")
}
Command::Header { name, value } => {
write!(
f,
"HEADER ({}: {:?})",
std::str::from_utf8(name).unwrap_or_default(),
std::str::from_utf8(value).unwrap_or_default()
)
}
Command::EndOfHeader => write!(f, "EOH"),
Command::Helo { hostname } => write!(
f,
"HELO {:?}",
std::str::from_utf8(hostname).unwrap_or_default()
),
Command::MailFrom { sender, args } => {
write!(
f,
"MAIL (from: {}, params: ",
std::str::from_utf8(sender).unwrap_or_default()
)?;
if let Some(args) = args {
for arg in args {
write!(f, " {}", std::str::from_utf8(arg).unwrap_or_default())?;
}
}
write!(f, ")")
}
Command::Rcpt { recipient, args } => {
write!(
f,
"RCPT (to: {}, params: ",
std::str::from_utf8(recipient).unwrap_or_default()
)?;
if let Some(args) = args {
for arg in args {
write!(f, " {}", std::str::from_utf8(arg).unwrap_or_default())?;
}
}
write!(f, ")")
}
Command::OptionNegotiation(opt) => write!(f, "OPTNEG ({})", opt),
Command::Quit => write!(f, "QUIT"),
Command::Data => write!(f, "DATA"),
Command::QuitNewConnection => write!(f, "QUIT_NC"),
}
}
}
impl Display for Response {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Response::Action(action) => write!(f, "ACTION ({})", action),
Response::Modification(modification) => write!(f, "MODIFICATION ({})", modification),
Response::Progress => write!(f, "PROGRESS"),
Response::OptionNegotiation(opt) => write!(f, "OPTNEG ({})", opt),
Response::Skip => write!(f, "SKIP"),
Response::SetSymbols => write!(f, "SET_SYMBOLS"),
}
}
}
impl Display for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Action::Accept => write!(f, "ACCEPT"),
Action::Continue => write!(f, "CONTINUE"),
Action::Discard => write!(f, "DISCARD"),
Action::Reject => write!(f, "REJECT"),
Action::TempFail => write!(f, "TEMPFAIL"),
Action::ReplyCode { code, text } => {
write!(f, "REPLYCODE (code: {:?}, text: {})", code, text)
}
Action::Shutdown => write!(f, "SHUTDOWN"),
Action::ConnectionFailure => write!(f, "CONN_FAIL"),
}
}
}
impl Display for Modification {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Modification::AddRcpt { recipient, args } => {
write!(f, "ADD_RCPT (recipient: {}, args: {})", recipient, args)
}
Modification::DeleteRcpt { recipient } => {
write!(f, "DEL_RCPT (recipient: {})", recipient)
}
Modification::ReplaceBody { value } => {
write!(f, "REPLACE_BODY ({} bytes)", value.len())
}
Modification::AddHeader { name, value } => {
write!(f, "ADD_HEADER ({}: {})", name, value)
}
Modification::ChangeHeader { index, name, value } => {
write!(f, "CHANGE_HEADER (index: {}, {}: {})", index, name, value)
}
Modification::Quarantine { reason } => write!(f, "QUARANTINE ({})", reason),
Modification::ChangeFrom { sender, args } => {
write!(f, "CHANGE_FROM (<{}> {})", sender, args)
}
Modification::InsertHeader { index, name, value } => {
write!(f, "INSERT_HEADER (index: {}, {}: {})", index, name, value)
}
}
}
}
impl Display for Options {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "version: {}, actions: [", self.version,)?;
if self.actions & SMFIF_ADDHDRS != 0 {
write!(f, "ADDHDRS ")?;
}
if self.actions & SMFIF_CHGBODY != 0 {
write!(f, "CHGBODY ")?;
}
if self.actions & SMFIF_CHGHDRS != 0 {
write!(f, "CHGHDRS ")?;
}
if self.actions & SMFIF_ADDRCPT != 0 {
write!(f, "ADDRCPT ")?;
}
if self.actions & SMFIF_DELRCPT != 0 {
write!(f, "DELRCPT ")?;
}
if self.actions & SMFIF_CHGFROM != 0 {
write!(f, "CHGFROM ")?;
}
if self.actions & SMFIF_QUARANTINE != 0 {
write!(f, "QUARANTINE ")?;
}
if self.actions & SMFIF_CHGFROM != 0 {
write!(f, "CHGFROM ")?;
}
if self.actions & SMFIF_ADDRCPT_PAR != 0 {
write!(f, "ADDRCPT_PAR ")?;
}
if self.actions & SMFIF_SETSYMLIST != 0 {
write!(f, "SETSYMLIST ")?;
}
write!(f, "], options: [",)?;
if self.protocol & SMFIP_NOCONNECT != 0 {
write!(f, "NOCONNECT ")?;
}
if self.protocol & SMFIP_NOHELO != 0 {
write!(f, "NOHELO ")?;
}
if self.protocol & SMFIP_NOMAIL != 0 {
write!(f, "NOMAIL ")?;
}
if self.protocol & SMFIP_NORCPT != 0 {
write!(f, "NORCPT ")?;
}
if self.protocol & SMFIP_NOBODY != 0 {
write!(f, "NOBODY ")?;
}
if self.protocol & SMFIP_NOHDRS != 0 {
write!(f, "NOHDRS ")?;
}
if self.protocol & SMFIP_NOEOH != 0 {
write!(f, "NOEOH ")?;
}
if self.protocol & SMFIP_NR_HDR != 0 {
write!(f, "NR_HDR ")?;
}
if self.protocol & SMFIP_NOUNKNOWN != 0 {
write!(f, "NOUNKNOWN ")?;
}
if self.protocol & SMFIP_NODATA != 0 {
write!(f, "NODATA ")?;
}
if self.protocol & SMFIP_SKIP != 0 {
write!(f, "SKIP ")?;
}
if self.protocol & SMFIP_RCPT_REJ != 0 {
write!(f, "RCPT_REJ ")?;
}
if self.protocol & SMFIP_NR_CONN != 0 {
write!(f, "NR_CONN ")?;
}
if self.protocol & SMFIP_NR_HELO != 0 {
write!(f, "NR_HELO ")?;
}
if self.protocol & SMFIP_NR_MAIL != 0 {
write!(f, "NR_MAIL ")?;
}
if self.protocol & SMFIP_NR_RCPT != 0 {
write!(f, "NR_RCPT ")?;
}
if self.protocol & SMFIP_NR_DATA != 0 {
write!(f, "NR_DATA ")?;
}
if self.protocol & SMFIP_NR_UNKN != 0 {
write!(f, "NR_UNKN ")?;
}
if self.protocol & SMFIP_NR_EOH != 0 {
write!(f, "NR_EOH ")?;
}
if self.protocol & SMFIP_NR_BODY != 0 {
write!(f, "NR_BODY ")?;
}
if self.protocol & SMFIP_HDR_LEADSPC != 0 {
write!(f, "HDR_LEADSPC ")?;
}
if self.protocol & SMFIP_MDS_256K != 0 {
write!(f, "MDS_256K ")?;
}
if self.protocol & SMFIP_MDS_1M != 0 {
write!(f, "MDS_1M ")?;
}
write!(f, "]")
}
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Io(err) => write!(f, "IO error: {}", err),
Error::FrameTooLarge(size) => {
write!(f, "Milter response of {} bytes is too large.", size)
}
Error::FrameInvalid(frame) => write!(
f,
"Invalid milter response: {:?}",
frame.get(0..100).unwrap_or(frame.as_ref())
),
Error::Unexpected(response) => write!(f, "Unexpected response: {}", response),
Error::Timeout => write!(f, "Connection timed out"),
Error::TLSInvalidName => write!(f, "Invalid TLS name"),
Error::Disconnected => write!(f, "Disconnected unexpectedly"),
}
}
}
+532
View File
@@ -0,0 +1,532 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::net::IpAddr;
use crate::inbound::milter::Action;
use super::{Command, Error, Modification, Options, Response};
pub const SMFIR_ADDRCPT: u8 = b'+'; /* add recipient */
pub const SMFIR_DELRCPT: u8 = b'-'; /* remove recipient */
pub const SMFIR_ADDRCPT_PAR: u8 = b'2'; /* add recipient (incl. ESMTP args) */
pub const SMFIR_SHUTDOWN: u8 = b'4'; /* 421: shutdown (internal to MTA) */
pub const SMFIR_ACCEPT: u8 = b'a'; /* accept */
pub const SMFIR_REPLBODY: u8 = b'b'; /* replace body (chunk) */
pub const SMFIR_CONTINUE: u8 = b'c'; /* continue */
pub const SMFIR_DISCARD: u8 = b'd'; /* discard */
pub const SMFIR_CHGFROM: u8 = b'e'; /* change envelope sender (from) */
pub const SMFIR_CONN_FAIL: u8 = b'f'; /* cause a connection failure */
pub const SMFIR_ADDHEADER: u8 = b'h'; /* add header */
pub const SMFIR_INSHEADER: u8 = b'i'; /* insert header */
pub const SMFIR_SETSYMLIST: u8 = b'l'; /* set list of symbols (macros) */
pub const SMFIR_CHGHEADER: u8 = b'm'; /* change header */
pub const SMFIR_PROGRESS: u8 = b'p'; /* progress */
pub const SMFIR_QUARANTINE: u8 = b'q'; /* quarantine */
pub const SMFIR_REJECT: u8 = b'r'; /* reject */
pub const SMFIR_SKIP: u8 = b's'; /* skip */
pub const SMFIR_TEMPFAIL: u8 = b't'; /* tempfail */
pub const SMFIR_REPLYCODE: u8 = b'y'; /* reply code etc */
pub const SMFIC_ABORT: u8 = b'A'; /* Abort */
pub const SMFIC_BODY: u8 = b'B'; /* Body chunk */
pub const SMFIC_CONNECT: u8 = b'C'; /* Connection information */
pub const SMFIC_MACRO: u8 = b'D'; /* Define macro */
pub const SMFIC_BODYEOB: u8 = b'E'; /* final body chunk (End) */
pub const SMFIC_HELO: u8 = b'H'; /* HELO/EHLO */
pub const SMFIC_QUIT_NC: u8 = b'K'; /* QUIT but new connection follows */
pub const SMFIC_HEADER: u8 = b'L'; /* Header */
pub const SMFIC_MAIL: u8 = b'M'; /* MAIL from */
pub const SMFIC_EOH: u8 = b'N'; /* EOH */
pub const SMFIC_OPTNEG: u8 = b'O'; /* Option negotiation */
pub const SMFIC_QUIT: u8 = b'Q'; /* QUIT */
pub const SMFIC_RCPT: u8 = b'R'; /* RCPT to */
pub const SMFIC_DATA: u8 = b'T'; /* DATA */
pub const SMFIC_UNKNOWN: u8 = b'U'; /* Any unknown command */
impl Command<'_> {
fn build(command: u8, len: u32) -> Vec<u8> {
let mut buf = Vec::with_capacity(len as usize + 1 + std::mem::size_of::<u32>());
buf.extend_from_slice((len + 1).to_be_bytes().as_slice());
buf.push(command);
buf
}
pub fn serialize(self) -> Vec<u8> {
match self {
Command::Abort => Command::build(SMFIC_ABORT, 0),
Command::Body { value } => {
let mut buf = Command::build(SMFIC_BODY, value.len() as u32);
buf.extend(value);
buf
}
Command::EndOfBody => Command::build(SMFIC_BODYEOB, 0),
Command::Connect {
hostname,
port,
address,
} => {
/*
char hostname[] Hostname, NUL terminated
char family Protocol family (see below)
uint16 port Port number (SMFIA_INET or SMFIA_INET6 only)
char address[] IP address (ASCII) or unix socket path, NUL terminated
*/
let (address, family) = match address {
IpAddr::V4(address) => (address.to_string(), b'4'),
IpAddr::V6(address) => (address.to_string(), b'6'),
};
let mut buf = Command::build(
SMFIC_CONNECT,
hostname.len() as u32 // hostname
+ 1 // NUL
+ 1 // family
+ std::mem::size_of::<u16>() as u32 // port
+ address.len() as u32 // address
+ 1, // NUL
);
buf.extend(hostname);
buf.push(0x00);
buf.push(family);
buf.extend(port.to_be_bytes().as_slice());
buf.extend(address.as_bytes());
buf.push(0x00);
buf
}
Command::Macro { macros } => {
let mut buf = Command::build(
SMFIC_MACRO,
macros.macros.iter().fold(1, |acc, macro_| {
acc + macro_.name.len() as u32 + 1 + macro_.value.len() as u32 + 1
}),
);
buf.push(macros.cmdcode);
for macro_ in macros.macros {
buf.extend(macro_.name);
buf.push(0x00);
buf.extend(macro_.value.as_ref());
buf.push(0x00);
}
buf
}
Command::Header { name, value } => {
let mut buf =
Command::build(SMFIC_HEADER, name.len() as u32 + 1 + value.len() as u32 + 1);
buf.extend(name);
buf.push(0x00);
buf.extend(value);
buf.push(0x00);
buf
}
Command::EndOfHeader => Command::build(SMFIC_EOH, 0),
Command::Helo { hostname } => {
let mut buf = Command::build(SMFIC_HELO, hostname.len() as u32 + 1);
buf.extend(hostname);
buf.push(0x00);
buf
}
Command::MailFrom { sender, args } => {
let mut buf = Command::build(
SMFIC_MAIL,
sender.len() as u32 // sender
+ 1 // NUL
+ args.as_ref().map_or(0, |args| args.iter().fold(0, |acc, arg| acc + arg.len() as u32) + 1), // args
);
buf.extend(sender);
buf.push(0x00);
if let Some(args) = args {
for arg in args {
buf.extend(arg);
buf.push(0x00);
}
}
buf
}
Command::Rcpt { recipient, args } => {
let mut buf = Command::build(
SMFIC_RCPT,
recipient.len() as u32 // recipient
+ 1 // NUL
+ args.as_ref().map_or(0, |args| args.iter().fold(0, |acc, arg| acc + arg.len() as u32) + 1), // args
);
buf.extend(recipient);
buf.push(0x00);
if let Some(args) = args {
for arg in args {
buf.extend(arg);
buf.push(0x00);
}
}
buf
}
Command::OptionNegotiation(opt) => {
let mut buf = Command::build(SMFIC_OPTNEG, 3 * std::mem::size_of::<u32>() as u32);
buf.extend(opt.version.to_be_bytes().as_slice());
buf.extend(opt.actions.to_be_bytes().as_slice());
buf.extend(opt.protocol.to_be_bytes().as_slice());
buf
}
Command::Quit => Command::build(SMFIC_QUIT, 0),
// Version 6
Command::Data => Command::build(SMFIC_DATA, 0),
Command::QuitNewConnection => Command::build(SMFIC_QUIT_NC, 0),
}
}
#[cfg(feature = "test_mode")]
pub fn deserialize(bytes: &[u8]) -> Command<'_> {
let mut reader = PacketReader::new(bytes);
match reader.byte() {
SMFIC_ABORT => Command::Abort,
SMFIC_BODY => Command::Body { value: &bytes[1..] },
SMFIC_BODYEOB => Command::EndOfBody,
SMFIC_CONNECT => {
let hostname = reader.read_nul_terminated().unwrap();
let family = reader.byte();
let port = reader.read_u16();
let address = std::str::from_utf8(reader.read_nul_terminated().unwrap()).unwrap();
Command::Connect {
hostname,
port,
address: match family {
b'4' => IpAddr::V4(address.parse().unwrap()),
b'6' => IpAddr::V6(address.parse().unwrap()),
_ => unreachable!(),
},
}
}
SMFIC_MACRO => {
let cmdcode = reader.byte();
let mut macros = Vec::new();
while let Some(name) = reader.read_nul_terminated() {
let value = reader.read_nul_terminated().unwrap();
macros.push(super::Macro {
name,
value: value.into(),
});
}
Command::Macro {
macros: super::Macros { cmdcode, macros },
}
}
SMFIC_HEADER => {
let name = reader.read_nul_terminated().unwrap();
let value = reader.read_nul_terminated().unwrap();
Command::Header { name, value }
}
SMFIC_EOH => Command::EndOfHeader,
SMFIC_HELO => {
let hostname = reader.read_nul_terminated().unwrap();
Command::Helo { hostname }
}
SMFIC_MAIL => {
let sender = reader.read_nul_terminated().unwrap();
let mut args = Vec::new();
while let Some(arg) = reader.read_nul_terminated() {
args.push(arg);
}
Command::MailFrom {
sender,
args: Some(args),
}
}
SMFIC_RCPT => {
let recipient = reader.read_nul_terminated().unwrap();
let mut args = Vec::new();
while let Some(arg) = reader.read_nul_terminated() {
args.push(arg);
}
Command::Rcpt {
recipient,
args: Some(args),
}
}
SMFIC_OPTNEG => Command::OptionNegotiation(super::Options {
version: reader.read_u32(),
actions: reader.read_u32(),
protocol: reader.read_u32(),
}),
SMFIC_QUIT => Command::Quit,
SMFIC_DATA => Command::Data,
SMFIC_QUIT_NC => Command::QuitNewConnection,
c => panic!("Unknown command: {}", char::from(c)),
}
}
}
impl Response {
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let frame_len = bytes.len().saturating_sub(1);
let mut bytes = bytes.iter();
match *bytes.next()? {
SMFIR_ADDRCPT => Response::Modification(Modification::AddRcpt {
recipient: read_nul_terminated(&mut bytes, frame_len)?,
args: String::new(),
}),
SMFIR_DELRCPT => Response::Modification(Modification::DeleteRcpt {
recipient: read_nul_terminated(&mut bytes, frame_len)?,
}),
SMFIR_ACCEPT => Response::Action(Action::Accept),
SMFIR_REPLBODY => {
let mut body = Vec::with_capacity(frame_len);
body.extend(bytes);
Response::Modification(Modification::ReplaceBody { value: body })
}
SMFIR_CONTINUE => Response::Action(Action::Continue),
SMFIR_DISCARD => Response::Action(Action::Discard),
SMFIR_ADDHEADER => Response::Modification(Modification::AddHeader {
name: read_nul_terminated(&mut bytes, 16)?,
value: read_nul_terminated(&mut bytes, frame_len)?,
}),
SMFIR_CHGHEADER => Response::Modification(Modification::ChangeHeader {
index: read_u32(&mut bytes)?,
name: read_nul_terminated(&mut bytes, 16)?,
value: read_nul_terminated(&mut bytes, frame_len)?,
}),
SMFIR_PROGRESS => Response::Progress,
SMFIR_QUARANTINE => Response::Modification(Modification::Quarantine {
reason: read_nul_terminated(&mut bytes, frame_len)?,
}),
SMFIR_REJECT => Response::Action(Action::Reject),
SMFIR_TEMPFAIL => Response::Action(Action::TempFail),
SMFIR_REPLYCODE => {
let code = [*bytes.next()?, *bytes.next()?, *bytes.next()?];
bytes.next()?; // Space
Response::Action(Action::ReplyCode {
code,
text: read_nul_terminated(&mut bytes, frame_len)?,
})
}
SMFIC_OPTNEG => Response::OptionNegotiation(Options {
version: read_u32(&mut bytes)?,
actions: read_u32(&mut bytes)?,
protocol: read_u32(&mut bytes)?,
}),
// V6
SMFIR_ADDRCPT_PAR => Response::Modification(Modification::AddRcpt {
recipient: read_nul_terminated(&mut bytes, frame_len)?,
args: read_nul_terminated(&mut bytes, frame_len)?,
}),
SMFIR_CHGFROM => Response::Modification(Modification::ChangeFrom {
sender: read_nul_terminated(&mut bytes, frame_len)?,
args: read_nul_terminated(&mut bytes, frame_len)?,
}),
SMFIR_SKIP => Response::Skip,
SMFIR_SETSYMLIST => Response::SetSymbols,
SMFIR_SHUTDOWN => Response::Action(Action::Shutdown),
SMFIR_CONN_FAIL => Response::Action(Action::ConnectionFailure),
SMFIR_INSHEADER => Response::Modification(Modification::InsertHeader {
index: read_u32(&mut bytes)?,
name: read_nul_terminated(&mut bytes, 16)?,
value: read_nul_terminated(&mut bytes, frame_len)?,
}),
_ => return None,
}
.into()
}
pub fn can_continue(&self) -> bool {
matches!(
self,
Response::Progress | Response::Action(Action::Accept | Action::Continue)
)
}
pub fn into_action(self) -> super::Result<Action> {
match self {
Response::Action(action) => Ok(action),
response => Err(Error::Unexpected(response)),
}
}
#[cfg(feature = "test_mode")]
pub fn serialize(&self) -> Vec<u8> {
match self {
Response::Action(action) => match action {
Action::Accept => Command::build(SMFIR_ACCEPT, 0),
Action::Continue => Command::build(SMFIR_CONTINUE, 0),
Action::Discard => Command::build(SMFIR_DISCARD, 0),
Action::Reject => Command::build(SMFIR_REJECT, 0),
Action::TempFail => Command::build(SMFIR_TEMPFAIL, 0),
Action::ReplyCode { code, text } => {
let mut buf = Command::build(SMFIR_REPLYCODE, text.len() as u32 + 4 + 1);
buf.extend(code);
buf.push(b' ');
buf.extend(text.as_bytes());
buf.push(0x00);
buf
}
Action::Shutdown => Command::build(SMFIR_SHUTDOWN, 0),
Action::ConnectionFailure => Command::build(SMFIR_CONN_FAIL, 0),
},
Response::Modification(modif) => match modif {
Modification::ChangeFrom { sender, args } => {
let mut buf =
Command::build(SMFIR_CHGFROM, sender.len() as u32 + args.len() as u32 + 2);
buf.extend(sender.as_bytes());
buf.push(0x00);
buf.extend(args.as_bytes());
buf.push(0x00);
buf
}
Modification::AddRcpt { recipient, args } => {
let mut buf = Command::build(
SMFIR_ADDRCPT_PAR,
recipient.len() as u32 + args.len() as u32 + 2,
);
buf.extend(recipient.as_bytes());
buf.push(0x00);
buf.extend(args.as_bytes());
buf.push(0x00);
buf
}
Modification::DeleteRcpt { recipient } => {
let mut buf = Command::build(SMFIR_DELRCPT, recipient.len() as u32 + 1);
buf.extend(recipient.as_bytes());
buf.push(0x00);
buf
}
Modification::ReplaceBody { value } => {
let mut buf = Command::build(SMFIR_REPLBODY, value.len() as u32);
buf.extend(value);
buf
}
Modification::AddHeader { name, value } => {
let mut buf =
Command::build(SMFIR_ADDHEADER, name.len() as u32 + value.len() as u32 + 2);
buf.extend(name.as_bytes());
buf.push(0x00);
buf.extend(value.as_bytes());
buf.push(0x00);
buf
}
Modification::InsertHeader { index, name, value } => {
let mut buf = Command::build(
SMFIR_INSHEADER,
name.len() as u32
+ value.len() as u32
+ std::mem::size_of::<u32>() as u32
+ 2,
);
buf.extend(index.to_be_bytes().as_slice());
buf.extend(name.as_bytes());
buf.push(0x00);
buf.extend(value.as_bytes());
buf.push(0x00);
buf
}
Modification::ChangeHeader { index, name, value } => {
let mut buf = Command::build(
SMFIR_CHGHEADER,
name.len() as u32
+ value.len() as u32
+ std::mem::size_of::<u32>() as u32
+ 2,
);
buf.extend(index.to_be_bytes().as_slice());
buf.extend(name.as_bytes());
buf.push(0x00);
buf.extend(value.as_bytes());
buf.push(0x00);
buf
}
Modification::Quarantine { reason } => {
let mut buf = Command::build(SMFIR_QUARANTINE, reason.len() as u32 + 1);
buf.extend(reason.as_bytes());
buf.push(0x00);
buf
}
},
Response::Progress => Command::build(SMFIR_PROGRESS, 0),
Response::Skip => Command::build(SMFIR_SKIP, 0),
Response::SetSymbols => Command::build(SMFIR_SETSYMLIST, 0),
Response::OptionNegotiation(opt) => {
let mut buf = Command::build(SMFIC_OPTNEG, 3 * std::mem::size_of::<u32>() as u32);
buf.extend(opt.version.to_be_bytes().as_slice());
buf.extend(opt.actions.to_be_bytes().as_slice());
buf.extend(opt.protocol.to_be_bytes().as_slice());
buf
}
}
}
}
fn read_nul_terminated(bytes: &mut std::slice::Iter<u8>, expected_len: usize) -> Option<String> {
let mut buf = Vec::with_capacity(expected_len);
loop {
match bytes.next()? {
0x00 => break,
byte => buf.push(*byte),
}
}
String::from_utf8(buf).ok()
}
fn read_u32(bytes: &mut std::slice::Iter<u8>) -> Option<u32> {
let mut buf = [0u8; 4];
for byte in buf.iter_mut() {
*byte = *bytes.next()?;
}
Some(u32::from_be_bytes(buf))
}
#[cfg(feature = "test_mode")]
pub struct PacketReader<'x> {
bytes: &'x [u8],
iter: std::iter::Enumerate<std::slice::Iter<'x, u8>>,
}
#[cfg(feature = "test_mode")]
impl<'x> PacketReader<'x> {
pub fn new(bytes: &'x [u8]) -> PacketReader<'x> {
Self {
bytes,
iter: bytes.iter().enumerate(),
}
}
pub fn byte(&mut self) -> u8 {
*self.iter.next().unwrap().1
}
pub fn read_nul_terminated(&mut self) -> Option<&'x [u8]> {
let (start_pos, ch) = self.iter.next()?;
let mut end_pos = start_pos;
if *ch != 0x00 {
loop {
match self.iter.next().unwrap().1 {
0x00 => break,
_ => end_pos += 1,
}
}
}
Some(&self.bytes[start_pos..end_pos + 1])
}
pub fn read_u32(&mut self) -> u32 {
let mut buf = [0u8; 4];
for byte in buf.iter_mut() {
*byte = self.byte();
}
u32::from_be_bytes(buf)
}
pub fn read_u16(&mut self) -> u16 {
let mut buf = [0u8; 2];
for byte in buf.iter_mut() {
*byte = self.byte();
}
u16::from_be_bytes(buf)
}
}
+110
View File
@@ -0,0 +1,110 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
enum State {
Len {
buf: [u8; std::mem::size_of::<u32>()],
bytes_read: usize,
},
Frame {
buf: Vec<u8>,
frame_len: usize,
},
}
pub struct Receiver {
packet_pos: usize,
state: State,
max_frame_len: usize,
}
pub enum FrameResult<'x> {
Frame(Cow<'x, [u8]>),
Incomplete,
TooLarge(usize),
}
impl Default for State {
fn default() -> Self {
State::Len {
buf: [0; std::mem::size_of::<u32>()],
bytes_read: 0,
}
}
}
impl Receiver {
pub fn with_max_frame_len(max_frame_len: usize) -> Self {
Receiver {
packet_pos: 0,
state: State::default(),
max_frame_len,
}
}
pub fn read_frame<'x>(&mut self, packet: &'x [u8]) -> FrameResult<'x> {
if !packet.is_empty() {
match &mut self.state {
State::Len { buf, bytes_read } => {
while *bytes_read < std::mem::size_of::<u32>() {
if let Some(byte) = packet.get(self.packet_pos) {
buf[*bytes_read] = *byte;
*bytes_read += 1;
self.packet_pos += 1;
} else {
self.packet_pos = 0;
return FrameResult::Incomplete;
}
}
let length = u32::from_be_bytes(*buf) as usize;
if length <= self.max_frame_len {
if let Some(frame) = packet.get(self.packet_pos..self.packet_pos + length) {
self.packet_pos += length;
self.state = State::default();
FrameResult::Frame(frame.into())
} else {
let mut buf = Vec::with_capacity(length);
if let Some(bytes_available) = packet.get(self.packet_pos..) {
buf.extend(bytes_available);
}
self.state = State::Frame {
buf,
frame_len: length,
};
self.packet_pos = 0;
FrameResult::Incomplete
}
} else {
FrameResult::TooLarge(length)
}
}
State::Frame { buf, frame_len } => {
let bytes_pending = *frame_len - buf.len();
if let Some(bytes) =
packet.get(self.packet_pos..self.packet_pos + bytes_pending)
{
let mut buf = std::mem::take(buf);
buf.extend(bytes);
self.packet_pos += bytes_pending;
self.state = State::default();
FrameResult::Frame(buf.into())
} else if let Some(bytes_available) = packet.get(self.packet_pos..) {
buf.extend(bytes_available);
self.packet_pos = 0;
FrameResult::Incomplete
} else {
self.packet_pos = 0;
FrameResult::Incomplete
}
}
}
} else {
FrameResult::Incomplete
}
}
}
+143
View File
@@ -0,0 +1,143 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_auth::{DkimResult, DmarcResult, IprevResult, SpfResult, dmarc::Policy};
use std::borrow::Cow;
pub mod auth;
pub mod data;
pub mod dkim;
pub mod ehlo;
pub mod hooks;
pub mod mail;
pub mod milter;
pub mod rcpt;
pub mod session;
pub mod spam;
pub mod spawn;
pub mod vrfy;
#[derive(Debug, Default)]
pub struct FilterResponse {
pub message: Cow<'static, str>,
pub disconnect: bool,
}
pub trait AuthResult {
fn as_str(&self) -> &'static str;
}
impl AuthResult for SpfResult {
fn as_str(&self) -> &'static str {
match self {
SpfResult::Pass => "pass",
SpfResult::Fail => "fail",
SpfResult::SoftFail => "softfail",
SpfResult::Neutral => "neutral",
SpfResult::None => "none",
SpfResult::TempError => "temperror",
SpfResult::PermError => "permerror",
}
}
}
impl AuthResult for IprevResult {
fn as_str(&self) -> &'static str {
match self {
IprevResult::Pass => "pass",
IprevResult::Fail(_) => "fail",
IprevResult::TempError(_) => "temperror",
IprevResult::PermError(_) => "permerror",
IprevResult::None => "none",
}
}
}
impl AuthResult for DkimResult {
fn as_str(&self) -> &'static str {
match self {
DkimResult::Pass => "pass",
DkimResult::None => "none",
DkimResult::Neutral(_) => "neutral",
DkimResult::Fail(_) => "fail",
DkimResult::PermError(_) => "permerror",
DkimResult::TempError(_) => "temperror",
}
}
}
impl AuthResult for DmarcResult {
fn as_str(&self) -> &'static str {
match self {
DmarcResult::Pass => "pass",
DmarcResult::Fail(_) => "fail",
DmarcResult::TempError(_) => "temperror",
DmarcResult::PermError(_) => "permerror",
DmarcResult::None => "none",
}
}
}
impl AuthResult for Policy {
fn as_str(&self) -> &'static str {
match self {
Policy::Reject => "reject",
Policy::Quarantine => "quarantine",
Policy::None | Policy::Unspecified => "none",
}
}
}
impl FilterResponse {
pub fn accept() -> Self {
Self {
message: Cow::Borrowed("250 2.0.0 Message queued for delivery.\r\n"),
disconnect: false,
}
}
pub fn reject() -> Self {
Self {
message: Cow::Borrowed("503 5.5.3 Message rejected.\r\n"),
disconnect: false,
}
}
pub fn temp_fail() -> Self {
Self {
message: Cow::Borrowed("451 4.3.5 Unable to accept message at this time.\r\n"),
disconnect: false,
}
}
pub fn shutdown() -> Self {
Self {
message: Cow::Borrowed("421 4.3.0 Server shutting down.\r\n"),
disconnect: true,
}
}
pub fn server_failure() -> Self {
Self {
message: Cow::Borrowed("451 4.3.5 Unable to accept message at this time.\r\n"),
disconnect: false,
}
}
pub fn disconnect(self) -> Self {
Self {
disconnect: true,
..self
}
}
pub fn into_bytes(self) -> Cow<'static, [u8]> {
match self.message {
Cow::Borrowed(s) => Cow::Borrowed(s.as_bytes()),
Cow::Owned(s) => Cow::Owned(s.into_bytes()),
}
}
}
+458
View File
@@ -0,0 +1,458 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
core::{Session, SessionAddress},
scripts::ScriptResult,
};
use common::{
KV_GREYLIST,
config::smtp::session::Stage,
network::{RcptResolution, SessionStream},
scripts::ScriptModification,
};
use smtp_proto::{
RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, RcptTo,
};
use std::borrow::Cow;
use store::dispatch::lookup::KeyValue;
use trc::{SecurityEvent, SmtpEvent};
use utils::DomainPart;
impl<T: SessionStream> Session<T> {
pub async fn handle_rcpt_to(&mut self, to: RcptTo<Cow<'_, str>>) -> Result<(), ()> {
#[cfg(feature = "test_mode")]
if self.instance.id.ends_with("-debug") {
if to.address.contains("fail@") {
return self.write(b"503 5.5.1 Invalid recipient.\r\n").await;
} else if (to.address.contains("delay-random@") && rand::random())
|| to.address.contains("delay@")
{
return self.write(b"451 4.5.3 Try again later.\r\n").await;
} else if to.address.contains("slow@") {
tokio::time::sleep(std::time::Duration::from_secs(
rand::random::<u64>() % 5 + 5,
))
.await;
}
}
if self.data.mail_from.is_none() {
trc::event!(
Smtp(SmtpEvent::MailFromMissing),
SpanId = self.data.session_id,
);
return self.write(b"503 5.5.1 MAIL is required first.\r\n").await;
} else if std::cmp::min(self.data.rcpt_to.len(), self.data.rcpt_oks) >= self.params.rcpt_max
{
trc::event!(
Smtp(SmtpEvent::TooManyRecipients),
SpanId = self.data.session_id,
Limit = self.params.rcpt_max,
);
return self.write(b"455 4.5.3 Too many recipients.\r\n").await;
}
// Verify parameters
if ((to.flags
& (RCPT_NOTIFY_DELAY | RCPT_NOTIFY_NEVER | RCPT_NOTIFY_SUCCESS | RCPT_NOTIFY_FAILURE)
!= 0)
|| to.orcpt.is_some())
&& !self.params.rcpt_dsn
{
trc::event!(Smtp(SmtpEvent::DsnDisabled), SpanId = self.data.session_id,);
return self
.write(b"501 5.5.4 DSN extension has been disabled.\r\n")
.await;
}
// Build RCPT
let address_lcase = to.address.to_lowercase_address(true);
let rcpt = SessionAddress {
domain: address_lcase.domain_part().into(),
address_lcase,
address: to.address.into_owned(),
flags: to.flags,
dsn_info: to.orcpt.map(|e| e.into_owned()),
};
if self.data.rcpt_to.contains(&rcpt) {
trc::event!(
Smtp(SmtpEvent::RcptToDuplicate),
SpanId = self.data.session_id,
To = rcpt.address_lcase,
);
self.data.rcpt_oks += 1;
return self.write(b"250 2.1.5 OK\r\n").await;
}
self.data.rcpt_to.push(rcpt);
// Address rewriting and Sieve filtering
let rcpt_config = &self.server.core.smtp.session.rcpt;
let rcpt_script = self
.server
.eval_if::<String, _>(&rcpt_config.script, self, self.data.session_id)
.await
.and_then(|name| {
self.server
.get_trusted_sieve_script(&name, self.data.session_id)
.map(|s| (s.clone(), name))
});
let session_config = &self.server.core.smtp.session;
if rcpt_script.is_some()
|| !rcpt_config.rewrite.is_empty()
|| session_config
.milters
.iter()
.any(|m| m.run_on_stage.contains(&Stage::Rcpt))
|| session_config
.hooks
.iter()
.any(|h| h.run_on_stage.contains(&Stage::Rcpt))
{
// Sieve filtering
if let Some((script, script_id)) = rcpt_script {
match self
.run_script(
script_id,
script.clone(),
self.build_script_parameters("rcpt"),
)
.await
{
ScriptResult::Accept { modifications } if !modifications.is_empty() => {
for modification in modifications {
if let ScriptModification::SetEnvelope { name, value } = modification {
self.data.apply_envelope_modification(name, value);
}
}
}
ScriptResult::Reject(message) => {
self.data.rcpt_to.pop();
return self.write(message.as_bytes()).await;
}
_ => (),
}
}
// Milter filtering
if let Err(message) = self.run_milters(Stage::Rcpt, None, None).await {
self.data.rcpt_to.pop();
return self.write(message.message.as_bytes()).await;
}
// MTAHook filtering
if let Err(message) = self.run_mta_hooks(Stage::Rcpt, None, None).await {
self.data.rcpt_to.pop();
return self.write(message.message.as_bytes()).await;
}
// Address rewriting
if let Some(new_address) = self
.server
.eval_if::<String, _>(&rcpt_config.rewrite, self, self.data.session_id)
.await
{
let rcpt = self.data.rcpt_to.last_mut().unwrap();
trc::event!(
Smtp(SmtpEvent::RcptToRewritten),
SpanId = self.data.session_id,
Details = rcpt.address_lcase.clone(),
To = new_address.clone(),
);
if new_address.contains('@') {
rcpt.address_lcase = new_address.to_lowercase_address(true);
rcpt.domain = rcpt.address_lcase.domain_part().into();
rcpt.address = new_address;
}
}
// Check for duplicates
let rcpt = self.data.rcpt_to.last().unwrap();
if self.data.rcpt_to.iter().filter(|r| r == &rcpt).count() > 1 {
trc::event!(
Smtp(SmtpEvent::RcptToDuplicate),
SpanId = self.data.session_id,
To = rcpt.address_lcase.clone(),
);
self.data.rcpt_to.pop();
self.data.rcpt_oks += 1;
return self.write(b"250 2.1.5 OK\r\n").await;
}
}
// Verify address
let rcpt = self.data.rcpt_to.last().unwrap();
let mut rcpt_members = None;
match self
.server
.rcpt_resolve(&rcpt.address_lcase, true, self.data.session_id)
.await
{
Ok(RcptResolution::Accept) => {}
Ok(RcptResolution::Rewrite(address)) => {
let orig_addr = self.data.rcpt_to.pop().unwrap();
let mut new_addr = SessionAddress::new(address);
if !self.data.rcpt_to.contains(&new_addr) {
new_addr.dsn_info = format!("rfc822;{}", orig_addr.address_lcase).into();
new_addr.flags = orig_addr.flags;
self.data.rcpt_to.push(new_addr);
} else {
trc::event!(
Smtp(SmtpEvent::RcptToDuplicate),
SpanId = self.data.session_id,
To = new_addr.address_lcase.clone(),
);
self.data.rcpt_oks += 1;
return self.write(b"250 2.1.5 OK\r\n").await;
}
}
Ok(RcptResolution::Expand(members)) => {
rcpt_members = Some(members);
}
Ok(RcptResolution::UnknownRecipient) => {
trc::event!(
Smtp(SmtpEvent::MailboxDoesNotExist),
SpanId = self.data.session_id,
To = rcpt.address_lcase.clone(),
);
let rcpt_to = self.data.rcpt_to.pop().unwrap().address_lcase;
return self
.rcpt_error(b"550 5.1.2 Mailbox does not exist.\r\n", rcpt_to)
.await;
}
Ok(RcptResolution::UnknownDomain) => {
if !self
.server
.eval_if(&rcpt_config.relay, self, self.data.session_id)
.await
.unwrap_or(false)
{
trc::event!(
Smtp(SmtpEvent::RelayNotAllowed),
SpanId = self.data.session_id,
To = rcpt.address_lcase.clone(),
);
let rcpt_to = self.data.rcpt_to.pop().unwrap().address_lcase;
return self
.rcpt_error(b"550 5.1.2 Relay not allowed.\r\n", rcpt_to)
.await;
}
}
Err(err) => {
trc::error!(
err.span_id(self.data.session_id)
.caused_by(trc::location!())
.details("Failed to verify address.")
);
self.data.rcpt_to.pop();
return self
.write(b"451 4.4.3 Unable to verify address at this time.\r\n")
.await;
}
}
if self.is_allowed().await {
// Greylist
if let Some(greylist_duration) = self
.server
.core
.spam
.grey_list_expiry
.filter(|_| self.data.authenticated_as.is_none())
{
let from_addr = self
.data
.mail_from
.as_ref()
.unwrap()
.address_lcase
.as_bytes();
let to_addr = self.data.rcpt_to.last().unwrap().address_lcase.as_bytes();
let mut key = Vec::with_capacity(from_addr.len() + to_addr.len() + 1);
key.push(KV_GREYLIST);
key.extend_from_slice(from_addr);
key.extend_from_slice(to_addr);
match self.server.in_memory_store().key_exists(key.clone()).await {
Ok(true) => (),
Ok(false) => {
match self
.server
.in_memory_store()
.key_set(KeyValue::new(key, vec![]).expires(greylist_duration))
.await
{
Ok(_) => {
let rcpt = self.data.rcpt_to.pop().unwrap();
trc::event!(
Smtp(SmtpEvent::RcptToGreylisted),
SpanId = self.data.session_id,
To = rcpt.address_lcase,
);
return self
.write(
concat!(
"452 4.2.2 Greylisted, please try ",
"again in a few moments.\r\n"
)
.as_bytes(),
)
.await;
}
Err(err) => {
trc::error!(
err.span_id(self.data.session_id)
.caused_by(trc::location!())
.details("Failed to set greylist.")
);
}
}
}
Err(err) => {
trc::error!(
err.span_id(self.data.session_id)
.caused_by(trc::location!())
.details("Failed to check greylist.")
);
}
}
}
trc::event!(
Smtp(SmtpEvent::RcptTo),
SpanId = self.data.session_id,
To = self.data.rcpt_to.last().unwrap().address_lcase.clone(),
);
} else {
trc::event!(
Smtp(SmtpEvent::RateLimitExceeded),
SpanId = self.data.session_id,
To = self.data.rcpt_to.last().unwrap().address_lcase.clone(),
);
self.data.rcpt_to.pop();
return self
.write(b"452 4.4.5 Rate limit exceeded, try again later.\r\n")
.await;
}
// Expand list
if let Some(members) = rcpt_members {
let list_addr = self.data.rcpt_to.pop().unwrap();
let orcpt = format!("rfc822;{}", list_addr.address_lcase);
for member in members.as_ref() {
let member_lcase = member.to_lowercase();
let is_local = match self
.server
.account_id_from_email(&member_lcase, false)
.await
{
Ok(account_id) => account_id.is_some(),
Err(err) => {
trc::error!(
err.span_id(self.data.session_id)
.caused_by(trc::location!())
.details("Failed to look up mailing list member.")
.ctx(trc::Key::To, member.to_string())
);
false
}
};
let address = if is_local {
member.to_string()
} else {
match self
.server
.rcpt_resolve(&member_lcase, false, self.data.session_id)
.await
{
Ok(RcptResolution::Rewrite(address)) => address,
Ok(_) => member.to_string(),
Err(err) => {
trc::error!(
err.span_id(self.data.session_id)
.caused_by(trc::location!())
.details("Failed to resolve mailing list member.")
.ctx(trc::Key::To, member.to_string())
);
member.to_string()
}
}
};
let mut member_addr = SessionAddress::new(address);
if !self.data.rcpt_to.contains(&member_addr)
&& member_addr.address_lcase != list_addr.address_lcase
{
member_addr.dsn_info = orcpt.clone().into();
member_addr.flags = list_addr.flags;
self.data.rcpt_to.push(member_addr);
}
}
}
self.data.rcpt_oks += 1;
self.write(b"250 2.1.5 OK\r\n").await
}
async fn rcpt_error(&mut self, response: &[u8], rcpt: String) -> Result<(), ()> {
tokio::time::sleep(self.params.rcpt_errors_wait).await;
self.data.rcpt_errors += 1;
let has_too_many_errors = self.data.rcpt_errors >= self.params.rcpt_errors_max;
match self
.server
.is_rcpt_fail2banned(self.data.remote_ip, &rcpt)
.await
{
Ok(true) => {
trc::event!(
Security(SecurityEvent::AbuseBan),
SpanId = self.data.session_id,
RemoteIp = self.data.remote_ip,
To = rcpt,
);
}
Ok(false) => {
if has_too_many_errors {
trc::event!(
Smtp(SmtpEvent::TooManyInvalidRcpt),
SpanId = self.data.session_id,
Limit = self.params.rcpt_errors_max,
To = rcpt,
);
}
}
Err(err) => {
trc::error!(
err.span_id(self.data.session_id)
.caused_by(trc::location!())
.details("Failed to check if IP should be banned.")
);
}
}
if !has_too_many_errors {
self.write(response).await
} else {
self.write(b"451 4.3.0 Too many errors, disconnecting.\r\n")
.await?;
Err(())
}
}
}
+639
View File
@@ -0,0 +1,639 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{
config::{server::ServerProtocol, smtp::session::Mechanism},
expr::{self, functions::ResolveVariable, *},
network::SessionStream,
};
use compact_str::ToCompactString;
use registry::schema::enums::ExpressionVariable;
use smtp_proto::{
request::receiver::{
BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, LineReceiver,
MAX_LINE_LENGTH,
},
*,
};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use trc::{NetworkEvent, SecurityEvent, SmtpEvent};
use crate::core::{Session, State};
use super::auth::SaslToken;
impl<T: SessionStream> Session<T> {
pub async fn ingest(&mut self, bytes: &[u8]) -> Result<bool, ()> {
let mut iter = bytes.iter();
let mut state = std::mem::replace(&mut self.state, State::None);
'outer: loop {
match &mut state {
State::Request(receiver) => loop {
match receiver.ingest(&mut iter) {
Ok(request) => match request {
Request::Rcpt { to } => {
self.handle_rcpt_to(to).await?;
}
Request::Mail { from } => {
self.handle_mail_from(from).await?;
}
Request::Ehlo { host } => {
if self.instance.protocol == ServerProtocol::Smtp {
self.handle_ehlo(host, true).await?;
} else {
trc::event!(
Smtp(SmtpEvent::LhloExpected),
SpanId = self.data.session_id,
);
self.write(b"500 5.5.1 Invalid command.\r\n").await?;
}
}
Request::Data => {
if let Some(response) = self.can_send_data().await {
self.write(response).await?;
} else {
self.write(b"354 Start mail input; end with <CRLF>.<CRLF>\r\n")
.await?;
self.data.message = Vec::with_capacity(1024);
state = State::Data(DataReceiver::new());
continue 'outer;
}
}
Request::Bdat {
chunk_size,
is_last,
} => {
state = if let Some(response) = self.can_send_data().await {
State::SkipData(
DummyDataReceiver::new_bdat(chunk_size),
response,
)
} else if chunk_size.saturating_add(self.data.message.len())
< self.params.max_message_size
{
if self.data.message.is_empty() {
self.data.message = Vec::with_capacity(chunk_size);
} else {
self.data.message.reserve(chunk_size);
}
State::Bdat(BdatReceiver::new(chunk_size, is_last))
} else {
trc::event!(
Smtp(SmtpEvent::MessageTooLarge),
SpanId = self.data.session_id,
Size = chunk_size.saturating_add(self.data.message.len()),
Limit = self.params.max_message_size,
);
State::SkipData(
DummyDataReceiver::new_bdat(chunk_size),
b"552 5.3.4 Message too big for system.\r\n",
)
};
continue 'outer;
}
Request::Auth {
mechanism,
initial_response,
} => {
let auth: u64 = self
.server
.eval_if::<Mechanism, _>(
&self.server.core.smtp.session.auth.mechanisms,
self,
self.data.session_id,
)
.await
.unwrap_or_default()
.into();
if auth == 0 {
trc::event!(
Smtp(SmtpEvent::AuthNotAllowed),
SpanId = self.data.session_id,
);
self.write(b"503 5.5.1 AUTH not allowed.\r\n").await?;
} else if let Some(authenticated_as) = self.authenticated_as() {
trc::event!(
Smtp(SmtpEvent::AlreadyAuthenticated),
SpanId = self.data.session_id,
AccountName = authenticated_as.to_string(),
);
self.write(b"503 5.5.1 Already authenticated.\r\n").await?;
} else if let Some(mut token) =
SaslToken::from_mechanism(mechanism & auth)
{
if self
.handle_sasl_response(
&mut token,
initial_response.as_bytes(),
)
.await?
{
state = State::Sasl(LineReceiver::new(token));
continue 'outer;
}
} else {
trc::event!(
Smtp(SmtpEvent::AuthMechanismNotSupported),
SpanId = self.data.session_id,
);
self.write(
b"554 5.7.8 Authentication mechanism not supported.\r\n",
)
.await?;
}
}
Request::Noop { .. } => {
trc::event!(Smtp(SmtpEvent::Noop), SpanId = self.data.session_id,);
self.write(b"250 2.0.0 OK\r\n").await?;
}
Request::Vrfy { value } => {
self.handle_vrfy(value).await?;
}
Request::Expn { value } => {
self.handle_expn(value).await?;
}
Request::StartTls => {
if !self.stream.is_tls() {
if self.instance.acceptor.is_tls() {
trc::event!(
Smtp(SmtpEvent::StartTls),
SpanId = self.data.session_id,
);
self.write(b"220 2.0.0 Ready to start TLS.\r\n").await?;
#[cfg(any(test, feature = "test_mode"))]
if self.data.helo_domain.contains("badtls") {
return Err(());
}
self.state = State::default();
self.reset_tls();
return Ok(false);
} else {
trc::event!(
Smtp(SmtpEvent::StartTlsUnavailable),
SpanId = self.data.session_id,
);
self.write(b"502 5.7.0 TLS not available.\r\n").await?;
}
} else {
trc::event!(
Smtp(SmtpEvent::StartTlsAlready),
SpanId = self.data.session_id,
);
self.write(b"504 5.7.4 Already in TLS mode.\r\n").await?;
}
}
Request::Rset => {
trc::event!(Smtp(SmtpEvent::Rset), SpanId = self.data.session_id,);
self.reset();
self.write(b"250 2.0.0 OK\r\n").await?;
}
Request::Quit => {
trc::event!(Smtp(SmtpEvent::Quit), SpanId = self.data.session_id,);
self.write(b"221 2.0.0 Bye.\r\n").await?;
return Err(());
}
Request::Help { .. } => {
trc::event!(Smtp(SmtpEvent::Help), SpanId = self.data.session_id,);
self.write(b"250 2.0.0 Help can be found at https://stalw.art\r\n")
.await?;
}
Request::Helo { host } => {
if self.instance.protocol == ServerProtocol::Smtp {
self.handle_ehlo(host, false).await?;
} else {
trc::event!(
Smtp(SmtpEvent::LhloExpected),
SpanId = self.data.session_id,
);
self.write(b"500 5.5.1 Invalid command: LHLO expected.\r\n")
.await?;
}
}
Request::Lhlo { host } => {
if self.instance.protocol == ServerProtocol::Lmtp {
self.handle_ehlo(host, true).await?;
} else {
trc::event!(
Smtp(SmtpEvent::EhloExpected),
SpanId = self.data.session_id,
);
self.write(b"502 5.5.1 Invalid command: EHLO expected.\r\n")
.await?;
}
}
cmd @ (Request::Etrn { .. }
| Request::Atrn { .. }
| Request::Burl { .. }) => {
trc::event!(
Smtp(SmtpEvent::CommandNotImplemented),
SpanId = self.data.session_id,
Details = format!("{cmd:?}"),
);
self.write(b"502 5.5.1 Command not implemented.\r\n")
.await?;
}
},
Err(err) => match err {
Error::NeedsMoreData { .. } => break 'outer,
Error::UnknownCommand | Error::InvalidResponse { .. } => {
// Check for port scanners
if !self.is_authenticated() {
match self
.server
.is_scanner_fail2banned(self.data.remote_ip)
.await
{
Ok(true) => {
trc::event!(
Security(SecurityEvent::ScanBan),
SpanId = self.data.session_id,
RemoteIp = self.data.remote_ip,
Reason = "Invalid SMTP command",
);
return Err(());
}
Ok(false) => {}
Err(err) => {
trc::error!(
err.span_id(self.data.session_id)
.details("Failed to check for fail2ban")
);
}
}
}
trc::event!(
Smtp(SmtpEvent::InvalidCommand),
SpanId = self.data.session_id,
);
self.write(b"500 5.5.1 Invalid command.\r\n").await?;
}
Error::InvalidSenderAddress => {
trc::event!(
Smtp(SmtpEvent::InvalidSenderAddress),
SpanId = self.data.session_id,
);
self.write(b"501 5.1.8 Bad sender's system address.\r\n")
.await?;
}
Error::InvalidRecipientAddress => {
trc::event!(
Smtp(SmtpEvent::InvalidRecipientAddress),
SpanId = self.data.session_id,
);
self.write(
b"501 5.1.3 Bad destination mailbox address syntax.\r\n",
)
.await?;
}
Error::SyntaxError { syntax } => {
trc::event!(
Smtp(SmtpEvent::SyntaxError),
SpanId = self.data.session_id,
Details = syntax
);
if !self.params.ehlo_reject_non_fqdn && syntax.starts_with("EHLO ")
{
self.handle_ehlo("null".into(), true).await?
} else {
self.write(
format!("501 5.5.2 Syntax error, expected: {syntax}\r\n")
.as_bytes(),
)
.await?;
}
}
Error::InvalidParameter { param } => {
trc::event!(
Smtp(SmtpEvent::InvalidParameter),
SpanId = self.data.session_id,
Details = param
);
self.write(
format!("501 5.5.4 Invalid parameter {param:?}.\r\n")
.as_bytes(),
)
.await?;
}
Error::UnsupportedParameter { param } => {
trc::event!(
Smtp(SmtpEvent::UnsupportedParameter),
SpanId = self.data.session_id,
Details = param.clone()
);
self.write(
format!("504 5.5.4 Unsupported parameter {param:?}.\r\n")
.as_bytes(),
)
.await?;
}
Error::ResponseTooLong => {
state = State::RequestTooLarge(DummyLineReceiver::default());
continue 'outer;
}
},
}
},
State::Data(receiver) => {
if self.data.message.len() + bytes.len() < self.params.max_message_size {
if receiver.ingest(&mut iter, &mut self.data.message) {
let message = self.queue_message().await;
let num_responses = if self.instance.protocol == ServerProtocol::Smtp {
1
} else {
self.data.rcpt_oks
};
if !message.is_empty() {
for _ in 0..num_responses {
self.write(message.as_ref()).await?;
}
self.reset();
state = State::default();
} else {
// Disconnect requested
return Err(());
}
} else {
break 'outer;
}
} else {
trc::event!(
Smtp(SmtpEvent::MessageTooLarge),
SpanId = self.data.session_id,
Size = self.data.message.len() + bytes.len(),
Limit = self.params.max_message_size,
);
state = State::SkipData(
DummyDataReceiver::new_data(receiver),
b"552 5.3.4 Message too big for system.\r\n",
);
}
}
State::Bdat(receiver) => {
if receiver.ingest(&mut iter, &mut self.data.message) {
if receiver.is_last {
let message = self.queue_message().await;
if !message.is_empty() {
let num_responses =
if self.instance.protocol == ServerProtocol::Smtp {
1
} else {
self.data.rcpt_oks
};
for _ in 0..num_responses {
self.write(message.as_ref()).await?;
}
self.reset();
} else {
// Disconnect requested
return Err(());
}
} else {
self.write(b"250 2.6.0 Chunk accepted.\r\n").await?;
}
state = State::default();
} else {
break 'outer;
}
}
State::Sasl(receiver) => {
if receiver.ingest(&mut iter) {
if receiver.buf.len() < MAX_LINE_LENGTH {
if self
.handle_sasl_response(&mut receiver.state, &receiver.buf)
.await?
{
receiver.buf.clear();
continue 'outer;
}
} else {
trc::event!(
Smtp(SmtpEvent::AuthExchangeTooLong),
SpanId = self.data.session_id,
Limit = MAX_LINE_LENGTH,
);
self.auth_error(
b"500 5.5.6 Authentication Exchange line is too long.\r\n",
)
.await?;
}
state = State::default();
} else {
break 'outer;
}
}
State::SkipData(receiver, response) => {
if receiver.ingest(&mut iter) {
self.data.message = Vec::with_capacity(0);
self.write(response).await?;
state = State::default();
} else {
break 'outer;
}
}
State::RequestTooLarge(receiver) => {
if receiver.ingest(&mut iter) {
trc::event!(
Smtp(SmtpEvent::RequestTooLarge),
SpanId = self.data.session_id,
);
self.write(b"554 5.3.4 Line is too long.\r\n").await?;
state = State::default();
} else {
break 'outer;
}
}
State::None | State::Accepted(_) => unreachable!(),
}
}
self.state = state;
Ok(true)
}
}
impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
pub fn reset(&mut self) {
self.data.mail_from = None;
self.data.spf_mail_from = None;
self.data.rcpt_to.clear();
self.data.message = Vec::with_capacity(0);
self.data.priority = 0;
self.data.delivery_by = 0;
self.data.future_release = 0;
self.data.rcpt_oks = 0;
}
pub fn reset_tls(&mut self) {
self.reset();
self.data.helo_domain.clear();
self.data.spf_ehlo = None;
self.data.authenticated_as = None;
}
#[inline(always)]
pub async fn write(&mut self, bytes: &[u8]) -> Result<(), ()> {
match self.stream.write_all(bytes).await {
Ok(_) => match self.stream.flush().await {
Ok(_) => {
trc::event!(
Smtp(SmtpEvent::RawOutput),
SpanId = self.data.session_id,
Size = bytes.len(),
Contents = trc::Value::from_maybe_string(bytes),
);
Ok(())
}
Err(err) => {
trc::event!(
Network(NetworkEvent::FlushError),
SpanId = self.data.session_id,
Reason = err.to_string(),
);
Err(())
}
},
Err(err) => {
trc::event!(
Network(NetworkEvent::WriteError),
SpanId = self.data.session_id,
Reason = err.to_string(),
);
Err(())
}
}
}
#[inline(always)]
pub async fn read(&mut self, bytes: &mut [u8]) -> Result<usize, ()> {
match self.stream.read(bytes).await {
Ok(len) => {
trc::event!(
Smtp(SmtpEvent::RawInput),
SpanId = self.data.session_id,
Size = len,
Contents =
String::from_utf8_lossy(bytes.get(0..len).unwrap_or_default()).into_owned(),
);
Ok(len)
}
Err(err) => {
trc::event!(
Network(NetworkEvent::ReadError),
SpanId = self.data.session_id,
Reason = err.to_string(),
);
Err(())
}
}
}
}
impl<T: SessionStream> ResolveVariable for Session<T> {
fn resolve_variable(&self, variable: ExpressionVariable) -> expr::Variable<'_> {
match variable {
ExpressionVariable::Rcpt => self
.data
.rcpt_to
.last()
.map(|r| r.address_lcase.as_str())
.unwrap_or_default()
.into(),
ExpressionVariable::RcptDomain => self
.data
.rcpt_to
.last()
.map(|r| r.domain.as_str())
.unwrap_or_default()
.into(),
ExpressionVariable::Recipients => self
.data
.rcpt_to
.iter()
.map(|r| Variable::from(r.address_lcase.as_str()))
.collect::<Vec<_>>()
.into(),
ExpressionVariable::Sender => self
.data
.mail_from
.as_ref()
.map(|m| m.address_lcase.as_str())
.unwrap_or_default()
.into(),
ExpressionVariable::SenderDomain => self
.data
.mail_from
.as_ref()
.map(|m| m.domain.as_str())
.unwrap_or_default()
.into(),
ExpressionVariable::HeloDomain => self.data.helo_domain.as_str().into(),
ExpressionVariable::AuthenticatedAs => {
self.authenticated_as().unwrap_or_default().into()
}
ExpressionVariable::Listener => self.instance.id.as_str().into(),
ExpressionVariable::RemoteIp => self.data.remote_ip_str.as_str().into(),
ExpressionVariable::RemotePort => self.data.remote_port.into(),
ExpressionVariable::LocalIp => self.data.local_ip_str.as_str().into(),
ExpressionVariable::LocalPort => self.data.local_port.into(),
ExpressionVariable::IsTls => self.stream.is_tls().into(),
ExpressionVariable::Priority => self.data.priority.to_compact_string().into(),
ExpressionVariable::Protocol => self.instance.protocol.as_str().into(),
ExpressionVariable::Asn => self
.data
.asn_geo_data
.asn
.as_ref()
.map(|a| a.id)
.unwrap_or_default()
.into(),
ExpressionVariable::Country => self
.data
.asn_geo_data
.country
.as_ref()
.map(|c| c.as_str())
.unwrap_or_default()
.into(),
_ => expr::Variable::default(),
}
}
fn resolve_global(&self, _: &str) -> Variable<'_> {
Variable::Integer(0)
}
}
+107
View File
@@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::core::Session;
use common::{config::mailstore::spamfilter::SpamFilterAction, network::SessionStream};
use mail_auth::{ArcOutput, DkimOutput, DmarcResult, dkim2::Dkim2Output, dmarc::Policy};
use mail_parser::Message;
use spam_filter::{
SpamFilterInput,
analysis::{
init::SpamFilterInit,
score::{SpamFilterAnalyzeScore, SpamFilterScore},
},
};
impl<T: SessionStream> Session<T> {
pub async fn spam_classify<'x>(
&'x self,
message: &'x Message<'x>,
dkim_result: &'x [DkimOutput<'x>],
dkim2_result: Option<&'x Dkim2Output<'x>>,
arc_result: Option<&'x ArcOutput<'x>>,
dmarc_result: Option<&'x DmarcResult>,
dmarc_policy: Option<&'x Policy>,
) -> SpamFilterAction<SpamFilterScore> {
let server = &self.server;
let mut ctx = server.spam_filter_init(self.build_spam_input(
message,
dkim_result,
dkim2_result,
arc_result,
dmarc_result,
dmarc_policy,
));
if !self.is_authenticated() {
// Spam classification
server.spam_filter_classify(&mut ctx).await
} else {
// Do not classify authenticated sessions
SpamFilterAction::Disabled
}
}
pub fn build_spam_input<'x>(
&'x self,
message: &'x Message<'x>,
dkim_result: &'x [DkimOutput<'x>],
dkim2_result: Option<&'x Dkim2Output<'x>>,
arc_result: Option<&'x ArcOutput>,
dmarc_result: Option<&'x DmarcResult>,
dmarc_policy: Option<&'x Policy>,
) -> SpamFilterInput<'x> {
SpamFilterInput {
message,
span_id: self.data.session_id,
arc_result,
spf_ehlo_result: self.data.spf_ehlo.as_ref(),
spf_mail_from_result: self.data.spf_mail_from.as_ref(),
dkim_result,
dkim2_result,
dmarc_result,
dmarc_policy,
iprev_result: self.data.iprev.as_ref(),
remote_ip: self.data.remote_ip,
ehlo_domain: self.data.helo_domain.as_str().into(),
authenticated_as: self.data.authenticated_as.as_ref().map(|a| a.name()),
asn: self.data.asn_geo_data.asn.as_ref().map(|a| a.id),
country: self.data.asn_geo_data.country.as_ref().map(|c| c.as_str()),
is_tls: self.stream.is_tls(),
env_from: self
.data
.mail_from
.as_ref()
.map(|m| m.address_lcase.as_str())
.unwrap_or_default(),
env_from_flags: self
.data
.mail_from
.as_ref()
.map(|m| m.flags)
.unwrap_or_default(),
env_rcpt_rewritten_to: self
.data
.rcpt_to
.iter()
.map(|r| r.address_lcase.as_str())
.collect(),
env_rcpt_orig_to: self
.data
.rcpt_to
.iter()
.map(|r| {
r.dsn_info
.as_deref()
.and_then(|info| info.strip_prefix("rfc822;"))
.unwrap_or(r.address_lcase.as_str())
})
.collect(),
is_test: false,
is_train: false,
}
}
}
+267
View File
@@ -0,0 +1,267 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
core::{Session, SessionData, SessionParameters, SmtpSessionManager, State},
scripts::ScriptResult,
};
use common::{
BuildServer,
config::smtp::session::Stage,
network::{self, SessionManager, SessionStream},
};
use std::time::Instant;
use tokio_rustls::server::TlsStream;
use trc::{SecurityEvent, SmtpEvent};
impl SessionManager for SmtpSessionManager {
async fn handle<T: SessionStream>(self, session: network::SessionData<T>) {
// Build server and create session
let server = self.inner.build_server();
let _in_flight = session.in_flight;
let mut session = Session {
data: SessionData::new(
session.local_ip,
session.local_port,
session.remote_ip,
session.remote_port,
server.lookup_asn_country(session.remote_ip).await,
session.session_id,
),
hostname: "".into(),
server,
instance: session.instance,
state: State::default(),
stream: session.stream,
params: SessionParameters::default(),
};
// Enforce throttle
if session.is_allowed().await
&& session.init_conn().await
&& session.handle_conn().await
&& session.instance.acceptor.is_tls()
&& let Ok(mut session) = session.into_tls().await
{
session.handle_conn().await;
}
}
#[allow(clippy::manual_async_fn)]
fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send {
async {
let _ = self
.inner
.ipc
.queue_tx
.send(common::ipc::QueueEvent::Stop)
.await;
let _ = self
.inner
.ipc
.report_tx
.send(common::ipc::ReportingEvent::Stop)
.await;
}
}
}
impl<T: SessionStream> Session<T> {
pub async fn init_conn(&mut self) -> bool {
self.eval_session_params().await;
let config = &self.server.core.smtp.session.connect;
// Sieve filtering
if let Some((script, script_id)) = self
.server
.eval_if::<String, _>(&config.script, self, self.data.session_id)
.await
.and_then(|name| {
self.server
.get_trusted_sieve_script(&name, self.data.session_id)
.map(|s| (s, name))
})
&& let ScriptResult::Reject(message) = self
.run_script(
script_id,
script.clone(),
self.build_script_parameters("connect"),
)
.await
{
let _ = self.write(message.as_bytes()).await;
return false;
}
// Milter filtering
if let Err(message) = self.run_milters(Stage::Connect, None, None).await {
let _ = self.write(message.message.as_bytes()).await;
return false;
}
// MTAHook filtering
if let Err(message) = self.run_mta_hooks(Stage::Connect, None, None).await {
let _ = self.write(message.message.as_bytes()).await;
return false;
}
// Obtain hostname
self.hostname = self
.server
.eval_if::<String, _>(&config.hostname, self, self.data.session_id)
.await
.unwrap_or_default();
if self.hostname.is_empty() {
trc::event!(
Smtp(SmtpEvent::MissingLocalHostname),
SpanId = self.data.session_id,
);
self.hostname = "localhost".into();
}
// Obtain greeting
let greeting = self
.server
.eval_if::<String, _>(&config.greeting, self, self.data.session_id)
.await
.filter(|g| !g.is_empty())
.map(|g| format!("220 {}\r\n", g))
.unwrap_or_else(|| "220 Stalwart ESMTP at your service.\r\n".to_string());
if self.write(greeting.as_bytes()).await.is_err() {
return false;
}
true
}
pub async fn handle_conn(&mut self) -> bool {
let mut buf = vec![0; 8192];
let mut shutdown_rx = self.instance.shutdown_rx.clone();
loop {
tokio::select! {
result = tokio::time::timeout(
self.params.timeout,
self.read(&mut buf)) => {
match result {
Ok(Ok(bytes_read)) => {
if bytes_read > 0 {
if Instant::now() < self.data.valid_until && bytes_read <= self.data.bytes_left {
self.data.bytes_left -= bytes_read;
match Box::pin(self.ingest(&buf[..bytes_read])).await {
Ok(true) => (),
Ok(false) => {
return true;
}
Err(_) => {
break;
}
}
} else if bytes_read > self.data.bytes_left {
self
.write(format!("452 4.7.28 {} Session exceeded transfer quota.\r\n", self.hostname).as_bytes())
.await
.ok();
trc::event!(
Smtp(SmtpEvent::TransferLimitExceeded),
SpanId = self.data.session_id,
);
break;
} else {
self
.write(format!("421 4.3.2 {} Session open for too long.\r\n", self.hostname).as_bytes())
.await
.ok();
match self.server.is_loiter_fail2banned(self.data.remote_ip)
.await
{
Ok(true) => {
trc::event!(
Security(SecurityEvent::LoiterBan),
SpanId = self.data.session_id,
RemoteIp = self.data.remote_ip,
);
}
Ok(false) => {
trc::event!(
Smtp(SmtpEvent::TimeLimitExceeded),
SpanId = self.data.session_id,
);
}
Err(err) => {
trc::error!(err
.span_id(self.data.session_id)
.caused_by(trc::location!())
.details("Failed to check if IP should be banned."));
}
}
break;
}
} else {
trc::event!(
Network(trc::NetworkEvent::Closed),
SpanId = self.data.session_id,
CausedBy = trc::location!()
);
break;
}
}
Ok(Err(_)) => {
break;
}
Err(_) => {
trc::event!(
Network(trc::NetworkEvent::Timeout),
SpanId = self.data.session_id,
CausedBy = trc::location!()
);
self
.write(format!("221 2.0.0 {} Disconnecting inactive client.\r\n", self.hostname).as_bytes())
.await
.ok();
break;
}
}
},
_ = shutdown_rx.changed() => {
trc::event!(
Network(trc::NetworkEvent::Closed),
SpanId = self.data.session_id,
Reason = "Server shutting down",
CausedBy = trc::location!()
);
self.write(format!("421 4.3.0 {} Server shutting down.\r\n", self.hostname).as_bytes()).await.ok();
break;
}
};
}
false
}
pub async fn into_tls(self) -> Result<Session<TlsStream<T>>, ()> {
Ok(Session {
hostname: self.hostname,
stream: self
.instance
.tls_accept(self.stream, self.data.session_id)
.await?,
state: self.state,
data: self.data,
instance: self.instance,
server: self.server,
params: self.params,
})
}
}
+130
View File
@@ -0,0 +1,130 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::core::Session;
use common::network::{RcptResolution, SessionStream};
use std::{borrow::Cow, fmt::Write};
use trc::SmtpEvent;
use utils::DomainPart;
impl<T: SessionStream> Session<T> {
pub async fn handle_vrfy(&mut self, address: Cow<'_, str>) -> Result<(), ()> {
if self.params.can_vrfy {
match self
.server
.rcpt_resolve(
&address.to_lowercase_address(true),
true,
self.data.session_id,
)
.await
{
Ok(RcptResolution::Accept | RcptResolution::Rewrite(_)) => {
trc::event!(
Smtp(SmtpEvent::Vrfy),
SpanId = self.data.session_id,
To = address.as_ref().to_string(),
);
self.write(format!("250 {}\r\n", address.as_ref()).as_bytes())
.await
}
Ok(
RcptResolution::UnknownRecipient
| RcptResolution::UnknownDomain
| RcptResolution::Expand(_),
) => {
trc::event!(
Smtp(SmtpEvent::VrfyNotFound),
SpanId = self.data.session_id,
To = address.as_ref().to_string(),
);
self.write(b"550 5.1.2 Address not found.\r\n").await
}
Err(err) => {
trc::error!(
err.span_id(self.data.session_id)
.caused_by(trc::location!())
.details("Failed to verify address.")
);
self.write(b"252 2.4.3 Unable to verify address at this time.\r\n")
.await
}
}
} else {
trc::event!(
Smtp(SmtpEvent::VrfyDisabled),
SpanId = self.data.session_id,
To = address.as_ref().to_string(),
);
self.write(b"252 2.5.1 VRFY is disabled.\r\n").await
}
}
pub async fn handle_expn(&mut self, address: Cow<'_, str>) -> Result<(), ()> {
if self.params.can_expn {
match self
.server
.rcpt_resolve(
&address.to_lowercase_address(true),
true,
self.data.session_id,
)
.await
{
Ok(RcptResolution::Expand(addresses)) => {
let mut result = String::with_capacity(32);
for (pos, value) in addresses.iter().enumerate() {
let _ = write!(
result,
"250{}{}\r\n",
if pos == addresses.len() - 1 { " " } else { "-" },
value
);
}
trc::event!(
Smtp(SmtpEvent::Expn),
SpanId = self.data.session_id,
To = address.as_ref().to_string(),
);
self.write(result.as_bytes()).await
}
Ok(_) => {
trc::event!(
Smtp(SmtpEvent::ExpnNotFound),
SpanId = self.data.session_id,
To = address.as_ref().to_string(),
);
self.write(b"550 5.1.2 Mailing list not found.\r\n").await
}
Err(err) => {
trc::error!(
err.span_id(self.data.session_id)
.caused_by(trc::location!())
.details("Failed to verify address.")
);
self.write(b"252 2.4.3 Unable to expand mailing list at this time.\r\n")
.await
}
}
} else {
trc::event!(
Smtp(SmtpEvent::ExpnDisabled),
SpanId = self.data.session_id,
To = address.as_ref().to_string(),
);
self.write(b"252 2.5.1 EXPN is disabled.\r\n").await
}
}
}
+49
View File
@@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
#![warn(clippy::large_futures)]
use common::{
Inner,
manager::boot::{BootManager, IpcReceivers},
};
use queue::manager::SpawnQueue;
use reporting::scheduler::SpawnReport;
use std::sync::Arc;
pub mod core;
pub mod inbound;
pub mod outbound;
pub mod queue;
pub mod reporting;
pub mod scripts;
pub trait StartQueueManager {
fn start_queue_manager(&mut self);
}
pub trait SpawnQueueManager {
fn spawn_queue_manager(&mut self, inner: Arc<Inner>);
}
impl StartQueueManager for BootManager {
fn start_queue_manager(&mut self) {
self.ipc_rxs.spawn_queue_manager(self.inner.clone());
}
}
impl SpawnQueueManager for IpcReceivers {
fn spawn_queue_manager(&mut self, inner: Arc<Inner>) {
let core = inner.shared_core.load();
if !core.storage.registry.is_recovery_mode() && core.network.roles.outbound_mta {
// Spawn queue manager
self.queue_rx.take().unwrap().spawn(inner.clone());
// Spawn report manager
self.report_rx.take().unwrap().spawn(inner);
}
}
}
+773
View File
@@ -0,0 +1,773 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::session::SessionParams;
use crate::{
outbound::error::{AssertReply, ClientError, ClientResult},
queue::{Error, ErrorDetails, HostResponse, MessageWrapper, Status},
};
use base64::{Engine, engine::general_purpose};
use directory::Credentials;
use rustls::ClientConnection;
use rustls_pki_types::ServerName;
use smtp_proto::{
AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2, EXT_START_TLS, EhloResponse, Response,
response::{
generate::BitToString,
parser::{MAX_RESPONSE_LENGTH, ResponseReceiver},
},
};
use std::{
net::{IpAddr, SocketAddr},
time::Duration,
};
use tokio::{
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
net::{TcpSocket, TcpStream},
};
use tokio_rustls::{TlsConnector, client::TlsStream};
use trc::DeliveryEvent;
pub struct SmtpClient<T: AsyncRead + AsyncWrite> {
pub stream: T,
pub timeout: Duration,
pub session_id: u64,
}
impl<T: AsyncRead + AsyncWrite + Unpin> SmtpClient<T> {
pub async fn authenticate(
&mut self,
credentials: &Credentials,
capabilities: impl AsRef<EhloResponse<String>>,
) -> ClientResult<&mut Self> {
let capabilities = capabilities.as_ref();
let mut available_mechanisms = match &credentials {
Credentials::Basic { .. } => AUTH_LOGIN | AUTH_PLAIN,
Credentials::Bearer { .. } => AUTH_OAUTHBEARER | AUTH_XOAUTH2,
} & capabilities.auth_mechanisms;
// Try authenticating from most secure to least secure
let mut has_err = None;
let mut has_failed = false;
while available_mechanisms != 0 && !has_failed {
let mechanism = 1 << ((63 - available_mechanisms.leading_zeros()) as u64);
available_mechanisms ^= mechanism;
match self.auth(mechanism, credentials).await {
Ok(_) => {
return Ok(self);
}
Err(err) => match err {
ClientError::UnexpectedReply(reply) => {
has_failed = reply.code() == 535;
has_err = reply.into();
}
ClientError::UnsupportedAuthMechanism => (),
_ => return Err(err),
},
}
}
if let Some(has_err) = has_err {
Err(ClientError::AuthenticationFailed(has_err))
} else {
Err(ClientError::UnsupportedAuthMechanism)
}
}
pub(crate) async fn auth(
&mut self,
mechanism: u64,
credentials: &Credentials,
) -> ClientResult<()> {
let mut reply = if (mechanism & (AUTH_PLAIN | AUTH_XOAUTH2 | AUTH_OAUTHBEARER)) != 0 {
self.cmd(
format!(
"AUTH {} {}\r\n",
mechanism.to_mechanism(),
encode_credentials(credentials, mechanism, "")?,
)
.as_bytes(),
)
.await?
} else {
self.cmd(format!("AUTH {}\r\n", mechanism.to_mechanism()).as_bytes())
.await?
};
for _ in 0..3 {
match reply.code() {
334 => {
reply = self
.cmd(
format!(
"{}\r\n",
encode_credentials(credentials, mechanism, reply.message())?
)
.as_bytes(),
)
.await?;
}
235 => {
return Ok(());
}
_ => {
return Err(ClientError::UnexpectedReply(Box::new(reply)));
}
}
}
Err(ClientError::UnexpectedReply(Box::new(reply)))
}
pub async fn read_greeting(
&mut self,
hostname: &str,
) -> Result<(), Status<HostResponse<Box<str>>, ErrorDetails>> {
tokio::time::timeout(self.timeout, self.read())
.await
.map_err(|_| Status::timeout(hostname, "reading greeting"))?
.and_then(|r| r.assert_code(220))
.map_err(|err| Status::from_smtp_error(hostname, "", err))
}
pub async fn read_smtp_data_response(
&mut self,
hostname: &str,
bdat_cmd: &Option<String>,
) -> Result<Response<String>, Status<HostResponse<Box<str>>, ErrorDetails>> {
tokio::time::timeout(self.timeout, self.read())
.await
.map_err(|_| Status::timeout(hostname, "reading SMTP DATA response"))?
.map_err(|err| {
Status::from_smtp_error(hostname, bdat_cmd.as_deref().unwrap_or("DATA"), err)
})
}
pub async fn read_lmtp_data_response(
&mut self,
hostname: &str,
num_responses: usize,
) -> Result<Vec<Response<Box<str>>>, Status<HostResponse<Box<str>>, ErrorDetails>> {
tokio::time::timeout(self.timeout, async { self.read_many(num_responses).await })
.await
.map_err(|_| Status::timeout(hostname, "reading LMTP DATA responses"))?
.map_err(|err| Status::from_smtp_error(hostname, "", err))
}
pub async fn write_chunks(&mut self, chunks: &[&[u8]]) -> Result<(), ClientError> {
for chunk in chunks {
self.stream
.write_all(chunk)
.await
.map_err(ClientError::from)?;
}
self.stream.flush().await.map_err(ClientError::from)
}
pub async fn send_message(
&mut self,
message: &MessageWrapper,
rcpt_headers: Option<&[u8]>,
bdat_cmd: &mut Option<String>,
params: &SessionParams<'_>,
) -> Result<(), Status<HostResponse<Box<str>>, ErrorDetails>> {
match params
.server
.blob_store()
.get_blob(message.message.blob_hash.as_slice(), 0..usize::MAX)
.await
{
Ok(Some(raw_message)) => {
tokio::time::timeout(params.conn_strategy.timeout_data, async {
if let Some(bdat_cmd) = bdat_cmd {
*bdat_cmd = format!(
"BDAT {} LAST\r\n",
raw_message.len() + rcpt_headers.map(|h| h.len()).unwrap_or(0)
);
trc::event!(
Delivery(DeliveryEvent::RawOutput),
SpanId = self.session_id,
Contents = bdat_cmd.clone(),
Size = bdat_cmd.len()
);
let chunks = if let Some(rcpt_headers) = rcpt_headers {
&[bdat_cmd.as_bytes(), rcpt_headers, &raw_message][..]
} else {
&[bdat_cmd.as_bytes(), &raw_message][..]
};
self.write_chunks(chunks).await
} else {
trc::event!(
Delivery(DeliveryEvent::RawOutput),
SpanId = self.session_id,
Contents = "DATA\r\n",
Size = 6
);
self.write_chunks(&[b"DATA\r\n"]).await?;
self.read().await?.assert_code(354)?;
if let Some(rcpt_headers) = rcpt_headers
&& let Err(err) = self.write_chunks(&[rcpt_headers]).await
{
Err(err)
} else {
self.write_message(&raw_message)
.await
.map_err(ClientError::from)
}
}
})
.await
.map_err(|_| Status::timeout(params.hostname, "sending message"))?
.map_err(|err| {
Status::from_smtp_error(
params.hostname,
bdat_cmd.as_deref().unwrap_or("DATA"),
err,
)
})
}
Ok(None) => {
trc::event!(
Queue(trc::QueueEvent::BlobNotFound),
SpanId = message.span_id,
BlobId = message.message.blob_hash.to_hex(),
CausedBy = trc::location!()
);
Err(Status::TemporaryFailure(ErrorDetails {
entity: "localhost".into(),
details: Error::Io("Queue system error.".into()),
}))
}
Err(err) => {
trc::error!(
err.span_id(message.span_id)
.details("Failed to fetch blobId")
.caused_by(trc::location!())
);
Err(Status::TemporaryFailure(ErrorDetails {
entity: "localhost".into(),
details: Error::Io("Queue system error.".into()),
}))
}
}
}
pub async fn say_helo(
&mut self,
params: &SessionParams<'_>,
) -> Result<EhloResponse<String>, Status<HostResponse<Box<str>>, ErrorDetails>> {
let cmd = if params.is_smtp {
format!("EHLO {}\r\n", params.local_hostname)
} else {
format!("LHLO {}\r\n", params.local_hostname)
};
trc::event!(
Delivery(DeliveryEvent::RawOutput),
SpanId = self.session_id,
Contents = cmd.clone(),
Size = cmd.len()
);
tokio::time::timeout(params.conn_strategy.timeout_ehlo, async {
self.stream.write_all(cmd.as_bytes()).await?;
self.stream.flush().await?;
self.read_ehlo().await
})
.await
.map_err(|_| Status::timeout(params.hostname, "reading EHLO response"))?
.map_err(|err| Status::from_smtp_error(params.hostname, &cmd, err))
}
pub async fn quit(mut self: SmtpClient<T>) {
trc::event!(
Delivery(DeliveryEvent::RawOutput),
SpanId = self.session_id,
Contents = "QUIT\r\n",
Size = 6
);
let _ = tokio::time::timeout(Duration::from_secs(10), async {
if self.stream.write_all(b"QUIT\r\n").await.is_ok() && self.stream.flush().await.is_ok()
{
let mut buf = [0u8; 128];
let _ = self.stream.read(&mut buf).await;
}
})
.await;
}
pub async fn read_ehlo(&mut self) -> ClientResult<EhloResponse<String>> {
let mut buf = vec![0u8; 8192];
let mut buf_concat = Vec::with_capacity(0);
loop {
let br = self.stream.read(&mut buf).await?;
if br == 0 {
return Err(ClientError::UnparseableReply);
}
trc::event!(
Delivery(DeliveryEvent::RawInput),
SpanId = self.session_id,
Contents = trc::Value::from_maybe_string(&buf[..br]),
Size = br,
);
let mut iter = if buf_concat.is_empty() {
buf[..br].iter()
} else if br + buf_concat.len() < MAX_RESPONSE_LENGTH {
buf_concat.extend_from_slice(&buf[..br]);
buf_concat.iter()
} else {
return Err(ClientError::UnparseableReply);
};
match EhloResponse::parse(&mut iter) {
Ok(reply) => return Ok(reply),
Err(err) => match err {
smtp_proto::Error::NeedsMoreData { .. } => {
if buf_concat.is_empty() {
buf_concat = buf[..br].to_vec();
}
}
smtp_proto::Error::InvalidResponse { code } => {
match ResponseReceiver::from_code(code).parse(&mut iter) {
Ok(response) => {
return Err(ClientError::UnexpectedReply(Box::new(response)));
}
Err(smtp_proto::Error::NeedsMoreData { .. }) => {
if buf_concat.is_empty() {
buf_concat = buf[..br].to_vec();
}
}
Err(_) => return Err(ClientError::UnparseableReply),
}
}
_ => {
return Err(ClientError::UnparseableReply);
}
},
}
}
}
pub async fn read(&mut self) -> ClientResult<Response<String>> {
let mut buf = vec![0u8; 8192];
let mut parser = ResponseReceiver::default();
loop {
let br = self.stream.read(&mut buf).await?;
if br > 0 {
trc::event!(
Delivery(DeliveryEvent::RawInput),
SpanId = self.session_id,
Contents = trc::Value::from_maybe_string(&buf[..br]),
Size = br
);
match parser.parse(&mut buf[..br].iter()) {
Ok(reply) => return Ok(reply),
Err(err) => match err {
smtp_proto::Error::NeedsMoreData { .. } => (),
_ => {
return Err(ClientError::UnparseableReply);
}
},
}
} else {
return Err(ClientError::UnparseableReply);
}
}
}
pub async fn read_many(&mut self, num: usize) -> ClientResult<Vec<Response<Box<str>>>> {
let mut buf = vec![0u8; 1024];
let mut response = Vec::with_capacity(num);
let mut parser = ResponseReceiver::default();
'outer: loop {
let br = self.stream.read(&mut buf).await?;
if br > 0 {
let mut iter = buf[..br].iter();
trc::event!(
Delivery(DeliveryEvent::RawInput),
SpanId = self.session_id,
Contents = trc::Value::from_maybe_string(&buf[..br]),
Size = br
);
loop {
match parser.parse(&mut iter) {
Ok(reply) => {
response.push(reply.into_box());
if response.len() != num {
parser.reset();
} else {
break 'outer;
}
}
Err(err) => match err {
smtp_proto::Error::NeedsMoreData { .. } => break,
_ => {
return Err(ClientError::UnparseableReply);
}
},
}
}
} else {
return Err(ClientError::UnparseableReply);
}
}
Ok(response)
}
/// Sends a command to the SMTP server and waits for a reply.
pub async fn cmd(&mut self, cmd: impl AsRef<[u8]>) -> ClientResult<Response<String>> {
tokio::time::timeout(self.timeout, async {
let cmd = cmd.as_ref();
trc::event!(
Delivery(DeliveryEvent::RawOutput),
SpanId = self.session_id,
Contents = trc::Value::from_maybe_string(cmd),
Size = cmd.len()
);
self.stream.write_all(cmd).await?;
self.stream.flush().await?;
self.read().await
})
.await
.map_err(|_| ClientError::Timeout)?
}
pub async fn write_message(&mut self, message: &[u8]) -> tokio::io::Result<()> {
// Transparency procedure
let mut is_cr_or_lf = false;
// As per RFC 5322bis, section 2.3:
// CR and LF MUST only occur together as CRLF; they MUST NOT appear
// independently in the body.
// For this reason, we apply the transparency procedure when there is
// a CR or LF followed by a dot.
trc::event!(
Delivery(DeliveryEvent::RawOutput),
SpanId = self.session_id,
Contents = "[message]",
Size = message.len() + 5
);
let mut last_pos = 0;
for (pos, byte) in message.iter().enumerate() {
if *byte == b'.' && is_cr_or_lf {
if let Some(bytes) = message.get(last_pos..pos) {
self.stream.write_all(bytes).await?;
self.stream.write_all(b".").await?;
last_pos = pos;
}
is_cr_or_lf = false;
} else {
is_cr_or_lf = *byte == b'\n' || *byte == b'\r';
}
}
if let Some(bytes) = message.get(last_pos..) {
self.stream.write_all(bytes).await?;
}
self.stream.write_all("\r\n.\r\n".as_bytes()).await?;
self.stream.flush().await
}
}
impl SmtpClient<TcpStream> {
/// Upgrade the connection to TLS.
pub async fn start_tls(
mut self,
tls_connector: &TlsConnector,
hostname: &str,
) -> ClientResult<SmtpClient<TlsStream<TcpStream>>> {
// Send STARTTLS command
self.cmd(b"STARTTLS\r\n")
.await?
.assert_positive_completion()?;
self.into_tls(tls_connector, hostname).await
}
pub async fn into_tls(
self,
tls_connector: &TlsConnector,
hostname: &str,
) -> ClientResult<SmtpClient<TlsStream<TcpStream>>> {
tokio::time::timeout(self.timeout, async {
Ok(SmtpClient {
stream: tls_connector
.connect(
ServerName::try_from(hostname)
.map_err(|_| ClientError::InvalidTLSName)?
.to_owned(),
self.stream,
)
.await
.map_err(|err| {
let kind = err.kind();
if let Some(inner) = err.into_inner() {
match inner.downcast::<rustls::Error>() {
Ok(error) => ClientError::Tls(error),
Err(error) => ClientError::Io(std::io::Error::new(kind, error)),
}
} else {
ClientError::Io(std::io::Error::new(kind, "Unspecified"))
}
})?,
timeout: self.timeout,
session_id: self.session_id,
})
})
.await
.map_err(|_| ClientError::Timeout)?
}
}
impl SmtpClient<TcpStream> {
/// Connects to a remote host address
pub async fn connect(
remote_addr: SocketAddr,
timeout: Duration,
session_id: u64,
) -> ClientResult<Self> {
tokio::time::timeout(timeout, async {
Ok(SmtpClient {
stream: TcpStream::connect(remote_addr).await?,
timeout,
session_id,
})
})
.await
.map_err(|_| ClientError::Timeout)?
}
/// Connects to a remote host address using the provided local IP
pub async fn connect_using(
local_ip: IpAddr,
remote_addr: SocketAddr,
timeout: Duration,
session_id: u64,
) -> ClientResult<Self> {
tokio::time::timeout(timeout, async {
let socket = if local_ip.is_ipv4() {
TcpSocket::new_v4()?
} else {
TcpSocket::new_v6()?
};
socket.bind(SocketAddr::new(local_ip, 0))?;
Ok(SmtpClient {
stream: socket.connect(remote_addr).await?,
timeout,
session_id,
})
})
.await
.map_err(|_| ClientError::Timeout)?
}
pub async fn try_start_tls(
mut self,
tls_connector: &TlsConnector,
hostname: &str,
capabilities: &EhloResponse<String>,
) -> StartTlsResult {
if capabilities.has_capability(EXT_START_TLS) {
match self.cmd("STARTTLS\r\n").await {
Ok(response) => {
if response.code() == 220 {
match self.into_tls(tls_connector, hostname).await {
Ok(smtp_client) => StartTlsResult::Success { smtp_client },
Err(error) => StartTlsResult::Error { error },
}
} else {
StartTlsResult::Unavailable {
response: response.into_box().into(),
smtp_client: self,
}
}
}
Err(error) => StartTlsResult::Error { error },
}
} else {
StartTlsResult::Unavailable {
smtp_client: self,
response: None,
}
}
}
}
fn encode_credentials(
credentials: &Credentials,
mechanism: u64,
challenge: &str,
) -> ClientResult<String> {
Ok(general_purpose::STANDARD.encode(
match (mechanism, credentials) {
(
AUTH_PLAIN,
Credentials::Basic {
username, secret, ..
},
) => {
format!("\u{0}{}\u{0}{}", username, secret)
}
(
AUTH_LOGIN,
Credentials::Basic {
username, secret, ..
},
) => {
let challenge = general_purpose::STANDARD.decode(challenge)?;
if b"user name"
.eq_ignore_ascii_case(challenge.get(0..9).ok_or(ClientError::InvalidChallenge)?)
|| b"username".eq_ignore_ascii_case(
// Because Google makes its own standards
challenge.get(0..8).ok_or(ClientError::InvalidChallenge)?,
)
{
&username
} else if b"password"
.eq_ignore_ascii_case(challenge.get(0..8).ok_or(ClientError::InvalidChallenge)?)
{
&secret
} else {
return Err(ClientError::InvalidChallenge);
}
.to_string()
}
(AUTH_XOAUTH2, Credentials::Bearer { token, username }) => format!(
"user={}\x01auth=Bearer {}\x01\x01",
username.as_deref().unwrap_or_default(),
token
),
(AUTH_OAUTHBEARER, Credentials::Bearer { token, .. }) => token.to_string(),
_ => return Err(ClientError::UnsupportedAuthMechanism),
}
.as_bytes(),
))
}
impl SmtpClient<TlsStream<TcpStream>> {
pub fn tls_connection(&self) -> &ClientConnection {
self.stream.get_ref().1
}
}
#[allow(clippy::large_enum_variant)]
pub enum StartTlsResult {
Success {
smtp_client: SmtpClient<TlsStream<TcpStream>>,
},
Error {
error: ClientError,
},
Unavailable {
response: Option<Response<Box<str>>>,
smtp_client: SmtpClient<TcpStream>,
},
}
pub(crate) trait BoxResponse {
fn into_box(self) -> Response<Box<str>>;
}
impl BoxResponse for Response<String> {
fn into_box(self) -> Response<Box<str>> {
Response {
code: self.code,
esc: self.esc,
message: self.message.into_boxed_str(),
}
}
}
pub(crate) fn from_mail_send_error(error: &ClientError) -> trc::Error {
let event = trc::EventType::Smtp(trc::SmtpEvent::Error).into_err();
match error {
ClientError::Io(err) => event.details("I/O Error").reason(err),
ClientError::Tls(err) => event.details("TLS Error").reason(err),
ClientError::Base64(err) => event.details("Base64 Error").reason(err),
ClientError::InvalidChallenge => event
.details("SMTP Authentication Error")
.reason("Invalid Challenge"),
ClientError::UnparseableReply => event.details("Unparseable SMTP Reply"),
ClientError::UnexpectedReply(reply) => event
.details("Unexpected SMTP Response")
.ctx(trc::Key::Code, reply.code)
.ctx(trc::Key::Reason, reply.message.clone()),
ClientError::AuthenticationFailed(reply) => event
.details("SMTP Authentication Failed")
.ctx(trc::Key::Code, reply.code)
.ctx(trc::Key::Reason, reply.message.clone()),
ClientError::InvalidTLSName => event.details("Invalid TLS Name"),
ClientError::MissingCredentials => event.details("Missing Authentication Credentials"),
ClientError::MissingMailFrom => event.details("Missing Message Sender"),
ClientError::MissingRcptTo => event.details("Missing Message Recipients"),
ClientError::UnsupportedAuthMechanism => {
event.details("Unsupported Authentication Mechanism")
}
ClientError::Timeout => event.details("Connection Timeout"),
ClientError::MissingStartTls => event.details("STARTTLS not available"),
}
}
pub(crate) fn from_error_status(err: &Status<HostResponse<Box<str>>, ErrorDetails>) -> trc::Error {
match err {
Status::Scheduled | Status::Completed(_) => {
trc::EventType::Smtp(trc::SmtpEvent::Error).into_err()
}
Status::TemporaryFailure(err) | Status::PermanentFailure(err) => {
from_error_details(&err.details)
}
}
}
pub(crate) fn from_error_details(err: &Error) -> trc::Error {
let event = trc::EventType::Smtp(trc::SmtpEvent::Error).into_err();
match err {
Error::DnsError(err) => event.details("DNS Error").reason(err),
Error::UnexpectedResponse(reply) => event
.details("Unexpected SMTP Response")
.ctx(trc::Key::Code, reply.response.code)
.ctx(trc::Key::Details, reply.command.clone())
.ctx(trc::Key::Reason, reply.response.message.clone()),
Error::ConnectionError(err) => event
.details("Connection Error")
.ctx(trc::Key::Reason, err.clone()),
Error::TlsError(err) => event
.details("TLS Error")
.ctx(trc::Key::Reason, err.clone()),
Error::DaneError(err) => event
.details("DANE Error")
.ctx(trc::Key::Reason, err.clone()),
Error::MtaStsError(err) => event.details("MTA-STS Error").reason(err),
Error::RateLimited => event.details("Rate Limited"),
Error::ConcurrencyLimited => event.details("Concurrency Limited"),
Error::Io(err) => event.details("I/O Error").reason(err),
}
}
+627
View File
@@ -0,0 +1,627 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{
Server,
config::smtp::resolver::{Tlsa, TlsaEntry, TlsaMatching},
};
pub use mail_auth::DnssecStatus;
use mail_auth::{
MX, RecordSet,
common::resolver::ToFqdn,
hickory_resolver::{
net::{DnsError, NetError},
proto::{
dnssec::Proof,
op::ResponseCode,
rr::{
Name, RData, Record, RecordType,
rdata::tlsa::{CertUsage, Matching, Selector},
},
},
},
};
use std::{
future::Future,
net::{Ipv4Addr, Ipv6Addr},
sync::Arc,
time::{Duration, Instant},
};
pub trait TlsaLookup: Sync + Send {
fn mx_lookup(
&self,
key: impl ToFqdn + Sync + Send,
) -> impl Future<Output = mail_auth::Result<RecordSet<MX>>> + Send;
fn tlsa_lookup(
&self,
key: impl ToFqdn + Sync + Send,
) -> impl Future<Output = mail_auth::Result<TlsaResult>> + Send;
fn ipv4_lookup_dnssec(
&self,
key: impl ToFqdn + Sync + Send,
) -> impl Future<Output = mail_auth::Result<RecordSet<Ipv4Addr>>> + Send;
fn ipv6_lookup_dnssec(
&self,
key: impl ToFqdn + Sync + Send,
) -> impl Future<Output = mail_auth::Result<RecordSet<Ipv6Addr>>> + Send;
}
pub enum TlsaResult {
Secure(Arc<Tlsa>),
Bogus,
Missing,
}
impl TlsaLookup for Server {
async fn mx_lookup(&self, key: impl ToFqdn + Sync + Send) -> mail_auth::Result<RecordSet<MX>> {
if !self.core.smtp.resolvers.dnssec_available {
return self
.core
.smtp
.resolvers
.dns
.mx_lookup(key, Some(&self.inner.cache.dns_mx))
.await;
}
let key = key.to_fqdn().into_owned().into_boxed_str();
if let Some(value) = self.inner.cache.dns_mx.get::<str>(key.as_ref())
&& value.dnssec_status != DnssecStatus::Indeterminate
{
return Ok(value);
}
#[cfg(any(test, feature = "test_mode"))]
if true {
return mail_auth::common::resolver::mock_resolve(key.as_ref());
}
let mx_lookup = match self
.core
.smtp
.resolvers
.dnssec
.resolver
.mx_lookup(Name::from_str_relaxed::<&str>(key.as_ref())?)
.await
{
Ok(mx_lookup) => mx_lookup,
Err(err) => {
if let Some(denial) = NegativeAnswer::from_error(&err)
&& denial.response_code == ResponseCode::NoError
{
let records = RecordSet {
rrset: Arc::new([]),
dnssec_status: denial.dnssec_status,
};
if let Some(valid_until) = denial.valid_until {
self.inner.cache.dns_mx.insert_with_expiry(
key,
records.clone(),
valid_until,
);
}
return Ok(records);
}
return Err(err.into());
}
};
let mx_records = mx_lookup.answers();
let mut dnssec_status: Option<DnssecStatus> = None;
let mut records: Vec<(u16, Vec<Box<str>>)> = Vec::with_capacity(mx_records.len());
for mx_record in mx_records {
if let RData::MX(mx) = &mx_record.data {
dnssec_status = Some(match dnssec_status {
Some(status) => least_secure(status, proof_to_dnssec_status(mx_record.proof)),
None => proof_to_dnssec_status(mx_record.proof),
});
let preference = mx.preference;
let exchange = mx.exchange.to_lowercase().to_ascii().into_boxed_str();
if let Some(record) = records.iter_mut().find(|r| r.0 == preference) {
record.1.push(exchange);
} else {
records.push((preference, vec![exchange]));
}
}
}
records.sort_unstable_by_key(|a| a.0);
let rrset: Arc<[MX]> = records
.into_iter()
.map(|(preference, exchanges)| MX {
preference,
exchanges: exchanges.into_boxed_slice(),
})
.collect::<Arc<[MX]>>();
let records = RecordSet {
rrset,
dnssec_status: dnssec_status.unwrap_or(DnssecStatus::Indeterminate),
};
self.inner
.cache
.dns_mx
.insert_with_expiry(key, records.clone(), mx_lookup.valid_until());
Ok(records)
}
async fn tlsa_lookup(&self, key: impl ToFqdn + Sync + Send) -> mail_auth::Result<TlsaResult> {
let key = key.to_fqdn().into_owned().into_boxed_str();
if let Some(value) = self.inner.cache.dns_tlsa.get(key.as_ref()) {
return Ok(TlsaResult::Secure(value));
}
#[cfg(any(test, feature = "test_mode"))]
if true {
if key.as_ref().contains("_dnssec_bogus.") {
return Ok(TlsaResult::Bogus);
}
return mail_auth::common::resolver::mock_resolve(key.as_ref());
}
let tlsa_lookup = match self
.core
.smtp
.resolvers
.dnssec
.resolver
.tlsa_lookup(Name::from_str_relaxed(key.as_ref())?)
.await
{
Ok(tlsa_lookup) => tlsa_lookup,
Err(err) => {
if let Some(denial) = NegativeAnswer::from_error(&err) {
return Ok(if denial.dnssec_status == DnssecStatus::Bogus {
TlsaResult::Bogus
} else {
TlsaResult::Missing
});
}
return Err(err.into());
}
};
let mut entries = Vec::new();
let mut has_end_entities = false;
let mut has_intermediates = false;
let mut dnssec_status: Option<DnssecStatus> = None;
for record in tlsa_lookup.answers() {
if let RData::TLSA(tlsa) = &record.data {
dnssec_status = Some(match dnssec_status {
Some(status) => least_secure(status, proof_to_dnssec_status(record.proof)),
None => proof_to_dnssec_status(record.proof),
});
if !record.proof.is_secure() {
continue;
}
let is_end_entity = match tlsa.cert_usage {
CertUsage::DaneEe => true,
CertUsage::DaneTa => false,
_ => continue,
};
let matching = match tlsa.matching {
Matching::Raw => TlsaMatching::Full,
Matching::Sha256 => TlsaMatching::Sha256,
Matching::Sha512 => TlsaMatching::Sha512,
_ => continue,
};
let is_spki = match tlsa.selector {
Selector::Spki => true,
Selector::Full => false,
_ => continue,
};
if is_end_entity {
has_end_entities = true;
} else {
has_intermediates = true;
}
entries.push(TlsaEntry {
is_end_entity,
is_spki,
matching,
data: tlsa.cert_data.clone(),
});
}
}
match dnssec_status {
Some(DnssecStatus::Bogus) => Ok(TlsaResult::Bogus),
Some(DnssecStatus::Secure) => {
let tlsa = Arc::new(Tlsa {
entries,
has_end_entities,
has_intermediates,
});
self.inner.cache.dns_tlsa.insert_with_expiry(
key,
tlsa.clone(),
tlsa_lookup.valid_until(),
);
Ok(TlsaResult::Secure(tlsa))
}
_ => Ok(TlsaResult::Missing),
}
}
async fn ipv4_lookup_dnssec(
&self,
key: impl ToFqdn + Sync + Send,
) -> mail_auth::Result<RecordSet<Ipv4Addr>> {
if !self.core.smtp.resolvers.dnssec_available {
return self
.core
.smtp
.resolvers
.dns
.ipv4_lookup(key, Some(&self.inner.cache.dns_ipv4))
.await;
}
let key = key.to_fqdn().into_owned().into_boxed_str();
if let Some(value) = self.inner.cache.dns_ipv4.get::<str>(key.as_ref())
&& value.dnssec_status != DnssecStatus::Indeterminate
{
return Ok(value);
}
#[cfg(any(test, feature = "test_mode"))]
if true {
return mail_auth::common::resolver::mock_resolve(key.as_ref());
}
let name = Name::from_str_relaxed::<&str>(key.as_ref())?;
let lookup = match self
.core
.smtp
.resolvers
.dnssec
.resolver
.ipv4_lookup(name.clone())
.await
{
Ok(lookup) => lookup,
Err(err) => {
if let Some(denial) = NegativeAnswer::from_error(&err)
&& denial.response_code == ResponseCode::NoError
{
let records = RecordSet {
rrset: Arc::new([]),
dnssec_status: denial.dnssec_status,
};
if let Some(valid_until) = denial.valid_until {
self.inner.cache.dns_ipv4.insert_with_expiry(
key,
records.clone(),
valid_until,
);
}
return Ok(records);
}
return Err(err.into());
}
};
let answers = lookup.answers();
let records = RecordSet {
rrset: answers
.iter()
.filter_map(|record| match &record.data {
RData::A(addr) => Some(addr.0),
_ => None,
})
.collect::<Arc<[Ipv4Addr]>>(),
dnssec_status: tlsa_base_status(&name, answers, RecordType::A),
};
self.inner
.cache
.dns_ipv4
.insert_with_expiry(key, records.clone(), lookup.valid_until());
Ok(records)
}
async fn ipv6_lookup_dnssec(
&self,
key: impl ToFqdn + Sync + Send,
) -> mail_auth::Result<RecordSet<Ipv6Addr>> {
if !self.core.smtp.resolvers.dnssec_available {
return self
.core
.smtp
.resolvers
.dns
.ipv6_lookup(key, Some(&self.inner.cache.dns_ipv6))
.await;
}
let key = key.to_fqdn().into_owned().into_boxed_str();
if let Some(value) = self.inner.cache.dns_ipv6.get::<str>(key.as_ref())
&& value.dnssec_status != DnssecStatus::Indeterminate
{
return Ok(value);
}
#[cfg(any(test, feature = "test_mode"))]
if true {
return mail_auth::common::resolver::mock_resolve(key.as_ref());
}
let name = Name::from_str_relaxed::<&str>(key.as_ref())?;
let lookup = match self
.core
.smtp
.resolvers
.dnssec
.resolver
.ipv6_lookup(name.clone())
.await
{
Ok(lookup) => lookup,
Err(err) => {
if let Some(denial) = NegativeAnswer::from_error(&err)
&& denial.response_code == ResponseCode::NoError
{
let records = RecordSet {
rrset: Arc::new([]),
dnssec_status: denial.dnssec_status,
};
if let Some(valid_until) = denial.valid_until {
self.inner.cache.dns_ipv6.insert_with_expiry(
key,
records.clone(),
valid_until,
);
}
return Ok(records);
}
return Err(err.into());
}
};
let answers = lookup.answers();
let records = RecordSet {
rrset: answers
.iter()
.filter_map(|record| match &record.data {
RData::AAAA(addr) => Some(addr.0),
_ => None,
})
.collect::<Arc<[Ipv6Addr]>>(),
dnssec_status: tlsa_base_status(&name, answers, RecordType::AAAA),
};
self.inner
.cache
.dns_ipv6
.insert_with_expiry(key, records.clone(), lookup.valid_until());
Ok(records)
}
}
struct NegativeAnswer {
response_code: ResponseCode,
dnssec_status: DnssecStatus,
valid_until: Option<Instant>,
}
impl NegativeAnswer {
fn from_error(err: &NetError) -> Option<Self> {
let NetError::Dns(dns_error) = err else {
return None;
};
match dns_error {
DnsError::NoRecordsFound(no_records) => Some(NegativeAnswer {
response_code: no_records.response_code,
dnssec_status: no_records
.authorities
.as_deref()
.map(denial_dnssec_status)
.unwrap_or(DnssecStatus::Indeterminate),
valid_until: no_records
.negative_ttl
.map(|ttl| Instant::now() + Duration::from_secs(ttl as u64)),
}),
DnsError::Nsec {
response, proof, ..
} => Some(NegativeAnswer {
response_code: response.response_code,
dnssec_status: proof_to_dnssec_status(*proof),
valid_until: None,
}),
_ => None,
}
}
}
fn denial_dnssec_status(authorities: &[Record]) -> DnssecStatus {
authorities
.iter()
.filter(|record| matches!(record.record_type(), RecordType::NSEC | RecordType::NSEC3))
.map(|record| proof_to_dnssec_status(record.proof))
.reduce(least_secure)
.unwrap_or(DnssecStatus::Indeterminate)
}
fn proof_to_dnssec_status(proof: Proof) -> DnssecStatus {
match proof {
Proof::Secure => DnssecStatus::Secure,
Proof::Insecure => DnssecStatus::Insecure,
Proof::Bogus => DnssecStatus::Bogus,
Proof::Indeterminate => DnssecStatus::Indeterminate,
}
}
fn tlsa_base_status(query: &Name, answers: &[Record], address_type: RecordType) -> DnssecStatus {
let mut addresses: Option<DnssecStatus> = None;
let mut alias: Option<DnssecStatus> = None;
for record in answers {
let status = proof_to_dnssec_status(record.proof);
if record.record_type() == address_type {
addresses = Some(match addresses {
Some(current) => least_secure(current, status),
None => status,
});
} else if record.record_type() == RecordType::CNAME && &record.name == query {
alias = Some(match alias {
Some(current) => least_secure(current, status),
None => status,
});
}
}
match (addresses, alias) {
(Some(DnssecStatus::Insecure), Some(DnssecStatus::Secure)) => DnssecStatus::Secure,
(Some(status), _) => status,
(None, _) => DnssecStatus::Indeterminate,
}
}
pub(crate) fn least_secure(a: DnssecStatus, b: DnssecStatus) -> DnssecStatus {
fn rank(status: DnssecStatus) -> u8 {
match status {
DnssecStatus::Bogus => 0,
DnssecStatus::Indeterminate => 1,
DnssecStatus::Insecure => 2,
DnssecStatus::Secure => 3,
}
}
if rank(a) <= rank(b) { a } else { b }
}
#[cfg(test)]
mod tests {
use super::*;
use mail_auth::hickory_resolver::proto::rr::rdata::{A, CNAME};
use std::net::Ipv4Addr;
fn name(value: &str) -> Name {
Name::from_ascii(value).unwrap()
}
fn address(owner: &str, proof: Proof) -> Record {
let mut record =
Record::from_rdata(name(owner), 3600, RData::A(A(Ipv4Addr::new(192, 0, 2, 1))));
record.proof = proof;
record
}
fn alias(owner: &str, target: &str, proof: Proof) -> Record {
let mut record = Record::from_rdata(name(owner), 3600, RData::CNAME(CNAME(name(target))));
record.proof = proof;
record
}
#[test]
fn tlsa_base_status_follows_address_records() {
let query = name("mx.example.org.");
for (proof, expected) in [
(Proof::Secure, DnssecStatus::Secure),
(Proof::Insecure, DnssecStatus::Insecure),
(Proof::Bogus, DnssecStatus::Bogus),
(Proof::Indeterminate, DnssecStatus::Indeterminate),
] {
assert_eq!(
tlsa_base_status(&query, &[address("mx.example.org.", proof)], RecordType::A),
expected,
"proof {proof}"
);
}
}
#[test]
fn tlsa_base_status_is_indeterminate_without_addresses() {
assert_eq!(
tlsa_base_status(&name("mx.example.org."), &[], RecordType::A),
DnssecStatus::Indeterminate
);
}
#[test]
fn tlsa_base_status_takes_least_secure_address() {
let query = name("mx.example.org.");
assert_eq!(
tlsa_base_status(
&query,
&[
address("mx.example.org.", Proof::Secure),
address("mx.example.org.", Proof::Insecure),
],
RecordType::A
),
DnssecStatus::Insecure
);
}
#[test]
fn tlsa_base_status_keeps_secure_alias_to_insecure_zone() {
let query = name("mx.example.org.");
assert_eq!(
tlsa_base_status(
&query,
&[
alias("mx.example.org.", "mx.provider.net.", Proof::Secure),
address("mx.provider.net.", Proof::Insecure),
],
RecordType::A
),
DnssecStatus::Secure
);
}
#[test]
fn tlsa_base_status_skips_insecure_alias() {
let query = name("mx.example.org.");
assert_eq!(
tlsa_base_status(
&query,
&[
alias("mx.example.org.", "mx.provider.net.", Proof::Insecure),
address("mx.provider.net.", Proof::Insecure),
],
RecordType::A
),
DnssecStatus::Insecure
);
}
#[test]
fn tlsa_base_status_ignores_alias_below_query_name() {
let query = name("mx.example.org.");
assert_eq!(
tlsa_base_status(
&query,
&[
alias("mx.provider.net.", "mx.other.net.", Proof::Secure),
address("mx.other.net.", Proof::Insecure),
],
RecordType::A
),
DnssecStatus::Insecure
);
}
}
+8
View File
@@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod dnssec;
pub mod verify;
+242
View File
@@ -0,0 +1,242 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::queue::{Error, ErrorDetails, HostResponse, Status};
use common::config::smtp::resolver::{Tlsa, TlsaEntry, TlsaMatching};
use rustls_pki_types::{CertificateDer, Der, ServerName, TrustAnchor, UnixTime};
use sha2::{Digest, Sha256, Sha512};
use trc::DaneEvent;
use webpki::{ALL_VERIFICATION_ALGS, EndEntityCert, KeyUsage, anchor_from_trusted_cert};
use x509_parser::asn1_rs::Any;
use x509_parser::prelude::{FromDer, X509Certificate};
pub trait TlsaVerify {
fn verify(
&self,
session_id: u64,
hostname: &str,
reference_ids: &[&str],
certificates: Option<&[CertificateDer<'_>]>,
) -> Result<(), Status<HostResponse<Box<str>>, ErrorDetails>>;
}
impl TlsaVerify for Tlsa {
fn verify(
&self,
session_id: u64,
hostname: &str,
reference_ids: &[&str],
certificates: Option<&[CertificateDer<'_>]>,
) -> Result<(), Status<HostResponse<Box<str>>, ErrorDetails>> {
let certificates = match certificates {
Some(certificates) if !certificates.is_empty() => certificates,
_ => {
trc::event!(
Dane(DaneEvent::NoCertificatesFound),
SpanId = session_id,
Hostname = hostname.to_string(),
);
return Err(Status::TemporaryFailure(ErrorDetails {
entity: hostname.into(),
details: Error::DaneError("No certificates were provided by host".into()),
}));
}
};
let mut parsed = Vec::with_capacity(certificates.len());
for der_certificate in certificates {
match X509Certificate::from_der(der_certificate.as_ref()) {
Ok((_, cert)) => parsed.push(cert),
Err(err) => {
trc::event!(
Dane(DaneEvent::CertificateParseError),
SpanId = session_id,
Hostname = hostname.to_string(),
Reason = err.to_string(),
);
return Err(Status::TemporaryFailure(ErrorDetails {
entity: hostname.into(),
details: Error::DaneError("Failed to parse X.509 certificate".into()),
}));
}
}
}
if verify_end_entity(self, session_id, hostname, certificates, &parsed)
|| verify_trust_anchor(
self,
session_id,
hostname,
reference_ids,
certificates,
&parsed,
)
{
trc::event!(
Dane(DaneEvent::AuthenticationSuccess),
SpanId = session_id,
Hostname = hostname.to_string(),
);
Ok(())
} else {
trc::event!(
Dane(DaneEvent::AuthenticationFailure),
SpanId = session_id,
Hostname = hostname.to_string(),
);
Err(Status::TemporaryFailure(ErrorDetails {
entity: hostname.into(),
details: Error::DaneError("No matching certificates found in TLSA records".into()),
}))
}
}
}
fn verify_end_entity(
tlsa: &Tlsa,
session_id: u64,
hostname: &str,
certificates: &[CertificateDer<'_>],
parsed: &[X509Certificate<'_>],
) -> bool {
if tlsa.has_end_entities {
for record in tlsa.entries.iter().filter(|record| record.is_end_entity) {
if record_matches(record, &parsed[0], certificates[0].as_ref()) {
trc::event!(
Dane(DaneEvent::TlsaRecordMatch),
SpanId = session_id,
Hostname = hostname.to_string(),
Type = "end-entity",
);
return true;
}
}
}
false
}
fn verify_trust_anchor(
tlsa: &Tlsa,
session_id: u64,
hostname: &str,
reference_ids: &[&str],
certificates: &[CertificateDer<'_>],
parsed: &[X509Certificate<'_>],
) -> bool {
if !tlsa.has_intermediates {
return false;
}
let end_entity = match EndEntityCert::try_from(&certificates[0]) {
Ok(end_entity) => end_entity,
Err(_) => return false,
};
let mut anchors: Vec<TrustAnchor<'static>> = Vec::new();
for record in tlsa.entries.iter().filter(|record| !record.is_end_entity) {
match (record.is_spki, record.matching) {
(false, TlsaMatching::Full) => {
let der = CertificateDer::from(record.data.clone());
if let Ok(anchor) = anchor_from_trusted_cert(&der) {
anchors.push(anchor.to_owned());
}
}
(true, TlsaMatching::Full) => {
if let Some(depth) = (1..certificates.len())
.find(|&depth| parsed[depth].public_key().raw == record.data.as_slice())
{
if let Ok(anchor) = anchor_from_trusted_cert(&certificates[depth]) {
anchors.push(anchor.to_owned());
}
} else if let Some(spki) = der_value(&record.data) {
for depth in 1..certificates.len() {
if is_chain_top(parsed, depth)
&& let Some(subject) = der_value(parsed[depth].issuer().as_raw())
{
anchors.push(TrustAnchor {
subject: Der::from(subject.to_vec()),
subject_public_key_info: Der::from(spki.to_vec()),
name_constraints: None,
});
}
}
}
}
_ => {
for depth in 1..certificates.len() {
if record_matches(record, &parsed[depth], certificates[depth].as_ref())
&& let Ok(anchor) = anchor_from_trusted_cert(&certificates[depth])
{
anchors.push(anchor.to_owned());
}
}
}
}
}
if anchors.is_empty()
|| end_entity
.verify_for_usage(
ALL_VERIFICATION_ALGS,
&anchors,
&certificates[1..],
UnixTime::now(),
KeyUsage::server_auth(),
None,
None,
)
.is_err()
|| !reference_ids.iter().any(|reference| {
ServerName::try_from(*reference)
.map(|name| end_entity.verify_is_valid_for_subject_name(&name).is_ok())
.unwrap_or(false)
})
{
false
} else {
trc::event!(
Dane(DaneEvent::TlsaRecordMatch),
SpanId = session_id,
Hostname = hostname.to_string(),
Type = "trust-anchor",
);
true
}
}
fn is_chain_top(parsed: &[X509Certificate<'_>], depth: usize) -> bool {
let issuer = parsed[depth].issuer().as_raw();
!parsed
.iter()
.enumerate()
.any(|(other, cert)| other != depth && cert.subject().as_raw() == issuer)
}
fn record_matches(record: &TlsaEntry, cert: &X509Certificate<'_>, raw: &[u8]) -> bool {
let selected: &[u8] = if record.is_spki {
cert.public_key().raw
} else {
raw
};
match record.matching {
TlsaMatching::Full => selected == record.data.as_slice(),
TlsaMatching::Sha256 => Sha256::digest(selected).as_slice() == record.data.as_slice(),
TlsaMatching::Sha512 => Sha512::digest(selected).as_slice() == record.data.as_slice(),
}
}
#[inline(always)]
fn der_value(der: &[u8]) -> Option<&[u8]> {
Any::from_der(der).ok().map(|(_, any)| any.data)
}
File diff suppressed because it is too large Load Diff
+150
View File
@@ -0,0 +1,150 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::fmt::Display;
use smtp_proto::{Response, Severity};
#[derive(Debug)]
pub enum ClientError {
/// I/O error
Io(std::io::Error),
/// TLS error
Tls(Box<rustls::Error>),
/// Base64 decode error
Base64(base64::DecodeError),
// SMTP authentication error.
InvalidChallenge,
/// Failure parsing SMTP reply
UnparseableReply,
/// Unexpected SMTP reply.
UnexpectedReply(Box<smtp_proto::Response<String>>),
/// SMTP authentication failure.
AuthenticationFailed(Box<smtp_proto::Response<String>>),
/// Invalid TLS name provided.
InvalidTLSName,
/// Missing authentication credentials.
MissingCredentials,
/// Missing message sender.
MissingMailFrom,
/// Missing message recipients.
MissingRcptTo,
/// The server does no support any of the available authentication methods.
UnsupportedAuthMechanism,
/// Connection timeout.
Timeout,
/// STARTTLS not available
MissingStartTls,
}
pub trait AssertReply: Sized {
fn is_positive_completion(&self) -> bool;
fn assert_positive_completion(self) -> ClientResult<()>;
fn assert_severity(self, severity: Severity) -> ClientResult<()>;
fn assert_code(self, code: u16) -> ClientResult<()>;
}
impl AssertReply for Response<String> {
/// Returns `true` if the reply is a positive completion.
#[inline(always)]
fn is_positive_completion(&self) -> bool {
(200..=299).contains(&self.code)
}
/// Returns Ok if the reply has the specified severity.
#[inline(always)]
fn assert_severity(self, severity: Severity) -> ClientResult<()> {
if self.severity() == severity {
Ok(())
} else {
Err(ClientError::UnexpectedReply(Box::new(self)))
}
}
/// Returns Ok if the reply returned a 2xx code.
#[inline(always)]
fn assert_positive_completion(self) -> ClientResult<()> {
if (200..=299).contains(&self.code) {
Ok(())
} else {
Err(ClientError::UnexpectedReply(Box::new(self)))
}
}
/// Returns Ok if the reply has the specified status code.
#[inline(always)]
fn assert_code(self, code: u16) -> ClientResult<()> {
if self.code() == code {
Ok(())
} else {
Err(ClientError::UnexpectedReply(Box::new(self)))
}
}
}
impl std::error::Error for ClientError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ClientError::Io(err) => err.source(),
ClientError::Tls(err) => err.source(),
ClientError::Base64(err) => err.source(),
_ => None,
}
}
}
pub type ClientResult<T> = std::result::Result<T, ClientError>;
impl Display for ClientError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClientError::Io(e) => write!(f, "I/O error: {e}"),
ClientError::Tls(e) => write!(f, "TLS error: {e}"),
ClientError::Base64(e) => write!(f, "Base64 decode error: {e}"),
ClientError::InvalidChallenge => {
write!(f, "SMTP authentication error: Invalid challenge")
}
ClientError::UnparseableReply => write!(f, "Unparseable SMTP reply"),
ClientError::UnexpectedReply(e) => write!(f, "Unexpected reply: {e}"),
ClientError::AuthenticationFailed(e) => write!(f, "Authentication failed: {e}"),
ClientError::InvalidTLSName => write!(f, "Invalid TLS name provided"),
ClientError::MissingCredentials => write!(f, "Missing authentication credentials"),
ClientError::MissingMailFrom => write!(f, "Missing message sender"),
ClientError::MissingRcptTo => write!(f, "Missing message recipients"),
ClientError::UnsupportedAuthMechanism => write!(
f,
"The server does no support any of the available authentication methods"
),
ClientError::Timeout => write!(f, "Connection timeout"),
ClientError::MissingStartTls => write!(f, "STARTTLS extension unavailable"),
}
}
}
impl From<std::io::Error> for ClientError {
fn from(err: std::io::Error) -> Self {
ClientError::Io(err)
}
}
impl From<base64::DecodeError> for ClientError {
fn from(err: base64::DecodeError) -> Self {
ClientError::Base64(err)
}
}
+145
View File
@@ -0,0 +1,145 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
inbound::dkim::DkimSign,
outbound::DeliveryResult,
queue::{
Error, ErrorDetails, FROM_AUTHENTICATED, FROM_UNAUTHENTICATED_DMARC, HostResponse,
MessageSource, MessageWrapper, Status, UnexpectedResponse,
quota::HasQueueQuota,
rcpt_spam_percentage,
spool::{QueueParams, SmtpSpool},
},
};
use common::Server;
use email::message::delivery::{IngestMessage, IngestRecipient, LocalDeliveryStatus, MailDelivery};
use smtp_proto::Response;
use trc::SieveEvent;
impl MessageWrapper {
pub(super) async fn deliver_local(
&self,
rcpt_idxs: &[usize],
statuses: &mut Vec<DeliveryResult>,
server: &Server,
) {
// Prepare recipients list
let mut pending_recipients = Vec::new();
let mut recipients = Vec::new();
for &rcpt_idx in rcpt_idxs {
let rcpt = &self.message.recipients[rcpt_idx];
let rcpt_addr = rcpt.address();
recipients.push(IngestRecipient {
address: rcpt_addr.to_lowercase(),
orcpt: rcpt.orcpt.as_ref().map(|orcpt| orcpt.to_string()),
spam_percentage: rcpt_spam_percentage(rcpt.flags),
});
pending_recipients.push((rcpt_idx, rcpt_addr));
}
// Deliver message
let delivery_result = server
.deliver_message(IngestMessage {
sender_address: self.message.return_path.to_string(),
sender_authenticated: self.message.flags
& (FROM_UNAUTHENTICATED_DMARC | FROM_AUTHENTICATED)
!= 0,
recipients,
message_blob: self.message.blob_hash.clone(),
message_size: self.message.size,
session_id: self.span_id,
})
.await;
// Process delivery results
for ((rcpt_idx, rcpt_addr), result) in
pending_recipients.into_iter().zip(delivery_result.status)
{
let status = match result {
LocalDeliveryStatus::Success => Status::Completed(HostResponse {
hostname: "localhost".into(),
response: Response {
code: 250,
esc: [2, 1, 5],
message: "OK".into(),
},
}),
LocalDeliveryStatus::TemporaryFailure { reason } => {
Status::TemporaryFailure(ErrorDetails {
entity: "localhost".into(),
details: Error::UnexpectedResponse(UnexpectedResponse {
command: format!("RCPT TO:<{rcpt_addr}>").into_boxed_str(),
response: Response {
code: 451,
esc: [4, 3, 0],
message: reason.into(),
},
}),
})
}
LocalDeliveryStatus::PermanentFailure { code, reason } => {
Status::PermanentFailure(ErrorDetails {
entity: "localhost".into(),
details: Error::UnexpectedResponse(UnexpectedResponse {
command: format!("RCPT TO:<{rcpt_addr}>").into_boxed_str(),
response: Response {
code: 550,
esc: code,
message: reason.into(),
},
}),
})
}
};
statuses.push(DeliveryResult::account(status, rcpt_idx));
}
// Process autogenerated messages
for autogenerated in delivery_result.autogenerated {
let mut message = server.new_message(
autogenerated.sender_address,
MessageSource::Autogenerated,
self.span_id,
);
for rcpt in autogenerated.recipients {
message.expand_and_add_recipient(rcpt, server).await;
}
// Queue Message
message.message.size = autogenerated.message.len() as u64;
if let Some(metadata) = server.has_quota(&mut message).await {
let dkim_signers = server
.eval_signers(
&server.core.sieve.untrusted_sign,
&message.message,
self.span_id,
)
.await;
message
.queue(
QueueParams::new(&autogenerated.message, self.span_id, server)
.with_dkim_signers(dkim_signers)
.with_metadata(metadata),
)
.await;
} else {
trc::event!(
Sieve(SieveEvent::QuotaExceeded),
SpanId = self.span_id,
From = message.message.return_path,
To = message
.message
.recipients
.into_iter()
.map(|r| trc::Value::from(r.address().to_string()))
.collect::<Vec<_>>(),
);
}
}
}
}
+296
View File
@@ -0,0 +1,296 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::NextHop;
use super::dane::dnssec::{TlsaLookup, least_secure};
use crate::queue::{Error, ErrorDetails, HostResponse, Status};
use common::{
Server,
config::smtp::queue::{ConnectionStrategy, HostOrIp, IpAndHost, MxConfig},
expr::functions::ResolveVariable,
};
use mail_auth::{DnssecStatus, IpLookupStrategy, MX, RecordSet};
use rand::{RngExt, seq::SliceRandom};
use registry::schema::enums::ExpressionVariable;
use std::{future::Future, net::IpAddr, sync::Arc};
pub struct ResolvedHost {
pub ips: Vec<IpAddr>,
pub dnssec_status: DnssecStatus,
}
pub trait DnsLookup: Sync + Send {
fn ip_lookup(
&self,
key: &str,
strategy: IpLookupStrategy,
max_results: usize,
dnssec: bool,
) -> impl Future<Output = mail_auth::Result<(Vec<IpAddr>, DnssecStatus)>> + Send;
fn resolve_host(
&self,
remote_host: &NextHop<'_>,
envelope: &impl ResolveVariable,
dnssec: bool,
) -> impl Future<Output = Result<ResolvedHost, Status<HostResponse<Box<str>>, ErrorDetails>>> + Send;
}
impl DnsLookup for Server {
async fn ip_lookup(
&self,
key: &str,
strategy: IpLookupStrategy,
max_results: usize,
dnssec: bool,
) -> mail_auth::Result<(Vec<IpAddr>, DnssecStatus)> {
let (has_ipv4, has_ipv6, v4_first) = match strategy {
IpLookupStrategy::Ipv4Only => (true, false, false),
IpLookupStrategy::Ipv6Only => (false, true, false),
IpLookupStrategy::Ipv4thenIpv6 => (true, true, true),
IpLookupStrategy::Ipv6thenIpv4 => (true, true, false),
};
let mut dnssec_status: Option<DnssecStatus> = None;
let ipv4_addrs = if has_ipv4 {
let result = if dnssec {
self.ipv4_lookup_dnssec(key).await
} else {
self.core
.smtp
.resolvers
.dns
.ipv4_lookup(key, Some(&self.inner.cache.dns_ipv4))
.await
};
match result {
Ok(addrs) => {
if !addrs.rrset.is_empty() {
dnssec_status = Some(addrs.dnssec_status);
}
addrs.rrset
}
Err(_) if has_ipv6 => Arc::new([]),
Err(err) => return Err(err),
}
} else {
Arc::new([])
};
let ipv6_addrs = if has_ipv6 {
let result = if dnssec {
self.ipv6_lookup_dnssec(key).await
} else {
self.core
.smtp
.resolvers
.dns
.ipv6_lookup(key, Some(&self.inner.cache.dns_ipv6))
.await
};
match result {
Ok(addrs) => {
if !addrs.rrset.is_empty() {
dnssec_status = Some(match dnssec_status {
Some(status) => least_secure(status, addrs.dnssec_status),
None => addrs.dnssec_status,
});
}
addrs.rrset
}
Err(_) if !ipv4_addrs.is_empty() => Arc::new([]),
Err(err) => return Err(err),
}
} else {
Arc::new([])
};
let remote_ips = if v4_first {
ipv4_addrs
.iter()
.copied()
.map(IpAddr::from)
.chain(ipv6_addrs.iter().copied().map(IpAddr::from))
.take(max_results)
.collect()
} else {
ipv6_addrs
.iter()
.copied()
.map(IpAddr::from)
.chain(ipv4_addrs.iter().copied().map(IpAddr::from))
.take(max_results)
.collect()
};
Ok((
remote_ips,
dnssec_status.unwrap_or(DnssecStatus::Indeterminate),
))
}
async fn resolve_host(
&self,
remote_host: &NextHop<'_>,
envelope: &impl ResolveVariable,
dnssec: bool,
) -> Result<ResolvedHost, Status<HostResponse<Box<str>>, ErrorDetails>> {
let (mut remote_ips, dnssec_status) = match remote_host.fqdn_hostname() {
HostOrIp::Host(hostname) => self
.ip_lookup(
hostname.as_ref(),
remote_host.ip_lookup_strategy(),
remote_host.max_multi_homed(),
dnssec,
)
.await
.map_err(|err| {
if let mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(_)) = &err {
if matches!(
remote_host,
NextHop::MX {
is_implicit: true,
..
}
) {
Status::PermanentFailure(ErrorDetails {
entity: remote_host.hostname().into(),
details: Error::DnsError("no MX record found.".into()),
})
} else {
Status::PermanentFailure(ErrorDetails {
entity: remote_host.hostname().into(),
details: Error::ConnectionError("record not found for MX".into()),
})
}
} else {
Status::TemporaryFailure(ErrorDetails {
entity: remote_host.hostname().into(),
details: Error::ConnectionError(
format!("lookup error: {err}").into_boxed_str(),
),
})
}
})?,
HostOrIp::Ip(ip) => (vec![ip], DnssecStatus::Indeterminate),
};
if !remote_ips.is_empty() {
if !remote_host.allow_loopback() && remote_ips.iter().any(|ip| ip.is_loopback()) {
remote_ips.retain(|ip| !ip.is_loopback());
if remote_ips.is_empty() {
return Err(Status::PermanentFailure(ErrorDetails {
entity: remote_host.hostname().into(),
details: Error::ConnectionError("host resolves loopback address".into()),
}));
}
}
Ok(ResolvedHost {
ips: remote_ips,
dnssec_status,
})
} else {
Err(Status::TemporaryFailure(ErrorDetails {
entity: remote_host.hostname().into(),
details: Error::DnsError(
format!(
"No IP addresses found for {:?}.",
envelope
.resolve_variable(ExpressionVariable::Mx)
.to_string()
)
.into_boxed_str(),
),
}))
}
}
}
pub trait SourceIp {
fn source_ip(&self, is_v4: bool) -> Option<&IpAndHost>;
}
impl SourceIp for ConnectionStrategy {
fn source_ip(&self, is_v4: bool) -> Option<&IpAndHost> {
let ips = if is_v4 {
&self.source_ipv4
} else {
&self.source_ipv6
};
match ips.len().cmp(&1) {
std::cmp::Ordering::Equal => ips.first(),
std::cmp::Ordering::Greater => Some(&ips[rand::rng().random_range(0..ips.len())]),
std::cmp::Ordering::Less => None,
}
}
}
pub trait ToNextHop {
fn to_remote_hosts<'x, 'y: 'x>(
&'x self,
domain: &'y str,
config: &'x MxConfig,
) -> Option<Vec<NextHop<'x>>>;
}
impl ToNextHop for RecordSet<MX> {
fn to_remote_hosts<'x, 'y: 'x>(
&'x self,
domain: &'y str,
config: &'x MxConfig,
) -> Option<Vec<NextHop<'x>>> {
if !self.rrset.is_empty() {
// Obtain max number of MX hosts to process
let mut remote_hosts = Vec::with_capacity(config.max_mx);
'outer: for mx in self.rrset.iter() {
if mx.exchanges.len() > 1 {
let mut slice = mx.exchanges.iter().collect::<Vec<_>>();
slice.shuffle(&mut rand::rng());
for remote_host in slice {
remote_hosts.push(NextHop::MX {
host: remote_host.as_ref(),
is_implicit: false,
dnssec_status: self.dnssec_status,
config,
});
if remote_hosts.len() == config.max_mx {
break 'outer;
}
}
} else if let Some(remote_host) = mx.exchanges.first() {
// Check for Null MX
if mx.preference == 0 && remote_host.as_ref() == "." {
return None;
}
remote_hosts.push(NextHop::MX {
host: remote_host.as_ref(),
is_implicit: false,
dnssec_status: self.dnssec_status,
config,
});
if remote_hosts.len() == config.max_mx {
break;
}
}
}
remote_hosts.into()
} else {
// If an empty list of MXs is returned, the address is treated as if it was
// associated with an implicit MX RR with a preference of 0, pointing to that host.
vec![NextHop::MX {
host: domain,
is_implicit: true,
dnssec_status: self.dnssec_status,
config,
}]
.into()
}
}
}
+379
View File
@@ -0,0 +1,379 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
outbound::{client::BoxResponse, error::ClientError},
queue::{Error, ErrorDetails, HostResponse, Status, UnexpectedResponse},
};
use common::config::{
server::ServerProtocol,
smtp::queue::{HostOrIp, MxConfig, RelayConfig},
};
use directory::Credentials;
use mail_auth::{DnssecStatus, IpLookupStrategy};
use smtp_proto::{Response, Severity};
use std::{borrow::Cow, net::IpAddr};
pub mod client;
pub mod dane;
pub mod delivery;
pub mod error;
pub mod local;
pub mod lookup;
pub mod mta_sts;
pub mod session;
pub(super) enum DeliveryResult {
Domain {
status: Status<HostResponse<Box<str>>, ErrorDetails>,
rcpt_idxs: Vec<usize>,
},
Account {
status: Status<HostResponse<Box<str>>, ErrorDetails>,
rcpt_idx: usize,
},
RateLimited {
rcpt_idxs: Vec<usize>,
retry_at: u64,
},
}
impl Status<HostResponse<Box<str>>, ErrorDetails> {
pub fn from_smtp_error(hostname: &str, command: &str, err: ClientError) -> Self {
match err {
ClientError::Io(_)
| ClientError::Tls(_)
| ClientError::Base64(_)
| ClientError::UnparseableReply
| ClientError::AuthenticationFailed(_)
| ClientError::MissingCredentials
| ClientError::MissingMailFrom
| ClientError::MissingRcptTo
| ClientError::Timeout => Status::TemporaryFailure(ErrorDetails {
entity: hostname.into(),
details: Error::ConnectionError(err.to_string().into_boxed_str()),
}),
ClientError::UnexpectedReply(response) => {
if response.severity() == Severity::PermanentNegativeCompletion {
Status::PermanentFailure(ErrorDetails {
entity: hostname.into(),
details: Error::UnexpectedResponse(UnexpectedResponse {
command: command.trim().into(),
response: response.into_box(),
}),
})
} else {
Status::TemporaryFailure(ErrorDetails {
entity: hostname.into(),
details: Error::UnexpectedResponse(UnexpectedResponse {
command: command.trim().into(),
response: response.into_box(),
}),
})
}
}
ClientError::InvalidChallenge
| ClientError::UnsupportedAuthMechanism
| ClientError::InvalidTLSName
| ClientError::MissingStartTls => Status::PermanentFailure(ErrorDetails {
entity: hostname.into(),
details: Error::ConnectionError(err.to_string().into_boxed_str()),
}),
}
}
pub fn from_starttls_error(hostname: &str, response: Option<Response<Box<str>>>) -> Self {
let entity = hostname.into();
if let Some(response) = response {
if response.severity() == Severity::PermanentNegativeCompletion {
Status::PermanentFailure(ErrorDetails {
entity,
details: Error::UnexpectedResponse(UnexpectedResponse {
command: "STARTTLS".into(),
response,
}),
})
} else {
Status::TemporaryFailure(ErrorDetails {
entity,
details: Error::UnexpectedResponse(UnexpectedResponse {
command: "STARTTLS".into(),
response,
}),
})
}
} else {
Status::PermanentFailure(ErrorDetails {
entity,
details: Error::TlsError("STARTTLS not advertised by host.".into()),
})
}
}
pub fn from_tls_error(hostname: &str, err: ClientError) -> Self {
match err {
ClientError::InvalidTLSName => Status::PermanentFailure(ErrorDetails {
entity: hostname.into(),
details: Error::TlsError("Invalid hostname".into()),
}),
ClientError::Timeout => Status::TemporaryFailure(ErrorDetails {
entity: hostname.into(),
details: Error::TlsError("TLS handshake timed out".into()),
}),
ClientError::Tls(err) => Status::TemporaryFailure(ErrorDetails {
entity: hostname.into(),
details: Error::TlsError(format!("Handshake failed: {err}").into_boxed_str()),
}),
ClientError::Io(err) => Status::TemporaryFailure(ErrorDetails {
entity: hostname.into(),
details: Error::TlsError(format!("I/O error: {err}").into_boxed_str()),
}),
_ => Status::PermanentFailure(ErrorDetails {
entity: hostname.into(),
details: Error::TlsError("Other TLS error".into()),
}),
}
}
pub fn timeout(hostname: &str, stage: &str) -> Self {
Status::TemporaryFailure(ErrorDetails {
entity: hostname.into(),
details: Error::ConnectionError(format!("Timeout while {stage}").into_boxed_str()),
})
}
pub fn local_error() -> Self {
Status::TemporaryFailure(ErrorDetails {
entity: "localhost".into(),
details: Error::ConnectionError("Could not deliver message locally.".into()),
})
}
pub fn from_mail_auth_error(entity: &str, err: mail_auth::Error) -> Self {
match &err {
mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(code)) => {
Status::PermanentFailure(ErrorDetails {
entity: entity.into(),
details: Error::DnsError(
format!("Domain not found: {code:?}").into_boxed_str(),
),
})
}
_ => Status::TemporaryFailure(ErrorDetails {
entity: entity.into(),
details: Error::DnsError(err.to_string().into_boxed_str()),
}),
}
}
pub fn from_mta_sts_error(entity: &str, err: mta_sts::Error) -> Self {
match &err {
mta_sts::Error::Dns(err) => match err {
mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(code)) => {
Status::PermanentFailure(ErrorDetails {
entity: entity.into(),
details: Error::MtaStsError(
format!("Record not found: {code:?}").into_boxed_str(),
),
})
}
mail_auth::Error::Dns(mail_auth::DnsError::InvalidRecordType) => {
Status::PermanentFailure(ErrorDetails {
entity: entity.into(),
details: Error::MtaStsError("Failed to parse MTA-STS DNS record.".into()),
})
}
_ => Status::TemporaryFailure(ErrorDetails {
entity: entity.into(),
details: Error::MtaStsError(
format!("DNS lookup error: {err}").into_boxed_str(),
),
}),
},
mta_sts::Error::Http(err) => {
if err.is_timeout() {
Status::TemporaryFailure(ErrorDetails {
entity: entity.into(),
details: Error::MtaStsError("Timeout fetching policy.".into()),
})
} else if err.is_connect() {
Status::TemporaryFailure(ErrorDetails {
entity: entity.into(),
details: Error::MtaStsError("Could not reach policy host.".into()),
})
} else if err.is_status()
& err
.status()
.is_some_and(|s| s == reqwest::StatusCode::NOT_FOUND)
{
Status::PermanentFailure(ErrorDetails {
entity: entity.into(),
details: Error::MtaStsError("Policy not found.".into()),
})
} else {
Status::TemporaryFailure(ErrorDetails {
entity: entity.into(),
details: Error::MtaStsError("Failed to fetch policy.".into()),
})
}
}
mta_sts::Error::InvalidPolicy(err) => Status::PermanentFailure(ErrorDetails {
entity: entity.into(),
details: Error::MtaStsError(
format!("Failed to parse policy: {err}").into_boxed_str(),
),
}),
}
}
}
#[derive(Debug)]
pub enum NextHop<'x> {
Relay(&'x RelayConfig),
MX {
is_implicit: bool,
host: &'x str,
config: &'x MxConfig,
dnssec_status: DnssecStatus,
},
}
impl NextHop<'_> {
#[inline(always)]
pub fn hostname(&self) -> &str {
match self {
NextHop::MX { host, .. } => {
if let Some(host) = host.strip_suffix('.') {
host
} else {
host
}
}
NextHop::Relay(host) => match &host.address {
HostOrIp::Host(host) => host.as_ref(),
HostOrIp::Ip(ip) => ip.ip_str.as_ref(),
},
}
}
#[inline(always)]
pub fn fqdn_hostname(&self) -> HostOrIp<Cow<'_, str>, IpAddr> {
match self {
NextHop::MX { host, .. } => {
if !host.ends_with('.') {
HostOrIp::Host(format!("{host}.").into())
} else {
HostOrIp::Host((*host).into())
}
}
NextHop::Relay(host) => match &host.address {
HostOrIp::Host(host) => HostOrIp::Host(host.as_ref().into()),
HostOrIp::Ip(ip) => HostOrIp::Ip(ip.ip),
},
}
}
#[inline(always)]
pub fn max_multi_homed(&self) -> usize {
match self {
NextHop::MX { config, .. } => config.max_multi_homed,
NextHop::Relay(_) => 10,
}
}
#[inline(always)]
pub fn ip_lookup_strategy(&self) -> IpLookupStrategy {
match self {
NextHop::MX { config, .. } => config.ip_lookup_strategy,
NextHop::Relay(_) => IpLookupStrategy::Ipv4thenIpv6,
}
}
#[inline(always)]
fn port(&self) -> u16 {
match self {
#[cfg(feature = "test_mode")]
NextHop::MX { .. } => 9925,
#[cfg(not(feature = "test_mode"))]
NextHop::MX { .. } => 25,
NextHop::Relay(host) => host.port,
}
}
#[inline(always)]
fn allow_loopback(&self) -> bool {
match self {
NextHop::MX { .. } => cfg!(feature = "test_mode"),
NextHop::Relay(_) => true,
}
}
#[inline(always)]
fn credentials(&self) -> Option<&Credentials> {
match self {
NextHop::MX { .. } => None,
NextHop::Relay(host) => host.auth.as_ref(),
}
}
#[inline(always)]
fn allow_invalid_certs(&self) -> bool {
#[cfg(feature = "test_mode")]
{
true
}
#[cfg(not(feature = "test_mode"))]
match self {
NextHop::MX { .. } => false,
NextHop::Relay(host) => host.tls_allow_invalid_certs,
}
}
#[inline(always)]
fn implicit_tls(&self) -> bool {
match self {
NextHop::MX { .. } => false,
NextHop::Relay(host) => host.tls_implicit,
}
}
#[inline(always)]
fn is_smtp(&self) -> bool {
match self {
NextHop::MX { .. } => true,
NextHop::Relay(host) => host.protocol == ServerProtocol::Smtp,
}
}
fn dnssec_status(&self) -> DnssecStatus {
match self {
NextHop::MX { dnssec_status, .. } => *dnssec_status,
NextHop::Relay(_) => DnssecStatus::Indeterminate,
}
}
}
impl DeliveryResult {
pub fn domain(
status: Status<HostResponse<Box<str>>, ErrorDetails>,
rcpt_idxs: Vec<usize>,
) -> Self {
DeliveryResult::Domain { status, rcpt_idxs }
}
pub fn rate_limited(rcpt_idxs: Vec<usize>, retry_at: u64) -> Self {
DeliveryResult::RateLimited {
rcpt_idxs,
retry_at,
}
}
pub fn account(status: Status<HostResponse<Box<str>>, ErrorDetails>, rcpt_idx: usize) -> Self {
DeliveryResult::Account { status, rcpt_idx }
}
}
+158
View File
@@ -0,0 +1,158 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{fmt::Display, sync::Arc, time::Duration};
#[cfg(feature = "test_mode")]
pub static STS_TEST_POLICY: parking_lot::Mutex<Vec<u8>> = parking_lot::Mutex::new(Vec::new());
use common::{Server, config::smtp::resolver::Policy};
use mail_auth::{mta_sts::MtaSts, report::tlsrpt::ResultType};
use super::{Error, parse::ParsePolicy};
#[cfg(not(feature = "test_mode"))]
use utils::HttpLimitResponse;
#[cfg(not(feature = "test_mode"))]
const MAX_POLICY_SIZE: usize = 1024 * 1024;
pub trait MtaStsLookup: Sync + Send {
fn lookup_mta_sts_policy(
&self,
domain: &str,
timeout: Duration,
) -> impl std::future::Future<Output = Result<Arc<Policy>, Error>> + Send;
}
#[allow(unused_variables)]
impl MtaStsLookup for Server {
async fn lookup_mta_sts_policy(
&self,
domain: &str,
timeout: Duration,
) -> Result<Arc<Policy>, Error> {
// Lookup MTA-STS TXT record
let record = match self
.core
.smtp
.resolvers
.dns
.txt_lookup::<MtaSts>(
format!("_mta-sts.{domain}."),
Some(&self.inner.cache.dns_txt),
)
.await
{
Ok(record) => record,
Err(err) => {
// Return the cached policy in case of failure
return if let Some(value) = self.inner.cache.dns_mta_sts.get(domain) {
Ok(value)
} else {
Err(err.into())
};
}
};
// Check if the policy has been cached
if let Some(value) = self.inner.cache.dns_mta_sts.get(domain)
&& value.id == record.id
{
return Ok(value);
}
// Fetch policy
#[cfg(not(feature = "test_mode"))]
let bytes = self
.core
.smtp
.mta_sts_client
.get(format!("https://mta-sts.{domain}/.well-known/mta-sts.txt"))
.timeout(timeout)
.send()
.await?
.bytes_with_limit(MAX_POLICY_SIZE)
.await?
.ok_or_else(|| Error::InvalidPolicy("Policy too large".to_string()))?;
#[cfg(feature = "test_mode")]
let bytes = STS_TEST_POLICY.lock().clone();
// Parse policy
let policy = Arc::new(Policy::parse(
std::str::from_utf8(&bytes).map_err(|err| Error::InvalidPolicy(err.to_string()))?,
record.id.clone(),
)?);
self.inner.cache.dns_mta_sts.insert(
domain.into(),
policy.clone(),
Duration::from_secs(if (3600..31557600).contains(&policy.max_age) {
policy.max_age
} else {
86400
}),
);
Ok(policy)
}
}
impl From<&Error> for ResultType {
fn from(err: &Error) -> Self {
match &err {
Error::InvalidPolicy(_) => ResultType::StsPolicyInvalid,
_ => ResultType::StsPolicyFetchError,
}
}
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Dns(err) => match err {
mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(code)) => {
write!(f, "Record not found: {code:?}")
}
mail_auth::Error::Dns(mail_auth::DnsError::InvalidRecordType) => {
f.write_str("Failed to parse MTA-STS DNS record.")
}
_ => write!(f, "DNS lookup error: {err}"),
},
Error::Http(err) => {
if err.is_timeout() {
f.write_str("Timeout fetching policy.")
} else if err.is_connect() {
f.write_str("Could not reach policy host.")
} else if err.is_status() && (err.status() == Some(reqwest::StatusCode::NOT_FOUND))
{
f.write_str("Policy not found.")
} else {
f.write_str("Failed to fetch policy.")
}
}
Error::InvalidPolicy(err) => write!(f, "Failed to parse policy: {err}"),
}
}
}
impl From<mail_auth::Error> for Error {
fn from(value: mail_auth::Error) -> Self {
Error::Dns(value)
}
}
impl From<reqwest::Error> for Error {
fn from(value: reqwest::Error) -> Self {
Error::Http(value)
}
}
impl From<String> for Error {
fn from(value: String) -> Self {
Error::InvalidPolicy(value)
}
}
+16
View File
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod lookup;
pub mod parse;
pub mod verify;
#[derive(Debug)]
pub enum Error {
Dns(mail_auth::Error),
Http(reqwest::Error),
InvalidPolicy(String),
}
+110
View File
@@ -0,0 +1,110 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::config::smtp::resolver::{Mode, MxPattern, Policy};
use utils::DomainPart;
fn to_a_label(domain: &str) -> String {
domain
.to_ascii_domain()
.map(|domain| domain.to_lowercase())
.unwrap_or_else(|| domain.to_lowercase())
}
pub trait ParsePolicy {
fn parse(data: &str, id: String) -> Result<Self, String>
where
Self: Sized;
}
impl ParsePolicy for Policy {
fn parse(mut data: &str, id: String) -> Result<Policy, String> {
let mut mode = Mode::None;
let mut max_age: u64 = 86400;
let mut mx = Vec::new();
while !data.is_empty() {
if let Some((key, next_data)) = data.split_once(':') {
let value = if let Some((value, next_data)) = next_data.split_once('\n') {
data = next_data;
value.trim()
} else {
data = "";
next_data.trim()
};
hashify::fnc_map!(key.trim().as_bytes(),
b"mx" => {
if let Some(suffix) = value.strip_prefix("*.") {
if !suffix.is_empty() {
mx.push(MxPattern::StartsWith(to_a_label(suffix)));
}
} else if !value.is_empty() {
mx.push(MxPattern::Equals(to_a_label(value)));
}
},
b"max_age" => {
if let Ok(value) = value.parse() {
max_age = value;
}
},
b"mode" => {
mode = match value {
"enforce" => Mode::Enforce,
"testing" => Mode::Testing,
"none" => Mode::None,
_ => return Err(format!("Unsupported mode {value:?}.")),
};
},
b"version" => {
if !value.eq_ignore_ascii_case("STSv1") {
return Err(format!("Unsupported version {value:?}."));
}
},
_ => {}
);
} else {
break;
}
}
if !mx.is_empty() {
Ok(Policy {
id,
mode,
mx: mx.into_boxed_slice(),
max_age,
})
} else {
Err("No 'mx' entries found.".to_string())
}
}
}
#[cfg(test)]
mod test {
use super::ParsePolicy;
use crate::outbound::mta_sts::verify::VerifyPolicy;
use common::config::smtp::resolver::Policy;
#[test]
fn mx_patterns_are_a_labels() {
let policy = Policy::parse(
concat!(
"version: STSv1\n",
"mode: enforce\n",
"mx: *.\u{5de}\u{5d9}\u{5d9}\u{5dc}.\u{5e7}\u{5d5}\u{5dd}\n",
"mx: MAIL.\u{5de}\u{5d9}\u{5d9}\u{5dc}.\u{5e7}\u{5d5}\u{5dd}\n",
"max_age: 604800\n"
),
"test".to_string(),
)
.unwrap();
assert!(policy.verify("mx.xn--eebajf.xn--9dbq2a"));
assert!(policy.verify("mail.xn--eebajf.xn--9dbq2a"));
assert!(!policy.verify("mx.example.org"));
}
}
@@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::config::smtp::resolver::{Mode, MxPattern, Policy};
pub trait VerifyPolicy {
fn verify(&self, mx_host: &str) -> bool;
fn enforce(&self) -> bool;
}
impl VerifyPolicy for Policy {
fn verify(&self, mx_host: &str) -> bool {
if self.mode != Mode::None {
for mx_pattern in &self.mx {
match mx_pattern {
MxPattern::Equals(host) => {
if host == mx_host {
return true;
}
}
MxPattern::StartsWith(domain) => {
if let Some((_, suffix)) = mx_host.split_once('.')
&& suffix == domain
{
return true;
}
}
}
}
false
} else {
true
}
}
fn enforce(&self) -> bool {
self.mode == Mode::Enforce
}
}
+481
View File
@@ -0,0 +1,481 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::client::SmtpClient;
use crate::outbound::DeliveryResult;
use crate::outbound::client::{BoxResponse, from_error_status, from_mail_send_error};
use crate::outbound::error::ClientError;
use crate::queue::{Error, MessageWrapper, Recipient, Status};
use crate::queue::{ErrorDetails, HostResponse, UnexpectedResponse};
use common::Server;
use common::config::smtp::queue::ConnectionStrategy;
use directory::Credentials;
use smtp_proto::{
EXT_CHUNKING, EXT_DSN, EXT_REQUIRE_TLS, EXT_SIZE, EXT_SMTP_UTF8, EhloResponse, MAIL_REQUIRETLS,
MAIL_RET_FULL, MAIL_RET_HDRS, MAIL_SMTPUTF8, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE,
RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, Severity,
};
use std::{fmt::Write, time::Instant};
use tokio::io::{AsyncRead, AsyncWrite};
use trc::DeliveryEvent;
pub struct SessionParams<'x> {
pub server: &'x Server,
pub hostname: &'x str,
pub credentials: Option<&'x Credentials>,
pub capabilities: Option<EhloResponse<String>>,
pub is_smtp: bool,
pub local_hostname: &'x str,
pub conn_strategy: &'x ConnectionStrategy,
pub session_id: u64,
}
impl MessageWrapper {
pub(super) async fn deliver<T: AsyncRead + AsyncWrite + Unpin>(
&self,
mut smtp_client: SmtpClient<T>,
rcpt_idxs: Vec<usize>,
rcpt_headers: Option<&[u8]>,
statuses: &mut Vec<DeliveryResult>,
mut params: SessionParams<'_>,
) {
// Obtain capabilities
let time = Instant::now();
let capabilities = if let Some(capabilities) = params.capabilities.take() {
capabilities
} else {
match smtp_client.say_helo(&params).await {
Ok(capabilities) => {
trc::event!(
Delivery(DeliveryEvent::Ehlo),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
Details = capabilities.capabilities(),
Elapsed = time.elapsed(),
);
capabilities
}
Err(status) => {
trc::event!(
Delivery(DeliveryEvent::EhloRejected),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
CausedBy = from_error_status(&status),
Elapsed = time.elapsed(),
);
smtp_client.quit().await;
statuses.push(DeliveryResult::domain(status, rcpt_idxs));
return;
}
}
};
// Authenticate
if let Some(credentials) = params.credentials {
let time = Instant::now();
if let Err(err) = smtp_client.authenticate(credentials, &capabilities).await {
trc::event!(
Delivery(DeliveryEvent::AuthFailed),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
CausedBy = from_mail_send_error(&err),
Elapsed = time.elapsed(),
);
smtp_client.quit().await;
statuses.push(DeliveryResult::domain(
Status::from_smtp_error(params.hostname, "AUTH ...", err),
rcpt_idxs,
));
return;
}
trc::event!(
Delivery(DeliveryEvent::Auth),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
Elapsed = time.elapsed(),
);
// Refresh capabilities
// Disabled as some SMTP servers deauthenticate after EHLO
/*capabilities = match say_helo(&mut smtp_client, &params).await {
Ok(capabilities) => capabilities,
Err(status) => {
trc::event!(
context = "ehlo",
event = "rejected",
mx = &params.hostname,
reason = %status,
);
smtp_client.quit().await;
return status;
}
};*/
}
// MAIL FROM
let time = Instant::now();
smtp_client.timeout = params.conn_strategy.timeout_mail;
let cmd = self.build_mail_from(&capabilities);
match smtp_client.cmd(cmd.as_bytes()).await.and_then(|r| {
if r.is_positive_completion() {
Ok(r)
} else {
Err(ClientError::UnexpectedReply(Box::new(r)))
}
}) {
Ok(response) => {
trc::event!(
Delivery(DeliveryEvent::MailFrom),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
From = self.message.return_path.to_string(),
Code = response.code,
Details = response.message.to_string(),
Elapsed = time.elapsed(),
);
}
Err(err) => {
trc::event!(
Delivery(DeliveryEvent::MailFromRejected),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
CausedBy = from_mail_send_error(&err),
Elapsed = time.elapsed(),
);
smtp_client.quit().await;
statuses.push(DeliveryResult::domain(
Status::from_smtp_error(params.hostname, &cmd, err),
rcpt_idxs,
));
return;
}
}
// RCPT TO
let mut accepted_rcpts = Vec::new();
smtp_client.timeout = params.conn_strategy.timeout_rcpt;
for rcpt_idx in &rcpt_idxs {
let time = Instant::now();
let rcpt = &self.message.recipients[*rcpt_idx];
if matches!(
&rcpt.status,
Status::Completed(_) | Status::PermanentFailure(_)
) {
continue;
}
let cmd = self.build_rcpt_to(rcpt, &capabilities);
match smtp_client.cmd(cmd.as_bytes()).await {
Ok(response) => match response.severity() {
Severity::PositiveCompletion => {
trc::event!(
Delivery(DeliveryEvent::RcptTo),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
To = rcpt.address().to_string(),
Code = response.code,
Details = response.message.to_string(),
Elapsed = time.elapsed(),
);
accepted_rcpts.push((
rcpt,
rcpt_idx,
Status::Completed(HostResponse {
hostname: params.hostname.into(),
response: response.into_box(),
}),
));
}
severity => {
trc::event!(
Delivery(DeliveryEvent::RcptToRejected),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
To = rcpt.address().to_string(),
Code = response.code,
Details = response.message.to_string(),
Elapsed = time.elapsed(),
);
let response = ErrorDetails {
entity: params.hostname.into(),
details: Error::UnexpectedResponse(UnexpectedResponse {
command: cmd.trim().into(),
response: response.into_box(),
}),
};
statuses.push(DeliveryResult::account(
if severity == Severity::PermanentNegativeCompletion {
Status::PermanentFailure(response)
} else {
Status::TemporaryFailure(response)
},
*rcpt_idx,
));
}
},
Err(err) => {
trc::event!(
Delivery(DeliveryEvent::RcptToFailed),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
To = rcpt.address().to_string(),
CausedBy = from_mail_send_error(&err),
Elapsed = time.elapsed(),
);
// Something went wrong, abort.
smtp_client.quit().await;
statuses.push(DeliveryResult::domain(
Status::from_smtp_error(params.hostname, "", err),
rcpt_idxs,
));
return;
}
}
}
// Send message
if !accepted_rcpts.is_empty() {
let time = Instant::now();
let mut bdat_cmd = capabilities.has_capability(EXT_CHUNKING).then(String::new);
if let Err(status) = smtp_client
.send_message(self, rcpt_headers, &mut bdat_cmd, &params)
.await
{
trc::event!(
Delivery(DeliveryEvent::MessageRejected),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
CausedBy = from_error_status(&status),
Elapsed = time.elapsed(),
);
smtp_client.quit().await;
statuses.push(DeliveryResult::domain(status, rcpt_idxs));
return;
}
if params.is_smtp {
// Handle SMTP response
match smtp_client
.read_smtp_data_response(params.hostname, &bdat_cmd)
.await
{
Ok(response) => {
// Mark recipients as delivered
if response.code() == 250 {
for (rcpt, rcpt_idx, status) in accepted_rcpts {
trc::event!(
Delivery(DeliveryEvent::Delivered),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
To = rcpt.address().to_string(),
Code = response.code,
Details = response.message.to_string(),
Elapsed = time.elapsed(),
);
statuses.push(DeliveryResult::account(status, *rcpt_idx));
}
} else {
trc::event!(
Delivery(DeliveryEvent::MessageRejected),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
Code = response.code,
Details = response.message.to_string(),
Elapsed = time.elapsed(),
);
smtp_client.quit().await;
statuses.push(DeliveryResult::domain(
Status::from_smtp_error(
params.hostname,
bdat_cmd.as_deref().unwrap_or("DATA"),
ClientError::UnexpectedReply(Box::new(response)),
),
rcpt_idxs,
));
return;
}
}
Err(status) => {
trc::event!(
Delivery(DeliveryEvent::MessageRejected),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
CausedBy = from_error_status(&status),
Elapsed = time.elapsed(),
);
smtp_client.quit().await;
statuses.push(DeliveryResult::domain(status, rcpt_idxs));
return;
}
}
} else {
// Handle LMTP responses
match smtp_client
.read_lmtp_data_response(params.hostname, accepted_rcpts.len())
.await
{
Ok(responses) => {
for ((rcpt, rcpt_idx, _), response) in
accepted_rcpts.into_iter().zip(responses)
{
let status: Status<HostResponse<Box<str>>, ErrorDetails> =
match response.severity() {
Severity::PositiveCompletion => {
trc::event!(
Delivery(DeliveryEvent::Delivered),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
To = rcpt.address().to_string(),
Code = response.code,
Details = response.message.to_string(),
Elapsed = time.elapsed(),
);
Status::Completed(HostResponse {
hostname: params.hostname.into(),
response,
})
}
severity => {
trc::event!(
Delivery(DeliveryEvent::RcptToRejected),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
To = rcpt.address().to_string(),
Code = response.code,
Details = response.message.to_string(),
Elapsed = time.elapsed(),
);
let response = ErrorDetails {
entity: params.hostname.into(),
details: Error::UnexpectedResponse(
UnexpectedResponse {
command: bdat_cmd
.as_deref()
.unwrap_or("DATA")
.into(),
response,
},
),
};
if severity == Severity::PermanentNegativeCompletion {
Status::PermanentFailure(response)
} else {
Status::TemporaryFailure(response)
}
}
};
statuses.push(DeliveryResult::account(status, *rcpt_idx));
}
}
Err(status) => {
trc::event!(
Delivery(DeliveryEvent::MessageRejected),
SpanId = params.session_id,
Hostname = params.hostname.to_string(),
CausedBy = from_error_status(&status),
Elapsed = time.elapsed(),
);
smtp_client.quit().await;
statuses.push(DeliveryResult::domain(status, rcpt_idxs));
return;
}
}
}
}
smtp_client.quit().await;
}
fn build_mail_from(&self, capabilities: &EhloResponse<String>) -> String {
let mut mail_from = String::with_capacity(self.message.return_path.len() + 60);
let _ = write!(mail_from, "MAIL FROM:<{}>", self.message.return_path);
if capabilities.has_capability(EXT_SIZE) {
let _ = write!(mail_from, " SIZE={}", self.message.size);
}
if self.has_flag(MAIL_REQUIRETLS) & capabilities.has_capability(EXT_REQUIRE_TLS) {
mail_from.push_str(" REQUIRETLS");
}
if self.has_flag(MAIL_SMTPUTF8) & capabilities.has_capability(EXT_SMTP_UTF8) {
mail_from.push_str(" SMTPUTF8");
}
if capabilities.has_capability(EXT_DSN) {
if self.has_flag(MAIL_RET_FULL) {
mail_from.push_str(" RET=FULL");
} else if self.has_flag(MAIL_RET_HDRS) {
mail_from.push_str(" RET=HDRS");
}
if let Some(env_id) = &self.message.env_id {
let _ = write!(mail_from, " ENVID={env_id}");
}
}
mail_from.push_str("\r\n");
mail_from
}
fn build_rcpt_to(&self, rcpt: &Recipient, capabilities: &EhloResponse<String>) -> String {
let mut rcpt_to = String::with_capacity(rcpt.address().len() + 60);
let _ = write!(rcpt_to, "RCPT TO:<{}>", rcpt.address());
if capabilities.has_capability(EXT_DSN) {
if rcpt.has_flag(RCPT_NOTIFY_SUCCESS | RCPT_NOTIFY_FAILURE | RCPT_NOTIFY_DELAY) {
rcpt_to.push_str(" NOTIFY=");
let mut add_comma = if rcpt.has_flag(RCPT_NOTIFY_SUCCESS) {
rcpt_to.push_str("SUCCESS");
true
} else {
false
};
if rcpt.has_flag(RCPT_NOTIFY_DELAY) {
if add_comma {
rcpt_to.push(',');
} else {
add_comma = true;
}
rcpt_to.push_str("DELAY");
}
if rcpt.has_flag(RCPT_NOTIFY_FAILURE) {
if add_comma {
rcpt_to.push(',');
}
rcpt_to.push_str("FAILURE");
}
} else if rcpt.has_flag(RCPT_NOTIFY_NEVER) {
rcpt_to.push_str(" NOTIFY=NEVER");
}
}
rcpt_to.push_str("\r\n");
rcpt_to
}
#[inline(always)]
pub fn has_flag(&self, flag: u64) -> bool {
(self.message.flags & flag) != 0
}
}
impl Recipient {
#[inline(always)]
pub fn has_flag(&self, flag: u64) -> bool {
(self.flags & flag) != 0
}
}
+669
View File
@@ -0,0 +1,669 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::spool::SmtpSpool;
use super::{
Error, ErrorDetails, HostResponse, Message, MessageSource, QueueEnvelope, RCPT_DSN_SENT,
Recipient, Status,
};
use crate::inbound::dkim::DkimSign;
use crate::queue::spool::QueueParams;
use crate::queue::{MessageWrapper, UnexpectedResponse};
use common::Server;
use mail_builder::MessageBuilder;
use mail_builder::headers::HeaderType;
use mail_builder::headers::content_type::ContentType;
use mail_builder::mime::{BodyPart, MimePart, make_boundary};
use mail_parser::DateTime;
use smtp_proto::{
RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, Response,
};
use std::fmt::Write;
use std::future::Future;
use store::write::now;
pub trait SendDsn: Sync + Send {
fn send_dsn(&self, message: &mut MessageWrapper) -> impl Future<Output = ()> + Send;
fn log_dsn(&self, message: &MessageWrapper) -> impl Future<Output = ()> + Send;
}
impl SendDsn for Server {
async fn send_dsn(&self, message: &mut MessageWrapper) {
// Send DSN events
self.log_dsn(message).await;
if !message.message.return_path.is_empty() {
// Build DSN
if let Some(dsn) = message.build_dsn(self).await {
let mut dsn_message = self.new_message("", MessageSource::Dsn, message.span_id);
dsn_message
.expand_and_add_recipient(message.message.return_path.as_ref(), self)
.await;
// Queue DSN
let dkim_signers = self
.eval_signers(
&self.core.smtp.queue.dsn.sign,
&message.message,
message.span_id,
)
.await;
dsn_message
.queue(
QueueParams::new(&dsn, message.span_id, self)
.with_dkim_signers(dkim_signers),
)
.await;
}
} else {
// Handle double bounce
message.handle_double_bounce();
}
// Update next DSN notify times
message.update_next_dsn(self).await;
}
async fn log_dsn(&self, message: &MessageWrapper) {
let now = now();
for rcpt in &message.message.recipients {
if rcpt.has_flag(RCPT_DSN_SENT) {
continue;
}
match &rcpt.status {
Status::Completed(response) => {
trc::event!(
Delivery(trc::DeliveryEvent::DsnSuccess),
SpanId = message.span_id,
To = rcpt.address.clone(),
Hostname = response.hostname.clone(),
Code = response.response.code,
Details = response.response.message.to_string(),
);
}
Status::TemporaryFailure(response) if rcpt.notify.due <= now => {
trc::event!(
Delivery(trc::DeliveryEvent::DsnTempFail),
SpanId = message.span_id,
To = rcpt.address.clone(),
Hostname = response.entity.clone(),
Details = response.details.to_string(),
NextRetry = trc::Value::Timestamp(rcpt.retry.due),
Expires = rcpt
.expiration_time(message.message.created)
.map(trc::Value::Timestamp),
Total = rcpt.retry.inner,
);
}
Status::PermanentFailure(response) => {
trc::event!(
Delivery(trc::DeliveryEvent::DsnPermFail),
SpanId = message.span_id,
To = rcpt.address.clone(),
Hostname = response.entity.clone(),
Details = response.details.to_string(),
Total = rcpt.retry.inner,
);
}
Status::Scheduled if rcpt.notify.due <= now => {
trc::event!(
Delivery(trc::DeliveryEvent::DsnTempFail),
SpanId = message.span_id,
To = rcpt.address.clone(),
Details = "Concurrency limited",
NextRetry = trc::Value::Timestamp(rcpt.retry.due),
Expires = rcpt
.expiration_time(message.message.created)
.map(trc::Value::Timestamp),
Total = rcpt.retry.inner,
);
}
_ => continue,
}
}
}
}
const MAX_HEADER_SIZE: usize = 4096;
impl MessageWrapper {
pub async fn build_dsn(&mut self, server: &Server) -> Option<Vec<u8>> {
let config = &server.core.smtp.queue;
let now = now();
let mut txt_success = String::new();
let mut txt_delay = String::new();
let mut txt_failed = String::new();
let mut dsn = String::new();
for rcpt in &mut self.message.recipients {
if rcpt.has_flag(RCPT_DSN_SENT | RCPT_NOTIFY_NEVER) {
continue;
}
match &rcpt.status {
Status::Completed(response) => {
rcpt.flags |= RCPT_DSN_SENT;
if !rcpt.has_flag(RCPT_NOTIFY_SUCCESS) {
continue;
}
rcpt.write_dsn(&mut dsn);
rcpt.status.write_dsn(&mut dsn);
response.write_dsn_text(&rcpt.address, &mut txt_success);
}
Status::TemporaryFailure(response)
if rcpt.notify.due <= now && rcpt.has_flag(RCPT_NOTIFY_DELAY) =>
{
rcpt.write_dsn(&mut dsn);
rcpt.status.write_dsn(&mut dsn);
rcpt.write_dsn_will_retry_until(self.message.created, &mut dsn);
response.write_dsn_text(&rcpt.address, &mut txt_delay);
}
Status::PermanentFailure(response) => {
rcpt.flags |= RCPT_DSN_SENT;
if !rcpt.has_flag(RCPT_NOTIFY_FAILURE) {
continue;
}
rcpt.write_dsn(&mut dsn);
rcpt.status.write_dsn(&mut dsn);
response.write_dsn_text(&rcpt.address, &mut txt_failed);
}
Status::Scheduled if rcpt.notify.due <= now && rcpt.has_flag(RCPT_NOTIFY_DELAY) => {
// This case should not happen under normal circumstances
rcpt.write_dsn(&mut dsn);
rcpt.status.write_dsn(&mut dsn);
rcpt.write_dsn_will_retry_until(self.message.created, &mut dsn);
ErrorDetails {
entity: "localhost".into(),
details: Error::ConcurrencyLimited,
}
.write_dsn_text(&rcpt.address, &mut txt_delay);
}
_ => continue,
}
dsn.push_str("\r\n");
}
let txt_len = txt_success.len() + txt_delay.len() + txt_failed.len();
if txt_len == 0 {
return None;
}
let has_success = !txt_success.is_empty();
let has_delay = !txt_delay.is_empty();
let has_failure = !txt_failed.is_empty();
let mut txt = String::with_capacity(txt_len + 128);
let (subject, is_mixed) = if has_success && !has_delay && !has_failure {
txt.push_str(
"Your message has been successfully delivered to the following recipients:\r\n\r\n",
);
("Successfully delivered message", false)
} else if has_delay && !has_success && !has_failure {
txt.push_str("There was a temporary problem delivering your message to the following recipients:\r\n\r\n");
("Warning: Delay in message delivery", false)
} else if has_failure && !has_success && !has_delay {
txt.push_str(
"Your message could not be delivered to the following recipients:\r\n\r\n",
);
("Failed to deliver message", false)
} else if has_success {
txt.push_str("Your message has been partially delivered:\r\n\r\n");
("Partially delivered message", true)
} else {
txt.push_str("Your message could not be delivered to some recipients:\r\n\r\n");
(
"Warning: Temporary and permanent failures during message delivery",
true,
)
};
if has_success {
if is_mixed {
txt.push_str(
" ----- Delivery to the following addresses was successful -----\r\n",
);
}
txt.push_str(&txt_success);
txt.push_str("\r\n");
}
if has_delay {
if is_mixed {
txt.push_str(
" ----- There was a temporary problem delivering to these addresses -----\r\n",
);
}
txt.push_str(&txt_delay);
txt.push_str("\r\n");
}
if has_failure {
if is_mixed {
txt.push_str(" ----- Delivery to the following addresses failed -----\r\n");
}
txt.push_str(&txt_failed);
txt.push_str("\r\n");
}
// Obtain hostname and sender addresses
let from_name = server
.eval_if(&config.dsn.name, &self.message, self.span_id)
.await
.unwrap_or_else(|| String::from("Mail Delivery Subsystem"));
let from_addr = server
.eval_if(&config.dsn.address, &self.message, self.span_id)
.await
.unwrap_or_else(|| String::from("MAILER-DAEMON@localhost"));
let reporting_mta = server
.eval_if(
&server.core.smtp.report.submitter,
&self.message,
self.span_id,
)
.await
.unwrap_or_else(|| String::from("localhost"));
// Prepare DSN
let mut dsn_header = String::with_capacity(dsn.len() + 128);
self.message
.write_dsn_headers(&mut dsn_header, &reporting_mta);
let dsn = dsn_header + dsn.as_str();
// Fetch up to MAX_HEADER_SIZE bytes of message headers
let headers = match server
.blob_store()
.get_blob(self.message.blob_hash.as_slice(), 0..MAX_HEADER_SIZE)
.await
{
Ok(Some(mut buf)) => {
let mut prev_ch = 0;
let mut last_lf = buf.len();
for (pos, &ch) in buf.iter().enumerate() {
match ch {
b'\n' => {
last_lf = pos + 1;
if prev_ch != b'\n' {
prev_ch = ch;
} else {
break;
}
}
b'\r' => (),
0 => break,
_ => {
prev_ch = ch;
}
}
}
if last_lf < MAX_HEADER_SIZE {
buf.truncate(last_lf);
}
String::from_utf8(buf).unwrap_or_default()
}
Ok(None) => {
trc::event!(
Queue(trc::QueueEvent::BlobNotFound),
SpanId = self.span_id,
BlobId = self.message.blob_hash.to_hex(),
CausedBy = trc::location!()
);
String::new()
}
Err(err) => {
trc::error!(
err.span_id(self.span_id)
.details("Failed to fetch blobId")
.caused_by(trc::location!())
);
String::new()
}
};
// Build message
MessageBuilder::new()
.from((from_name.as_str(), from_addr.as_str()))
.header(
"To",
HeaderType::Text(self.message.return_path.as_ref().into()),
)
.header("Auto-Submitted", HeaderType::Text("auto-generated".into()))
.message_id(format!("{}@{}", make_boundary("."), reporting_mta))
.subject(subject)
.body(MimePart::new(
ContentType::new("multipart/report").attribute("report-type", "delivery-status"),
BodyPart::Multipart(vec![
MimePart::new(ContentType::new("text/plain"), BodyPart::Text(txt.into())),
MimePart::new(
ContentType::new("message/delivery-status"),
BodyPart::Text(dsn.into()),
),
MimePart::new(
ContentType::new("message/rfc822"),
BodyPart::Text(headers.into()),
),
]),
))
.write_to_vec()
.unwrap_or_default()
.into()
}
pub async fn update_next_dsn(&mut self, server: &Server) {
let now = now();
let mut notify_changes = Vec::new();
for (rcpt_idx, rcpt) in self.message.recipients.iter().enumerate() {
if matches!(
&rcpt.status,
Status::TemporaryFailure(_) | Status::Scheduled
) && rcpt.notify.due <= now
{
let envelope = QueueEnvelope::new(&self.message, rcpt);
let queue_id = server
.eval_if::<String, _>(&server.core.smtp.queue.queue, &envelope, self.span_id)
.await
.unwrap_or_else(|| "default".to_string());
let queue = server.get_queue_or_default(&queue_id, self.span_id);
if let Some(next_notify) =
queue.notify.get((rcpt.notify.inner + 1) as usize).copied()
{
notify_changes.push((rcpt_idx, 1, now + next_notify));
} else {
notify_changes.push((rcpt_idx, 0, u64::MAX));
}
}
}
for (rcpt_idx, inner, due) in notify_changes {
let rcpt = &mut self.message.recipients[rcpt_idx];
rcpt.notify.inner += inner;
rcpt.notify.due = due;
}
}
fn handle_double_bounce(&mut self) {
let mut is_double_bounce = Vec::with_capacity(0);
let now = now();
for rcpt in &mut self.message.recipients {
if !rcpt.has_flag(RCPT_DSN_SENT | RCPT_NOTIFY_NEVER)
&& let Status::PermanentFailure(err) = &rcpt.status
{
rcpt.flags |= RCPT_DSN_SENT;
let mut dsn = String::new();
err.write_dsn_text(&rcpt.address, &mut dsn);
is_double_bounce.push(dsn);
}
if rcpt.notify.due <= now {
rcpt.notify.due = rcpt
.expiration_time(self.message.created)
.map(|d| d + 10)
.unwrap_or(u64::MAX);
}
}
if !is_double_bounce.is_empty() {
trc::event!(
Delivery(trc::DeliveryEvent::DoubleBounce),
SpanId = self.span_id,
To = is_double_bounce
);
}
}
}
impl HostResponse<Box<str>> {
fn write_dsn_text(&self, addr: &str, dsn: &mut String) {
let _ = write!(
dsn,
"<{}> (delivered to '{}' with code {} ({}.{}.{}) '",
addr,
self.hostname,
self.response.code,
self.response.esc[0],
self.response.esc[1],
self.response.esc[2]
);
self.response.write_response(dsn);
dsn.push_str("')\r\n");
}
}
impl UnexpectedResponse {
fn write_dsn_text(&self, host: &str, addr: &str, dsn: &mut String) {
let _ = write!(dsn, "<{addr}> (host '{host}' rejected ");
if !self.command.is_empty() {
let _ = write!(dsn, "command '{}'", self.command);
} else {
dsn.push_str("transaction");
}
let _ = write!(
dsn,
" with code {} ({}.{}.{}) '",
self.response.code, self.response.esc[0], self.response.esc[1], self.response.esc[2]
);
self.response.write_response(dsn);
dsn.push_str("')\r\n");
}
}
impl ErrorDetails {
fn write_dsn_text(&self, addr: &str, dsn: &mut String) {
let entity = self.entity.as_ref();
match &self.details {
Error::UnexpectedResponse(response) => {
response.write_dsn_text(entity, addr, dsn);
}
Error::DnsError(err) => {
let _ = write!(dsn, "<{addr}> (failed to lookup '{entity}': {err})\r\n",);
}
Error::ConnectionError(details) => {
let _ = write!(
dsn,
"<{addr}> (connection to '{entity}' failed: {details})\r\n",
);
}
Error::TlsError(details) => {
let _ = write!(dsn, "<{addr}> (TLS error from '{entity}': {details})\r\n",);
}
Error::DaneError(details) => {
let _ = write!(
dsn,
"<{addr}> (DANE failed to authenticate '{entity}': {details})\r\n",
);
}
Error::MtaStsError(details) => {
let _ = write!(
dsn,
"<{addr}> (MTA-STS failed to authenticate '{entity}': {details})\r\n",
);
}
Error::RateLimited => {
let _ = write!(dsn, "<{addr}> (rate limited)\r\n");
}
Error::ConcurrencyLimited => {
let _ = write!(
dsn,
"<{addr}> (too many concurrent connections to remote server)\r\n",
);
}
Error::Io(err) => {
let _ = write!(dsn, "<{addr}> (queue error: {err})\r\n");
}
}
}
}
impl Message {
fn write_dsn_headers(&self, dsn: &mut String, reporting_mta: &str) {
let _ = write!(dsn, "Reporting-MTA: dns;{reporting_mta}\r\n");
dsn.push_str("Arrival-Date: ");
dsn.push_str(&DateTime::from_timestamp(self.created as i64).to_rfc822());
dsn.push_str("\r\n");
if let Some(env_id) = &self.env_id {
let _ = write!(dsn, "Original-Envelope-Id: {env_id}\r\n");
}
dsn.push_str("\r\n");
}
}
impl Recipient {
fn write_dsn(&self, dsn: &mut String) {
if let Some(orcpt) = &self.orcpt {
let _ = write!(dsn, "Original-Recipient: rfc822;{orcpt}\r\n");
}
let _ = write!(dsn, "Final-Recipient: rfc822;{}\r\n", self.address);
}
fn write_dsn_will_retry_until(&self, created: u64, dsn: &mut String) {
if let Some(expires) = self.expiration_time(created)
&& expires > now()
{
dsn.push_str("Will-Retry-Until: ");
dsn.push_str(&DateTime::from_timestamp(expires as i64).to_rfc822());
dsn.push_str("\r\n");
}
}
}
impl<T, E> Status<T, E> {
pub fn into_permanent(self) -> Self {
match self {
Status::TemporaryFailure(v) => Status::PermanentFailure(v),
v => v,
}
}
pub fn into_temporary(self) -> Self {
match self {
Status::PermanentFailure(err) => Status::TemporaryFailure(err),
other => other,
}
}
pub fn is_permanent(&self) -> bool {
matches!(self, Status::PermanentFailure(_))
}
fn write_dsn_action(&self, dsn: &mut String) {
dsn.push_str("Action: ");
dsn.push_str(match self {
Status::Completed(_) => "delivered",
Status::PermanentFailure(_) => "failed",
Status::TemporaryFailure(_) | Status::Scheduled => "delayed",
});
dsn.push_str("\r\n");
}
}
impl Status<HostResponse<Box<str>>, ErrorDetails> {
fn write_dsn(&self, dsn: &mut String) {
self.write_dsn_action(dsn);
self.write_dsn_status(dsn);
self.write_dsn_diagnostic(dsn);
self.write_dsn_remote_mta(dsn);
}
fn write_dsn_status(&self, dsn: &mut String) {
dsn.push_str("Status: ");
match self {
Status::Completed(response) => {
response.response.write_dsn_status(dsn);
}
Status::TemporaryFailure(err) | Status::PermanentFailure(err) => {
if let Error::UnexpectedResponse(response) = &err.details {
response.response.write_dsn_status(dsn);
} else {
dsn.push_str(if matches!(self, Status::PermanentFailure(_)) {
"5.0.0"
} else {
"4.0.0"
});
}
}
Status::Scheduled => {
dsn.push_str("4.0.0");
}
}
dsn.push_str("\r\n");
}
fn write_dsn_remote_mta(&self, dsn: &mut String) {
match self {
Status::Completed(response) => {
dsn.push_str("Remote-MTA: dns;");
dsn.push_str(&response.hostname);
dsn.push_str("\r\n");
}
Status::TemporaryFailure(err) | Status::PermanentFailure(err) => match &err.details {
Error::UnexpectedResponse(_)
| Error::ConnectionError(_)
| Error::TlsError(_)
| Error::DaneError(_) => {
dsn.push_str("Remote-MTA: dns;");
dsn.push_str(&err.entity);
dsn.push_str("\r\n");
}
_ => (),
},
Status::Scheduled => (),
}
}
fn write_dsn_diagnostic(&self, dsn: &mut String) {
if let Status::PermanentFailure(err) | Status::TemporaryFailure(err) = self
&& let Error::UnexpectedResponse(response) = &err.details
{
response.response.write_dsn_diagnostic(dsn);
}
}
}
impl WriteDsn for Response<Box<str>> {
fn write_dsn_status(&self, dsn: &mut String) {
if self.esc[0] > 0 {
let _ = write!(dsn, "{}.{}.{}", self.esc[0], self.esc[1], self.esc[2]);
} else {
let _ = write!(
dsn,
"{}.{}.{}",
self.code / 100,
(self.code / 10) % 10,
self.code % 10
);
}
}
fn write_dsn_diagnostic(&self, dsn: &mut String) {
let _ = write!(dsn, "Diagnostic-Code: smtp;{} ", self.code);
self.write_response(dsn);
dsn.push_str("\r\n");
}
fn write_response(&self, dsn: &mut String) {
for ch in self.message.chars() {
if ch != '\n' && ch != '\r' {
dsn.push(ch);
}
}
}
}
trait WriteDsn {
fn write_dsn_status(&self, dsn: &mut String);
fn write_dsn_diagnostic(&self, dsn: &mut String);
fn write_response(&self, dsn: &mut String);
}
+501
View File
@@ -0,0 +1,501 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{Message, QueueId, Status, spool::SmtpSpool};
use crate::queue::{
Recipient,
spool::{INFINITE_LOCK, LOCK_EXPIRY, QUEUE_REFRESH},
};
use ahash::AHashMap;
use common::{
BuildServer, Inner,
config::smtp::queue::{QueueExpiry, QueueName},
ipc::{QueueEvent, QueueEventStatus},
};
use rand::{RngExt, seq::SliceRandom};
use std::{
collections::hash_map::Entry,
sync::{Arc, atomic::Ordering},
time::{Duration, Instant},
};
use store::write::now;
use tokio::sync::mpsc;
pub struct Queue {
pub core: Arc<Inner>,
pub locked: AHashMap<(QueueId, QueueName), LockedMessage>,
pub locked_revision: u64,
pub stats: AHashMap<QueueName, QueueStats>,
pub next_refresh: Instant,
pub rx: mpsc::Receiver<QueueEvent>,
pub is_paused: bool,
pub scan_from: u64,
pub scan_ceiling: u64,
pub has_pending_work: bool,
pub pending_refresh: bool,
pub urgent_refresh: bool,
pub last_scan: Instant,
pub last_full_scan: Instant,
}
#[derive(Debug)]
pub struct QueueStats {
pub in_flight: usize,
pub max_in_flight: usize,
pub budget: usize,
pub last_warning: Instant,
}
#[derive(Debug)]
pub struct LockedMessage {
pub expires: u64,
pub revision: u64,
pub due: u64,
}
impl SpawnQueue for mpsc::Receiver<QueueEvent> {
fn spawn(self, core: Arc<Inner>) {
tokio::spawn(async move {
Queue::new(core, self).start().await;
});
}
}
const BACK_PRESSURE_WARN_INTERVAL: Duration = Duration::from_secs(60);
const MIN_SCAN_INTERVAL: Duration = Duration::from_millis(100);
const FULL_SCAN_INTERVAL: Duration = Duration::from_secs(QUEUE_REFRESH / 2);
impl Queue {
pub fn new(core: Arc<Inner>, rx: mpsc::Receiver<QueueEvent>) -> Self {
let now = Instant::now();
Queue {
core,
locked: AHashMap::with_capacity(128),
locked_revision: 0,
stats: AHashMap::new(),
next_refresh: now + Duration::from_secs(1),
is_paused: false,
rx,
scan_from: 0,
scan_ceiling: u64::MAX,
has_pending_work: false,
pending_refresh: false,
urgent_refresh: false,
last_scan: now.checked_sub(MIN_SCAN_INTERVAL).unwrap_or(now),
last_full_scan: now,
}
}
pub async fn start(&mut self) {
trc::event!(Queue(trc::QueueEvent::Started));
loop {
let mut refresh_queue;
match tokio::time::timeout(
self.next_refresh.duration_since(Instant::now()),
self.rx.recv(),
)
.await
{
Ok(Some(event)) => {
refresh_queue = self.handle_event(event).await;
while let Ok(event) = self.rx.try_recv() {
refresh_queue = self.handle_event(event).await || refresh_queue;
}
}
Err(_) => {
refresh_queue = true;
self.urgent_refresh = true;
}
Ok(None) => {
break;
}
};
if self.is_paused {
self.next_refresh = Instant::now() + Duration::from_secs(86400);
continue;
}
self.pending_refresh |= refresh_queue;
if !self.pending_refresh && self.next_refresh > Instant::now() {
continue;
}
// Coalesce bursts of worker notifications into a single scan
let scan_at = self.last_scan + MIN_SCAN_INTERVAL;
if !self.urgent_refresh && scan_at > Instant::now() {
self.next_refresh = scan_at;
continue;
}
if self.scan_from != 0 && self.last_full_scan.elapsed() >= FULL_SCAN_INTERVAL {
self.scan_from = 0;
}
if self.scan_from == 0 {
self.last_full_scan = Instant::now();
}
let scan_floor = self.scan_from;
self.pending_refresh = false;
self.urgent_refresh = false;
// Process queue events
let server = self.core.build_server();
let mut queue_events = server.next_event(self).await;
self.last_scan = Instant::now();
if queue_events.messages.len() > 3 {
queue_events.messages.shuffle(&mut rand::rng());
}
// A truncated scan left events behind
let now = now();
self.has_pending_work = self.scan_ceiling != u64::MAX;
for queue_event in &queue_events.messages {
// A message may hold more than one event key, dispatch it only once
if self
.locked
.get(&(queue_event.queue_id, queue_event.queue_name))
.is_some_and(|locked| locked.expires > now)
{
continue;
}
// Fetch queue stats
let stats = match self.stats.get_mut(&queue_event.queue_name) {
Some(stats) => stats,
None => {
let queue_config =
server.get_virtual_queue_or_default(&queue_event.queue_name);
self.stats.insert(
queue_event.queue_name,
QueueStats::new(queue_config.threads),
);
self.stats.get_mut(&queue_event.queue_name).unwrap()
}
};
// Enforce concurrency limits
if stats.has_capacity() {
// Deliver message
stats.in_flight += 1;
self.locked.insert(
(queue_event.queue_id, queue_event.queue_name),
LockedMessage {
expires: now + INFINITE_LOCK,
revision: self.locked_revision,
due: queue_event.due,
},
);
queue_event.try_deliver(server.clone());
} else {
if stats.last_warning.elapsed() >= BACK_PRESSURE_WARN_INTERVAL {
stats.last_warning = Instant::now();
trc::event!(
Queue(trc::QueueEvent::BackPressure),
Reason = "Processing capacity for this queue exceeded.",
QueueName = queue_event.queue_name.to_string(),
Limit = stats.max_in_flight,
);
}
self.has_pending_work = true;
if queue_event.due < self.scan_from {
self.scan_from = queue_event.due;
}
}
}
// Remove expired locks, revisiting any event they were holding back
let scan_ceiling = self.scan_ceiling;
let mut dropped_due = u64::MAX;
self.locked.retain(|_, locked| {
let keep = locked.expires > now
&& (locked.revision == self.locked_revision
|| locked.due < scan_floor
|| locked.due >= scan_ceiling);
if !keep && locked.due < dropped_due {
dropped_due = locked.due;
}
keep
});
// Do not wait for the next scheduled event while there is work left over
let mut next_refresh = queue_events.next_refresh.saturating_sub(now);
if self.has_pending_work {
next_refresh = std::cmp::min(next_refresh, FULL_SCAN_INTERVAL.as_secs());
}
let mut next_refresh = Instant::now() + Duration::from_secs(next_refresh);
// A released lock uncovered an event below the floor that no scan can see
if dropped_due < self.scan_from {
self.scan_from = dropped_due;
self.has_pending_work = true;
self.pending_refresh = true;
let scan_at = self.last_scan + MIN_SCAN_INTERVAL;
if scan_at < next_refresh {
next_refresh = scan_at;
}
}
self.next_refresh = next_refresh;
}
}
async fn handle_event(&mut self, event: QueueEvent) -> bool {
match event {
QueueEvent::WorkerDone {
queue_id,
queue_name,
status,
} => {
let has_capacity = match self.stats.get_mut(&queue_name) {
Some(queue_stats) => {
queue_stats.in_flight = queue_stats.in_flight.saturating_sub(1);
queue_stats.has_capacity()
}
None => true,
};
match status {
QueueEventStatus::Completed => {
self.core.ipc.task_tx.notify_one();
self.locked.remove(&(queue_id, queue_name));
!self.locked.is_empty() || !has_capacity || self.has_pending_work
}
QueueEventStatus::Locked => {
let expires = LOCK_EXPIRY + rand::rng().random_range(5..10);
let due_in = Instant::now() + Duration::from_secs(expires);
if due_in < self.next_refresh {
self.next_refresh = due_in;
}
// The event was not delivered, so it has to be visited again
// once the remote lock expires.
let expires = now() + expires;
let due = match self.locked.entry((queue_id, queue_name)) {
Entry::Occupied(mut entry) => {
let locked = entry.get_mut();
locked.expires = expires;
locked.revision = self.locked_revision;
locked.due
}
Entry::Vacant(entry) => {
entry.insert(LockedMessage {
expires,
revision: self.locked_revision,
due: 0,
});
0
}
};
if due < self.scan_from {
self.scan_from = due;
}
self.locked.len() > 1 || !has_capacity || self.has_pending_work
}
QueueEventStatus::Deferred => {
self.locked.remove(&(queue_id, queue_name));
self.scan_from = 0;
true
}
}
}
QueueEvent::Refresh => {
self.scan_from = 0;
self.urgent_refresh = true;
true
}
QueueEvent::Paused(paused) => {
self.core
.data
.queue_status
.store(!paused, Ordering::Relaxed);
self.is_paused = paused;
self.scan_from = 0;
self.urgent_refresh = !paused;
!paused
}
QueueEvent::ReloadSettings => {
let server = self.core.build_server();
let virtual_queues = &server.core.smtp.queue.virtual_queues;
for (name, settings) in virtual_queues {
if let Some(stats) = self.stats.get_mut(name) {
stats.max_in_flight = settings.threads;
} else {
self.stats.insert(*name, QueueStats::new(settings.threads));
}
}
self.stats
.retain(|name, stats| stats.in_flight > 0 || virtual_queues.contains_key(name));
self.scan_from = 0;
false
}
QueueEvent::Stop => {
self.rx.close();
self.is_paused = true;
false
}
}
}
}
impl Message {
pub fn next_event(&self, queue: Option<QueueName>) -> Option<u64> {
let mut next_event = None;
for rcpt in &self.recipients {
if matches!(rcpt.status, Status::Scheduled | Status::TemporaryFailure(_))
&& queue.is_none_or(|q| rcpt.queue == q)
{
let mut earlier_event = std::cmp::min(rcpt.retry.due, rcpt.notify.due);
if let Some(expires) = rcpt.expiration_time(self.created) {
earlier_event = std::cmp::min(earlier_event, expires);
}
if let Some(next_event) = &mut next_event {
if earlier_event < *next_event {
*next_event = earlier_event;
}
} else {
next_event = Some(earlier_event);
}
}
}
next_event
}
pub fn next_delivery_event(&self, queue: Option<QueueName>) -> Option<u64> {
let mut next_delivery = None;
for rcpt in self.recipients.iter().filter(|rcpt| {
matches!(rcpt.status, Status::Scheduled | Status::TemporaryFailure(_))
&& queue.is_none_or(|q| rcpt.queue == q)
}) {
if let Some(next_delivery) = &mut next_delivery {
if rcpt.retry.due < *next_delivery {
*next_delivery = rcpt.retry.due;
}
} else {
next_delivery = Some(rcpt.retry.due);
}
}
next_delivery
}
pub fn next_dsn(&self, queue: Option<QueueName>) -> Option<u64> {
let mut next_dsn = None;
for rcpt in self.recipients.iter().filter(|rcpt| {
matches!(rcpt.status, Status::Scheduled | Status::TemporaryFailure(_))
&& queue.is_none_or(|q| rcpt.queue == q)
}) {
if let Some(next_dsn) = &mut next_dsn {
if rcpt.notify.due < *next_dsn {
*next_dsn = rcpt.notify.due;
}
} else {
next_dsn = Some(rcpt.notify.due);
}
}
next_dsn
}
pub fn expires(&self, queue: Option<QueueName>) -> Option<u64> {
let mut expires = None;
for rcpt in self.recipients.iter().filter(|d| {
matches!(d.status, Status::Scheduled | Status::TemporaryFailure(_))
&& queue.is_none_or(|q| d.queue == q)
}) {
if let Some(rcpt_expires) = rcpt.expiration_time(self.created) {
if let Some(expires) = &mut expires {
if rcpt_expires > *expires {
*expires = rcpt_expires;
}
} else {
expires = Some(rcpt_expires)
}
}
}
expires
}
pub fn next_events(&self) -> AHashMap<QueueName, u64> {
let mut next_events = AHashMap::new();
for rcpt in &self.recipients {
if matches!(rcpt.status, Status::Scheduled | Status::TemporaryFailure(_)) {
let mut earlier_event = std::cmp::min(rcpt.retry.due, rcpt.notify.due);
if let Some(expires) = rcpt.expiration_time(self.created) {
earlier_event = std::cmp::min(earlier_event, expires);
}
match next_events.entry(rcpt.queue) {
Entry::Occupied(mut entry) => {
let entry = entry.get_mut();
if earlier_event < *entry {
*entry = earlier_event;
}
}
Entry::Vacant(entry) => {
entry.insert(earlier_event);
}
}
}
}
next_events
}
}
impl Recipient {
pub fn expiration_time(&self, created: u64) -> Option<u64> {
match self.expires {
QueueExpiry::Ttl(time) => Some(created + time),
QueueExpiry::Attempts(_) => None,
}
}
pub fn is_expired(&self, created: u64, now: u64) -> bool {
match self.expires {
QueueExpiry::Ttl(time) => created + time <= now,
QueueExpiry::Attempts(count) => self.retry.inner >= count,
}
}
}
pub trait SpawnQueue {
fn spawn(self, core: Arc<Inner>);
}
impl QueueStats {
pub(crate) fn new(max_in_flight: usize) -> Self {
QueueStats {
in_flight: 0,
max_in_flight,
budget: 0,
last_warning: Instant::now()
.checked_sub(BACK_PRESSURE_WARN_INTERVAL)
.unwrap_or_else(Instant::now),
}
}
#[inline]
pub fn has_capacity(&self) -> bool {
self.in_flight < self.max_in_flight
}
}
+666
View File
@@ -0,0 +1,666 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{
config::smtp::queue::{QueueExpiry, QueueName},
expr::{self, functions::ResolveVariable, *},
};
use compact_str::ToCompactString;
use registry::schema::enums::ExpressionVariable;
use smtp_proto::Response;
use std::{
fmt::Display,
net::{IpAddr, Ipv4Addr},
time::{Duration, Instant, SystemTime},
};
use store::write::now;
use types::blob_hash::BlobHash;
use utils::DomainPart;
pub mod dsn;
pub mod manager;
pub mod quota;
pub mod spool;
pub mod throttle;
pub type QueueId = u64;
#[derive(Debug, Clone, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, serde::Deserialize)]
pub struct Schedule<T> {
pub due: u64,
pub inner: T,
}
#[derive(Debug, Clone, Copy)]
pub struct QueuedMessage {
pub due: u64,
pub queue_id: QueueId,
pub queue_name: QueueName,
}
#[derive(Debug, Clone, Copy)]
pub enum MessageSource {
Authenticated,
Unauthenticated { dmarc_pass: bool },
Dsn,
Report,
Autogenerated,
}
impl MessageSource {
pub fn flags(&self) -> u64 {
match self {
MessageSource::Authenticated => FROM_AUTHENTICATED,
MessageSource::Unauthenticated { dmarc_pass: true } => FROM_UNAUTHENTICATED_DMARC,
MessageSource::Unauthenticated { dmarc_pass: false } => FROM_UNAUTHENTICATED,
MessageSource::Dsn => FROM_DSN,
MessageSource::Report => FROM_REPORT,
MessageSource::Autogenerated => FROM_AUTOGENERATED,
}
}
}
#[derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, Debug, Clone, PartialEq, Eq)]
pub struct Message {
pub created: u64,
pub blob_hash: BlobHash,
pub return_path: Box<str>,
pub recipients: Vec<Recipient>,
pub received_from_ip: IpAddr,
pub received_via_port: u16,
pub flags: u64,
pub env_id: Option<Box<str>>,
pub priority: i16,
pub size: u64,
pub metadata: Box<[Metadata]>,
}
impl Message {
pub fn queued_event(&self) -> trc::QueueEvent {
if (self.flags & FROM_AUTHENTICATED) != 0 {
trc::QueueEvent::AuthenticatedMessageQueued
} else if (self.flags & FROM_DSN) != 0 {
trc::QueueEvent::DsnQueued
} else if (self.flags & FROM_REPORT) != 0 {
trc::QueueEvent::ReportQueued
} else if (self.flags & FROM_AUTOGENERATED) != 0 {
trc::QueueEvent::AutogeneratedQueued
} else {
trc::QueueEvent::MessageQueued
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MessageWrapper {
pub queue_id: QueueId,
pub queue_name: QueueName,
pub is_multi_queue: bool,
pub span_id: u64,
pub message: Message,
}
#[derive(
rkyv::Serialize,
rkyv::Deserialize,
rkyv::Archive,
Debug,
Clone,
PartialEq,
Eq,
serde::Deserialize,
)]
pub enum Metadata {
QueueSize { key: Box<[u8]>, id: u64 },
QueueCount { key: Box<[u8]>, id: u64 },
Headers { value: Box<[u8]>, id: u64 },
}
#[derive(
rkyv::Serialize,
rkyv::Deserialize,
rkyv::Archive,
Debug,
Clone,
PartialEq,
Eq,
serde::Deserialize,
)]
pub struct Recipient {
pub address: Box<str>,
pub retry: Schedule<u32>,
pub notify: Schedule<u32>,
pub expires: QueueExpiry,
pub queue: QueueName,
pub status: Status<HostResponse<Box<str>>, ErrorDetails>,
pub flags: u64,
pub orcpt: Option<Box<str>>,
}
pub const FROM_AUTHENTICATED: u64 = 1 << 32;
pub const FROM_UNAUTHENTICATED: u64 = 1 << 33;
pub const FROM_UNAUTHENTICATED_DMARC: u64 = 1 << 34;
pub const FROM_DSN: u64 = 1 << 35;
pub const FROM_REPORT: u64 = 1 << 36;
pub const FROM_AUTOGENERATED: u64 = 1 << 37;
pub const RCPT_DSN_SENT: u64 = 1 << 32;
pub const RCPT_SPAM_SHIFT: u64 = 56;
pub const RCPT_SPAM_MASK: u64 = 0xff << RCPT_SPAM_SHIFT;
pub const fn rcpt_spam_flag(percentage: u8) -> u64 {
(percentage as u64 + 1) << RCPT_SPAM_SHIFT
}
pub const fn rcpt_spam_percentage(flags: u64) -> Option<u8> {
match (flags >> RCPT_SPAM_SHIFT) as u8 {
0 => None,
percentage => Some(percentage - 1),
}
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
rkyv::Serialize,
rkyv::Deserialize,
rkyv::Archive,
serde::Serialize,
serde::Deserialize,
)]
pub enum Status<T, E> {
#[serde(rename = "scheduled")]
Scheduled,
#[serde(rename = "completed")]
Completed(T),
#[serde(rename = "temp_fail")]
TemporaryFailure(E),
#[serde(rename = "perm_fail")]
PermanentFailure(E),
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
rkyv::Serialize,
rkyv::Deserialize,
rkyv::Archive,
serde::Deserialize,
)]
pub struct HostResponse<T> {
pub hostname: T,
pub response: Response<Box<str>>,
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
rkyv::Serialize,
rkyv::Deserialize,
rkyv::Archive,
serde::Deserialize,
Default,
)]
pub enum Error {
DnsError(Box<str>),
UnexpectedResponse(UnexpectedResponse),
ConnectionError(Box<str>),
TlsError(Box<str>),
DaneError(Box<str>),
MtaStsError(Box<str>),
RateLimited,
#[default]
ConcurrencyLimited,
Io(Box<str>),
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
rkyv::Serialize,
rkyv::Deserialize,
rkyv::Archive,
serde::Deserialize,
)]
pub struct UnexpectedResponse {
pub command: Box<str>,
pub response: Response<Box<str>>,
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
rkyv::Serialize,
rkyv::Deserialize,
rkyv::Archive,
Default,
serde::Deserialize,
)]
pub struct ErrorDetails {
pub entity: Box<str>,
pub details: Error,
}
impl<T> Ord for Schedule<T> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
other.due.cmp(&self.due)
}
}
impl<T> PartialOrd for Schedule<T> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<T> PartialEq for Schedule<T> {
fn eq(&self, other: &Self) -> bool {
self.due == other.due
}
}
impl<T> Eq for Schedule<T> {}
impl<T: Default> Schedule<T> {
pub fn now() -> Self {
Schedule {
due: now(),
inner: T::default(),
}
}
pub fn later(duration: u64) -> Self {
Schedule {
due: now() + duration,
inner: T::default(),
}
}
}
pub struct QueueEnvelope<'x> {
pub message: &'x Message,
pub domain: &'x str,
pub mx: &'x str,
pub rcpt: &'x Recipient,
pub remote_ip: IpAddr,
pub local_ip: IpAddr,
}
impl<'x> QueueEnvelope<'x> {
pub fn new(message: &'x Message, rcpt: &'x Recipient) -> Self {
Self {
message,
domain: rcpt.address.domain_part(),
rcpt,
mx: "",
remote_ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
local_ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
}
}
}
impl<'x> ResolveVariable for QueueEnvelope<'x> {
fn resolve_variable(&self, variable: ExpressionVariable) -> expr::Variable<'x> {
match variable {
ExpressionVariable::Sender => self.message.return_path.as_ref().into(),
ExpressionVariable::SenderDomain => self.message.return_path.domain_part().into(),
ExpressionVariable::RcptDomain => self.domain.into(),
ExpressionVariable::Rcpt => self.rcpt.address.as_ref().into(),
ExpressionVariable::Recipients => self
.message
.recipients
.iter()
.map(|r| Variable::from(r.address.as_ref()))
.collect::<Vec<_>>()
.into(),
ExpressionVariable::RetryNum => self.rcpt.retry.inner.into(),
ExpressionVariable::NotifyNum => self.rcpt.notify.inner.into(),
ExpressionVariable::ExpiresIn => match &self.rcpt.expires {
QueueExpiry::Ttl(time) => (*time + self.message.created).saturating_sub(now()),
QueueExpiry::Attempts(count) => {
(count.saturating_sub(self.rcpt.retry.inner)) as u64
}
}
.into(),
ExpressionVariable::LastStatus => self.rcpt.status.to_compact_string().into(),
ExpressionVariable::LastError => match &self.rcpt.status {
Status::Scheduled | Status::Completed(_) => "none",
Status::TemporaryFailure(err) | Status::PermanentFailure(err) => {
match &err.details {
Error::DnsError(_) => "dns",
Error::UnexpectedResponse(_) => "unexpected-reply",
Error::ConnectionError(_) => "connection",
Error::TlsError(_) => "tls",
Error::DaneError(_) => "dane",
Error::MtaStsError(_) => "mta-sts",
Error::RateLimited => "rate",
Error::ConcurrencyLimited => "concurrency",
Error::Io(_) => "io",
}
}
}
.into(),
ExpressionVariable::QueueName => self.rcpt.queue.as_str().into(),
ExpressionVariable::QueueAge => now().saturating_sub(self.message.created).into(),
ExpressionVariable::Source => if (self.message.flags & FROM_AUTHENTICATED) != 0 {
"authenticated"
} else if (self.message.flags & FROM_UNAUTHENTICATED_DMARC) != 0 {
"dmarc_pass"
} else if (self.message.flags & FROM_UNAUTHENTICATED) != 0 {
"unauthenticated"
} else if (self.message.flags & FROM_DSN) != 0 {
"dsn"
} else if (self.message.flags & FROM_REPORT) != 0 {
"report"
} else if (self.message.flags & FROM_AUTOGENERATED) != 0 {
"autogenerated"
} else {
"unknown"
}
.into(),
ExpressionVariable::Mx => self.mx.into(),
ExpressionVariable::Priority => self.message.priority.into(),
ExpressionVariable::RemoteIp => self.remote_ip.to_compact_string().into(),
ExpressionVariable::LocalIp => self.local_ip.to_compact_string().into(),
ExpressionVariable::ReceivedFromIp => {
self.message.received_from_ip.to_compact_string().into()
}
ExpressionVariable::ReceivedViaPort => self.message.received_via_port.into(),
ExpressionVariable::Size => self.message.size.into(),
_ => "".into(),
}
}
fn resolve_global(&self, _: &str) -> Variable<'_> {
Variable::Integer(0)
}
}
impl ResolveVariable for Message {
fn resolve_variable(&self, variable: ExpressionVariable) -> expr::Variable<'_> {
match variable {
ExpressionVariable::Sender => self.return_path.as_ref().into(),
ExpressionVariable::SenderDomain => self.return_path.domain_part().into(),
ExpressionVariable::Recipients => self
.recipients
.iter()
.map(|r| Variable::from(r.address.as_ref()))
.collect::<Vec<_>>()
.into(),
ExpressionVariable::Priority => self.priority.into(),
_ => "".into(),
}
}
fn resolve_global(&self, _: &str) -> Variable<'_> {
Variable::Integer(0)
}
}
impl ResolveVariable for MessageWrapper {
fn resolve_variable(&self, variable: ExpressionVariable) -> expr::Variable<'_> {
match variable {
ExpressionVariable::Sender => self.message.return_path.as_ref().into(),
ExpressionVariable::SenderDomain => self.message.return_path.domain_part().into(),
ExpressionVariable::Recipients => self
.message
.recipients
.iter()
.map(|r| Variable::from(r.address.as_ref()))
.collect::<Vec<_>>()
.into(),
ExpressionVariable::Priority => self.message.priority.into(),
ExpressionVariable::QueueName => self.queue_name.as_str().into(),
ExpressionVariable::QueueAge => now().saturating_sub(self.message.created).into(),
ExpressionVariable::Source => if (self.message.flags & FROM_AUTHENTICATED) != 0 {
"authenticated"
} else if (self.message.flags & FROM_UNAUTHENTICATED_DMARC) != 0 {
"dmarc_pass"
} else if (self.message.flags & FROM_UNAUTHENTICATED) != 0 {
"unauthenticated"
} else if (self.message.flags & FROM_DSN) != 0 {
"dsn"
} else if (self.message.flags & FROM_REPORT) != 0 {
"report"
} else if (self.message.flags & FROM_AUTOGENERATED) != 0 {
"autogenerated"
} else {
"unknown"
}
.into(),
ExpressionVariable::ReceivedFromIp => {
self.message.received_from_ip.to_compact_string().into()
}
ExpressionVariable::ReceivedViaPort => self.message.received_via_port.into(),
ExpressionVariable::Size => self.message.size.into(),
_ => "".into(),
}
}
fn resolve_global(&self, _: &str) -> Variable<'_> {
Variable::Integer(0)
}
}
pub struct RecipientDomain<'x>(&'x str);
impl<'x> RecipientDomain<'x> {
pub fn new(domain: &'x str) -> Self {
Self(domain)
}
}
impl<'x> ResolveVariable for RecipientDomain<'x> {
fn resolve_variable(&self, variable: ExpressionVariable) -> expr::Variable<'x> {
match variable {
ExpressionVariable::RcptDomain => self.0.into(),
_ => "".into(),
}
}
fn resolve_global(&self, _: &str) -> Variable<'_> {
Variable::Integer(0)
}
}
#[inline(always)]
pub fn instant_to_timestamp(now: Instant, time: Instant) -> u64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
+ time.checked_duration_since(now).map_or(0, |d| d.as_secs())
}
impl Recipient {
pub fn new(address: impl AsRef<str>) -> Self {
Recipient {
address: address.to_lowercase_address(false).into_boxed_str(),
status: Status::Scheduled,
flags: 0,
orcpt: None,
retry: Schedule::now(),
notify: Schedule::now(),
expires: QueueExpiry::Attempts(0),
queue: QueueName::default(),
}
}
pub fn with_flags(mut self, flags: u64) -> Self {
self.flags = flags;
self
}
pub fn with_orcpt(mut self, orcpt: Option<Box<str>>) -> Self {
self.orcpt = orcpt;
self
}
pub fn address(&self) -> &str {
&self.address
}
pub fn domain_part(&self) -> &str {
self.address.domain_part()
}
}
impl ArchivedRecipient {
pub fn address(&self) -> &str {
self.address.as_ref()
}
pub fn domain_part(&self) -> &str {
self.address.domain_part()
}
}
pub trait InstantFromTimestamp {
fn to_instant(&self) -> Instant;
}
impl InstantFromTimestamp for u64 {
fn to_instant(&self) -> Instant {
let timestamp = *self;
let current_timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
if timestamp > current_timestamp {
Instant::now() + Duration::from_secs(timestamp - current_timestamp)
} else {
Instant::now()
}
}
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::UnexpectedResponse(response) => {
write!(
f,
"Unexpected response for {}: {}",
response.command, response.response
)
}
Error::DnsError(err) => {
write!(f, "DNS lookup failed: {err}")
}
Error::ConnectionError(details) => {
write!(f, "Connection failed: {details}",)
}
Error::TlsError(details) => {
write!(f, "TLS error: {details}",)
}
Error::DaneError(details) => {
write!(f, "DANE authentication failure: {details}",)
}
Error::MtaStsError(details) => {
write!(f, "MTA-STS auth failed: {details}")
}
Error::RateLimited => {
write!(f, "Rate limited")
}
Error::ConcurrencyLimited => {
write!(f, "Too many concurrent connections to remote server")
}
Error::Io(err) => {
write!(f, "Queue error: {err}")
}
}
}
}
impl Display for ArchivedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ArchivedError::UnexpectedResponse(response) => {
write!(
f,
"Unexpected response for {}: {}",
response.command, response.response
)
}
ArchivedError::DnsError(err) => {
write!(f, "DNS lookup failed: {err}")
}
ArchivedError::ConnectionError(details) => {
write!(f, "Connection failed: {details}",)
}
ArchivedError::TlsError(details) => {
write!(f, "TLS error: {details}",)
}
ArchivedError::DaneError(details) => {
write!(f, "DANE authentication failure: {details}",)
}
ArchivedError::MtaStsError(details) => {
write!(f, "MTA-STS auth failed: {details}")
}
ArchivedError::RateLimited => {
write!(f, "Rate limited")
}
ArchivedError::ConcurrencyLimited => {
write!(f, "Too many concurrent connections to remote server")
}
ArchivedError::Io(err) => {
write!(f, "Queue error: {err}")
}
}
}
}
impl Display for Status<HostResponse<Box<str>>, ErrorDetails> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Status::Scheduled => write!(f, "Scheduled"),
Status::Completed(response) => write!(f, "Delivered: {}", response.response),
Status::TemporaryFailure(err) => {
write!(f, "Temporary Failure for {}: {}", err.entity, err.details)
}
Status::PermanentFailure(err) => {
write!(f, "Permanent Failure for {}: {}", err.entity, err.details)
}
}
}
}
impl Display for ArchivedErrorDetails {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Error for {}: {}", self.entity, self.details)
}
}
/*
pub trait DisplayArchivedResponse {
fn to_string(&self) -> String;
}
impl DisplayArchivedResponse for ArchivedResponse<Box<str>> {
fn to_string(&self) -> String {
format!(
"Code: {}, Enhanced code: {}.{}.{}, Message: {}",
self.code, self.esc[0], self.esc[1], self.esc[2], self.message,
)
}
}
*/
+230
View File
@@ -0,0 +1,230 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{Metadata, QueueEnvelope, Status};
use crate::{core::throttle::NewKey, queue::MessageWrapper};
use ahash::AHashSet;
use common::{Server, config::smtp::queue::QueueQuota, expr::functions::ResolveVariable};
use std::future::Future;
use store::{
ValueKey,
write::{BatchBuilder, QueueClass, ValueClass},
};
use trc::QueueEvent;
use utils::DomainPart;
pub trait HasQueueQuota: Sync + Send {
fn has_quota(
&self,
message: &mut MessageWrapper,
) -> impl Future<Output = Option<Vec<Metadata>>> + Send;
fn check_quota<'x>(
&'x self,
quota: &'x QueueQuota,
envelope: &impl ResolveVariable,
size: u64,
id: u64,
refs: &mut Vec<Metadata>,
session_id: u64,
) -> impl Future<Output = bool> + Send;
}
impl HasQueueQuota for Server {
async fn has_quota(&self, message: &mut MessageWrapper) -> Option<Vec<Metadata>> {
let mut quota_keys = Vec::new();
if !self.core.smtp.queue.quota.sender.is_empty() {
for quota in &self.core.smtp.queue.quota.sender {
if !self
.check_quota(
quota,
&message.message,
message.message.size,
0,
&mut quota_keys,
message.span_id,
)
.await
{
trc::event!(
Queue(QueueEvent::QuotaExceeded),
SpanId = message.span_id,
Id = quota.id.to_string(),
Type = "Sender"
);
return None;
}
}
}
if !self.core.smtp.queue.quota.rcpt_domain.is_empty() {
let mut seen_domains = AHashSet::new();
for quota in &self.core.smtp.queue.quota.rcpt_domain {
for (rcpt_idx, rcpt) in message.message.recipients.iter().enumerate() {
if seen_domains.insert(rcpt.address.domain_part())
&& !self
.check_quota(
quota,
&QueueEnvelope::new(&message.message, rcpt),
message.message.size,
((rcpt_idx + 1) << 32) as u64,
&mut quota_keys,
message.span_id,
)
.await
{
trc::event!(
Queue(QueueEvent::QuotaExceeded),
SpanId = message.span_id,
Id = quota.id.to_string(),
Type = "Domain"
);
return None;
}
}
}
}
for quota in &self.core.smtp.queue.quota.rcpt {
for (rcpt_idx, rcpt) in message.message.recipients.iter().enumerate() {
if !self
.check_quota(
quota,
&QueueEnvelope::new(&message.message, rcpt),
message.message.size,
(rcpt_idx + 1) as u64,
&mut quota_keys,
message.span_id,
)
.await
{
trc::event!(
Queue(QueueEvent::QuotaExceeded),
SpanId = message.span_id,
Id = quota.id.to_string(),
Type = "Recipient"
);
return None;
}
}
}
Some(quota_keys)
}
async fn check_quota<'x>(
&'x self,
quota: &'x QueueQuota,
envelope: &impl ResolveVariable,
size: u64,
id: u64,
refs: &mut Vec<Metadata>,
session_id: u64,
) -> bool {
if !quota.expr.is_empty()
&& self
.eval_if(&quota.expr, envelope, session_id)
.await
.unwrap_or(false)
{
let key = quota.new_key(envelope, "");
if let Some(max_size) = quota.size {
let used_size = self
.core
.storage
.data
.get_counter(ValueKey::from(ValueClass::Queue(QueueClass::QuotaSize(
key.as_ref().to_vec(),
))))
.await
.unwrap_or(0) as u64;
if used_size + size > max_size {
return false;
} else {
refs.push(Metadata::QueueSize {
key: key.as_ref().into(),
id,
});
}
}
if let Some(max_messages) = quota.messages {
let total_messages = self
.core
.storage
.data
.get_counter(ValueKey::from(ValueClass::Queue(QueueClass::QuotaCount(
key.as_ref().to_vec(),
))))
.await
.unwrap_or(0) as u64;
if total_messages + 1 > max_messages {
return false;
} else {
refs.push(Metadata::QueueCount {
key: key.as_ref().into(),
id,
});
}
}
}
true
}
}
impl MessageWrapper {
pub fn release_quota(&mut self, batch: &mut BatchBuilder) {
if !self.message.metadata.iter().any(|metadata| {
matches!(
metadata,
Metadata::QueueSize { .. } | Metadata::QueueCount { .. }
)
}) {
return;
}
let mut quota_ids = Vec::with_capacity(self.message.recipients.len());
let mut seen_domains = AHashSet::new();
for (pos, rcpt) in self.message.recipients.iter().enumerate() {
if matches!(
&rcpt.status,
Status::Completed(_) | Status::PermanentFailure(_)
) {
if seen_domains.insert(rcpt.address.domain_part()) {
quota_ids.push(((pos + 1) as u64) << 32);
}
quota_ids.push((pos + 1) as u64);
}
}
if !quota_ids.is_empty() {
let mut metadata = Vec::new();
for entry in std::mem::take(&mut self.message.metadata) {
match entry {
Metadata::QueueCount { id, key } if quota_ids.contains(&id) => {
batch.add(
ValueClass::Queue(QueueClass::QuotaCount(key.into_vec())),
-1,
);
}
Metadata::QueueSize { id, key } if quota_ids.contains(&id) => {
batch.add(
ValueClass::Queue(QueueClass::QuotaSize(key.into_vec())),
-(self.message.size as i64),
);
}
_ => {
metadata.push(entry);
}
}
}
self.message.metadata = metadata.into_boxed_slice();
}
}
}
File diff suppressed because it is too large Load Diff
+65
View File
@@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::core::throttle::NewKey;
use common::{
KV_RATE_LIMIT_SMTP, Server, config::smtp::QueueRateLimiter, expr::functions::ResolveVariable,
};
use std::future::Future;
use store::write::now;
pub trait IsAllowed: Sync + Send {
fn is_allowed<'x>(
&'x self,
throttle: &'x QueueRateLimiter,
envelope: &impl ResolveVariable,
session_id: u64,
) -> impl Future<Output = Result<(), u64>> + Send;
}
impl IsAllowed for Server {
async fn is_allowed<'x>(
&'x self,
throttle: &'x QueueRateLimiter,
envelope: &impl ResolveVariable,
session_id: u64,
) -> Result<(), u64> {
if throttle.expr.is_empty()
|| self
.eval_if(&throttle.expr, envelope, session_id)
.await
.unwrap_or(false)
{
let key = throttle.new_key(envelope, "outbound");
match self
.in_memory_store()
.is_rate_allowed(KV_RATE_LIMIT_SMTP, key.as_ref(), &throttle.rate, false)
.await
{
Ok(Some(next_refill)) => {
trc::event!(
Queue(trc::QueueEvent::RateLimitExceeded),
SpanId = session_id,
Id = throttle.id.to_string(),
Limit = vec![
trc::Value::from(throttle.rate.count),
trc::Value::from(throttle.rate.period.into_inner())
],
);
return Err(now() + next_refill);
}
Err(err) => {
trc::error!(err.span_id(session_id).caused_by(trc::location!()));
}
_ => (),
}
}
Ok(())
}
}
+412
View File
@@ -0,0 +1,412 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use ahash::AHashSet;
use common::{Server, psl};
use mail_auth::{
flate2::read::GzDecoder,
report::{Feedback, Report, tlsrpt::TlsReport},
zip,
};
use mail_parser::{Message, MessagePart, MimeHeaders, PartType};
use registry::{
schema::structs::{ArfExternalReport, DmarcExternalReport, TlsExternalReport},
types::datetime::UTCDateTime,
};
use std::{
borrow::Cow,
io::{Cursor, Read},
};
use store::write::{BatchBuilder, now};
use trc::IncomingReportEvent;
use types::id::Id;
use crate::reporting::{inbound::LogReport, index::ExternalReportIndex};
enum Compression {
None,
Gzip,
Zip,
}
enum Format<D, T, A> {
Dmarc(D),
Tls(T),
Arf(A),
}
pub(crate) struct ReportData<'x> {
compression: Compression,
format: Format<(), (), ()>,
data: &'x [u8],
}
impl<'x> ReportData<'x> {
fn from_part(part: &'x MessagePart<'x>) -> Option<Self> {
match &part.body {
PartType::Text(report) => {
if part
.content_type()
.and_then(|ct| ct.subtype())
.is_some_and(|t| t.eq_ignore_ascii_case("xml"))
|| part
.attachment_name()
.and_then(|n| n.rsplit_once('.'))
.is_some_and(|(_, e)| e.eq_ignore_ascii_case("xml"))
{
Some(ReportData {
compression: Compression::None,
format: Format::Dmarc(()),
data: report.as_bytes(),
})
} else if part.is_content_type("message", "feedback-report") {
Some(ReportData {
compression: Compression::None,
format: Format::Arf(()),
data: report.as_bytes(),
})
} else {
None
}
}
PartType::Binary(report) | PartType::InlineBinary(report) => {
if part.is_content_type("message", "feedback-report") {
return Some(ReportData {
compression: Compression::None,
format: Format::Arf(()),
data: report.as_ref(),
});
}
let subtype = part
.content_type()
.and_then(|ct| ct.subtype())
.unwrap_or("");
let attachment_name = part.attachment_name();
let ext = attachment_name
.and_then(|f| f.rsplit_once('.'))
.map_or("", |(_, e)| e);
let tls_parts = subtype.rsplit_once('+');
let compression = match (tls_parts.map(|(_, c)| c).unwrap_or(subtype), ext) {
("gzip", _) => Compression::Gzip,
("zip", _) => Compression::Zip,
(_, "gz") => Compression::Gzip,
(_, "zip") => Compression::Zip,
_ => Compression::None,
};
let format = match (tls_parts.map(|(c, _)| c).unwrap_or(subtype), ext) {
("xml", _) => Format::Dmarc(()),
("tlsrpt", _) | (_, "json") => Format::Tls(()),
_ => {
if attachment_name.is_some_and(|n| n.contains(".xml") || n.contains('!')) {
Format::Dmarc(())
} else {
return None;
}
}
};
Some(ReportData {
compression,
format,
data: report.as_ref(),
})
}
_ => None,
}
}
fn extract(message: &'x Message<'x>) -> Vec<Self> {
message.parts.iter().filter_map(Self::from_part).collect()
}
pub(crate) fn is_present(message: &Message<'_>) -> bool {
message
.parts
.iter()
.any(|part| ReportData::from_part(part).is_some())
}
}
pub trait AnalyzeReport: Sync + Send {
fn analyze_report(&self, message: Message<'static>, session_id: u64);
}
impl AnalyzeReport for Server {
fn analyze_report(&self, message: Message<'static>, session_id: u64) {
let core = self.clone();
tokio::spawn(async move {
let from: String = message
.from()
.and_then(|a| a.last())
.and_then(|a| a.address())
.unwrap_or_default()
.into();
let to: Vec<String> = message.to().map_or_else(Vec::new, |a| {
a.iter()
.filter_map(|a| a.address())
.map(|a| a.into())
.collect()
});
let subject: String = message.subject().unwrap_or_default().into();
let reports = ReportData::extract(&message);
let max_size = core.core.smtp.report.analysis.max_size;
for report in reports {
let data = match report.compression {
Compression::None => Cow::Borrowed(report.data),
Compression::Gzip => {
match read_capped(GzDecoder::new(report.data), 0, max_size) {
Ok(buf) => Cow::Owned(buf),
Err(err) => {
trc::event!(
IncomingReport(IncomingReportEvent::DecompressError),
SpanId = session_id,
From = from.to_string(),
Reason = err.to_string(),
CausedBy = trc::location!()
);
continue;
}
}
}
Compression::Zip => {
let data = report.data.to_vec();
let result = tokio::task::spawn_blocking(
move || -> Result<Vec<u8>, std::io::Error> {
let mut archive = zip::ZipArchive::new(Cursor::new(data))
.map_err(std::io::Error::other)?;
if archive.is_empty() {
return Ok(Vec::new());
}
let mut file =
archive.by_index(0).map_err(std::io::Error::other)?;
let size_hint = file.size();
read_capped(&mut file, size_hint, max_size)
},
)
.await;
match result {
Ok(Ok(buf)) => Cow::Owned(buf),
Ok(Err(err)) => {
trc::event!(
IncomingReport(IncomingReportEvent::DecompressError),
SpanId = session_id,
From = from.to_string(),
Reason = err.to_string(),
CausedBy = trc::location!()
);
continue;
}
Err(err) => {
trc::event!(
IncomingReport(IncomingReportEvent::DecompressError),
SpanId = session_id,
From = from.to_string(),
Reason = err.to_string(),
CausedBy = trc::location!()
);
continue;
}
}
}
};
let report = match report.format {
Format::Dmarc(_) => match Report::parse_xml(&data) {
Ok(report) => {
// Log
report.log();
Format::Dmarc(report)
}
Err(err) => {
trc::event!(
IncomingReport(IncomingReportEvent::DmarcParseFailed),
SpanId = session_id,
From = from.to_string(),
Reason = err,
CausedBy = trc::location!()
);
continue;
}
},
Format::Tls(_) => match TlsReport::parse_json(&data) {
Ok(report) => {
// Log
report.log();
Format::Tls(report)
}
Err(err) => {
trc::event!(
IncomingReport(IncomingReportEvent::TlsRpcParseFailed),
SpanId = session_id,
From = from.to_string(),
Reason = format!("{err:?}"),
CausedBy = trc::location!()
);
continue;
}
},
Format::Arf(_) => match Feedback::parse_arf(&data) {
Some(report) => {
// Log
report.log();
Format::Arf(report.into_owned())
}
None => {
trc::event!(
IncomingReport(IncomingReportEvent::ArfParseFailed),
SpanId = session_id,
From = from.to_string(),
CausedBy = trc::location!()
);
continue;
}
},
};
// Store report
if let Some(expires_in) = &core.core.smtp.report.analysis.store {
let expires = now() + expires_in.as_secs();
let item_id = core.inner.data.queue_id_gen.generate();
let mut batch = BatchBuilder::new();
match report {
Format::Dmarc(report) => {
let mut report = DmarcExternalReport {
from,
to: to.into(),
subject,
member_tenant_id: None,
expires_at: UTCDateTime::from_timestamp(expires as i64),
received_at: UTCDateTime::now(),
report: report.into(),
};
report.member_tenant_id = tenant_ids(
&core,
report
.domains()
.filter_map(psl::domain_str)
.collect::<AHashSet<_>>(),
)
.await;
report.write_ops(&mut batch, item_id, true);
}
Format::Tls(report) => {
let mut report = TlsExternalReport {
from,
to: to.into(),
subject,
member_tenant_id: None,
expires_at: UTCDateTime::from_timestamp(expires as i64),
received_at: UTCDateTime::now(),
report: report.into(),
};
report.member_tenant_id = tenant_ids(
&core,
report
.domains()
.filter_map(psl::domain_str)
.collect::<AHashSet<_>>(),
)
.await;
report.write_ops(&mut batch, item_id, true);
}
Format::Arf(report) => {
let mut report = ArfExternalReport {
from,
to: to.into(),
subject,
member_tenant_id: None,
expires_at: UTCDateTime::from_timestamp(expires as i64),
received_at: UTCDateTime::now(),
report: report.into(),
};
report.member_tenant_id = tenant_ids(
&core,
report
.domains()
.filter_map(psl::domain_str)
.collect::<AHashSet<_>>(),
)
.await;
report.write_ops(&mut batch, item_id, true);
}
}
if let Err(err) = core.core.storage.data.write(batch.build_all()).await
&& !err.is_assertion_failure()
{
trc::error!(
err.span_id(session_id)
.caused_by(trc::location!())
.details("Failed to write report")
);
}
}
return;
}
});
}
}
async fn tenant_ids(server: &Server, domains: AHashSet<&str>) -> Option<Id> {
let mut tenant_ids = Vec::with_capacity(domains.len());
for domain in domains {
if let Some(tenant_id) = server
.domain(domain)
.await
.map_err(|err| {
trc::error!(
err.caused_by(trc::location!())
.details("Failed to lookup domain")
);
})
.unwrap_or_default()
.and_then(|domain| domain.id_tenant)
.map(Id::from)
&& !tenant_ids.contains(&tenant_id)
{
tenant_ids.push(tenant_id);
}
}
if tenant_ids.len() == 1 {
tenant_ids.into_iter().next()
} else {
None
}
}
fn read_capped(
reader: impl Read,
size_hint: u64,
max_size: usize,
) -> Result<Vec<u8>, std::io::Error> {
let max_size = max_size as u64;
if size_hint > max_size {
return Err(std::io::Error::other(format!(
"Report is larger than the {max_size} byte limit"
)));
}
let mut buf = Vec::with_capacity(size_hint.min(64 * 1024) as usize);
reader
.take(max_size.saturating_add(1))
.read_to_end(&mut buf)?;
if buf.len() as u64 > max_size {
return Err(std::io::Error::other(format!(
"Report is larger than the {max_size} byte limit"
)));
}
Ok(buf)
}
+109
View File
@@ -0,0 +1,109 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{core::Session, reporting::send::MtaReportSend};
use common::network::SessionStream;
use mail_auth::{
AuthenticatedMessage, AuthenticationResults, DkimOutput, common::verify::VerifySignature,
};
use registry::schema::structs::Rate;
use trc::OutgoingReportEvent;
impl<T: SessionStream> Session<T> {
pub async fn send_dkim_report(
&self,
rcpt: &str,
message: &AuthenticatedMessage<'_>,
rate: &Rate,
rejected: bool,
output: &DkimOutput<'_>,
) {
// Generate report
let signature = if let Some(signature) = output.signature() {
signature
} else {
return;
};
if self
.server
.is_local_report_domain(signature.domain(), self.data.session_id)
.await
{
return;
}
// Throttle recipient
if !self.throttle_rcpt(rcpt, rate, "dkim").await {
trc::event!(
OutgoingReport(OutgoingReportEvent::DkimRateLimited),
SpanId = self.data.session_id,
To = rcpt.to_string(),
Limit = vec![
trc::Value::from(rate.count),
trc::Value::from(rate.period.into_inner())
],
);
return;
}
let config = &self.server.core.smtp.report.dkim;
let from_addr = self
.server
.eval_if(&config.address, self, self.data.session_id)
.await
.unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string());
let mut report = Vec::with_capacity(128);
self.new_auth_failure(output.result().into(), rejected)
.with_authentication_results(
AuthenticationResults::new(&self.hostname)
.with_dkim_result(output, message.from())
.to_string(),
)
.with_dkim_domain(signature.domain())
.with_dkim_selector(signature.selector())
.with_dkim_identity(signature.identity())
.with_headers(std::str::from_utf8(message.raw_headers()).unwrap_or_default())
.write_rfc5322(
(
self.server
.eval_if(&config.name, self, self.data.session_id)
.await
.unwrap_or_else(|| "Mail Delivery Subsystem".to_string())
.as_str(),
from_addr.as_str(),
),
rcpt,
&self
.server
.eval_if(&config.subject, self, self.data.session_id)
.await
.unwrap_or_else(|| "DKIM Report".to_string()),
&mut report,
)
.ok();
trc::event!(
OutgoingReport(OutgoingReportEvent::DkimReport),
SpanId = self.data.session_id,
From = from_addr.to_string(),
To = rcpt.to_string(),
);
// Send report
self.server
.send_report(
&from_addr,
[rcpt].into_iter(),
report,
&config.sign,
true,
self.data.session_id,
)
.await;
}
}
+695
View File
@@ -0,0 +1,695 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::AggregateTimestamp;
use crate::{
core::Session,
queue::RecipientDomain,
reporting::{index::InternalReportIndex, send::MtaReportSend},
};
use common::{
Server,
config::smtp::report::AggregateFrequency,
ipc::{DmarcEvent, ToHash},
network::SessionStream,
};
use compact_str::ToCompactString;
use mail_auth::{
ArcOutput, AuthenticatedMessage, AuthenticationResults, DkimOutput, DkimResult, DmarcOutput,
DmarcResult, SpfResult,
common::verify::VerifySignature,
dkim2::Dkim2Output,
dmarc::{self},
report::{AuthFailureType, IdentityAlignment, PolicyPublished, Record, SPFDomainScope},
};
use registry::{
schema::{
enums::FailureReportingOption,
prelude::{ObjectType, Property},
structs::{DmarcInternalReport, DmarcReport, DmarcReportRecord, Rate},
},
types::{EnumImpl, ObjectImpl, datetime::UTCDateTime, map::Map},
};
use std::{borrow::Cow, future::Future};
use store::{
SerializeInfallible, U64_LEN, ValueKey,
registry::ObjectIdVersioned,
write::{BatchBuilder, RegistryClass, ValueClass, assert::AssertValue, key::KeySerializer},
};
use trc::{AddContext, OutgoingReportEvent};
use utils::DomainPart;
impl<T: SessionStream> Session<T> {
#[allow(clippy::too_many_arguments)]
pub async fn send_dmarc_report(
&self,
message: &AuthenticatedMessage<'_>,
auth_results: &AuthenticationResults<'_>,
rejected: bool,
dmarc_output: DmarcOutput,
dkim_output: &[DkimOutput<'_>],
dkim2_output: Option<&Dkim2Output<'_>>,
arc_output: &Option<ArcOutput<'_>>,
) {
let dmarc_record = dmarc_output.dmarc_record_cloned().unwrap();
let config = &self.server.core.smtp.report.dmarc;
if self
.server
.is_local_report_domain(dmarc_output.domain(), self.data.session_id)
.await
{
return;
}
// Send failure report. RFC 9991 Section 2: report generators MUST NOT
// honor "ruf" for policy records published with "psd=y".
if !matches!(dmarc_record.psd, dmarc::Psd::Yes)
&& let (Some(failure_rate), Some(report_options)) = (
self.server
.eval_if::<Rate, _>(&config.send, self, self.data.session_id)
.await,
dmarc_output.failure_report(),
)
{
// Verify that any external reporting addresses are authorized
let rcpts = match self
.server
.core
.smtp
.resolvers
.dns
.verify_dmarc_report_address(
dmarc_output.domain(),
dmarc_record.ruf(),
Some(&self.server.inner.cache.dns_txt),
)
.await
{
Some(rcpts) => {
if !rcpts.is_empty() {
let mut new_rcpts = Vec::with_capacity(rcpts.len());
for rcpt in rcpts {
if self.throttle_rcpt(rcpt.uri(), &failure_rate, "dmarc").await {
new_rcpts.push(rcpt.uri());
}
}
new_rcpts
} else {
if !dmarc_record.ruf().is_empty() {
trc::event!(
OutgoingReport(OutgoingReportEvent::UnauthorizedReportingAddress),
SpanId = self.data.session_id,
Url = dmarc_record
.ruf()
.iter()
.map(|u| trc::Value::String(u.uri().to_compact_string()))
.collect::<Vec<_>>(),
);
}
vec![]
}
}
None => {
trc::event!(
OutgoingReport(OutgoingReportEvent::ReportingAddressValidationError),
SpanId = self.data.session_id,
Url = dmarc_record
.ruf()
.iter()
.map(|u| trc::Value::String(u.uri().to_compact_string()))
.collect::<Vec<_>>(),
);
vec![]
}
};
// Throttle recipient
if !rcpts.is_empty() {
let mut report = Vec::with_capacity(128);
let from_addr = self
.server
.eval_if(&config.address, self, self.data.session_id)
.await
.unwrap_or_else(|| "MAILER-DAEMON@localhost".to_compact_string());
let mut auth_failure = self
.new_auth_failure(AuthFailureType::Dmarc, rejected)
.with_authentication_results(auth_results.to_string())
.with_headers(std::str::from_utf8(message.raw_headers()).unwrap_or_default());
let dkim_aligned = matches!(dmarc_output.dkim_result(), DmarcResult::Pass);
let spf_aligned = matches!(dmarc_output.spf_result(), DmarcResult::Pass);
// Report the first failed signature
if let (
dmarc::Report::Dkim
| dmarc::Report::DkimSpf
| dmarc::Report::All
| dmarc::Report::Any,
Some(signature),
) = (
&report_options,
if !dkim_aligned {
dkim_output
.iter()
.find_map(|o| {
let s = o.signature()?;
if !matches!(o.result(), DkimResult::Pass) {
Some(s)
} else {
None
}
})
.or_else(|| dkim_output.iter().find_map(|o| o.signature()))
} else {
None
},
) {
auth_failure = auth_failure
.with_dkim_domain(signature.domain())
.with_dkim_selector(signature.selector())
.with_dkim_identity(signature.identity());
}
// Report SPF failure
if let (
dmarc::Report::Spf
| dmarc::Report::DkimSpf
| dmarc::Report::All
| dmarc::Report::Any,
Some(output),
) = (
&report_options,
if !spf_aligned {
self.data
.spf_ehlo
.as_ref()
.and_then(|s| {
if s.result() != SpfResult::Pass {
s.into()
} else {
None
}
})
.or_else(|| {
self.data.spf_mail_from.as_ref().and_then(|s| {
if s.result() != SpfResult::Pass {
s.into()
} else {
None
}
})
})
.or(self.data.spf_mail_from.as_ref())
} else {
None
},
) {
auth_failure =
auth_failure.with_spf_dns(format!("txt : {} : v=SPF1", output.domain()));
// TODO use DNS record
}
auth_failure
.with_identity_alignment(match (dkim_aligned, spf_aligned) {
(false, false) => IdentityAlignment::DkimSpf,
(false, true) => IdentityAlignment::Dkim,
(true, false) => IdentityAlignment::Spf,
(true, true) => IdentityAlignment::None,
})
.write_rfc5322(
(
self.server
.eval_if(&config.name, self, self.data.session_id)
.await
.unwrap_or_else(|| "Mail Delivery Subsystem".to_compact_string())
.as_str(),
from_addr.as_str(),
),
&rcpts.join(", "),
&self
.server
.eval_if(&config.subject, self, self.data.session_id)
.await
.unwrap_or_else(|| "DMARC Report".to_compact_string()),
&mut report,
)
.ok();
trc::event!(
OutgoingReport(OutgoingReportEvent::DmarcReport),
SpanId = self.data.session_id,
From = from_addr.to_string(),
To = rcpts
.iter()
.map(|a| trc::Value::String(a.to_compact_string()))
.collect::<Vec<_>>(),
);
// Send report
self.server
.send_report(
&from_addr,
rcpts.into_iter(),
report,
&config.sign,
true,
self.data.session_id,
)
.await;
} else {
trc::event!(
OutgoingReport(OutgoingReportEvent::DmarcRateLimited),
SpanId = self.data.session_id,
Limit = vec![
trc::Value::from(failure_rate.count),
trc::Value::from(failure_rate.period.into_inner())
],
);
}
}
// Send aggregate reports
let interval = self
.server
.eval_if(
&self.server.core.smtp.report.dmarc_aggregate.send,
self,
self.data.session_id,
)
.await
.unwrap_or(AggregateFrequency::Never);
if matches!(interval, AggregateFrequency::Never) || dmarc_record.rua().is_empty() {
return;
}
// Report the same identifier forms that were used for alignment
let message_from = message.from();
let header_from = message_from.domain_part();
let header_from = header_from
.to_ascii_domain()
.unwrap_or(Cow::Borrowed(header_from));
let envelope_from = self
.data
.mail_from
.as_ref()
.map(|mf| mf.domain.as_str())
.unwrap_or_else(|| self.data.helo_domain.as_str());
let envelope_from = envelope_from
.to_ascii_domain()
.unwrap_or(Cow::Borrowed(envelope_from));
// Create DMARC report record
let mut report_record = Record::new()
.with_dmarc_output(&dmarc_output)
.with_dkim_output(dkim_output)
.with_source_ip(self.data.remote_ip)
.with_header_from(header_from.as_ref())
.with_envelope_from(envelope_from.as_ref());
if let Some(dkim2_output) = dkim2_output {
report_record = report_record.with_dkim2_output(dkim2_output);
}
if let Some(spf_ehlo) = &self.data.spf_ehlo {
report_record = report_record.with_spf_output(spf_ehlo, SPFDomainScope::Helo);
}
if let Some(spf_mail_from) = &self.data.spf_mail_from {
report_record = report_record.with_spf_output(spf_mail_from, SPFDomainScope::MailFrom);
}
if let Some(arc_output) = arc_output {
report_record = report_record.with_arc_output(arc_output);
}
// Submit DMARC report event
self.server
.schedule_report(DmarcEvent {
domain: dmarc_output.into_domain(),
report_record,
dmarc_record,
interval,
span_id: self.data.session_id,
})
.await;
}
}
pub trait DmarcReporting: Sync + Send {
fn send_dmarc_aggregate_report(
&self,
report_id: u64,
) -> impl Future<Output = trc::Result<()>> + Send;
fn schedule_dmarc(&self, event: Box<DmarcEvent>) -> impl Future<Output = ()> + Send;
}
impl DmarcReporting for Server {
async fn send_dmarc_aggregate_report(&self, item_id: u64) -> trc::Result<()> {
let object_id = ObjectType::DmarcInternalReport.to_id();
let key = ValueClass::Registry(RegistryClass::Item { object_id, item_id });
let Some(report) = self
.store()
.get_value::<DmarcInternalReport>(ValueKey::from(key.clone()))
.await
.caused_by(trc::location!())?
else {
return Ok(());
};
// Delete report
let mut batch = BatchBuilder::new();
batch.clear(key).clear(RegistryClass::PrimaryKey {
object_id: object_id.into(),
index_id: Property::Domain.to_id(),
key: KeySerializer::new(report.domain.len() + U64_LEN)
.write(&report.domain)
.write(report.policy_identifier)
.finalize(),
});
self.store()
.write(batch.build_all())
.await
.caused_by(trc::location!())?;
let span_id = self.inner.data.span_id_gen.generate();
let event_from = report.report.date_range_begin.timestamp() as u64;
let event_to = report.report.date_range_end.timestamp() as u64;
trc::event!(
OutgoingReport(OutgoingReportEvent::DmarcAggregateReport),
SpanId = span_id,
ReportId = event_from,
Domain = report.domain.clone(),
RangeFrom = trc::Value::Timestamp(event_from),
RangeTo = trc::Value::Timestamp(event_to),
);
// Verify external reporting addresses
let rua = match self
.core
.smtp
.resolvers
.dns
.verify_dmarc_report_address(
&report.domain,
report.rua.as_slice(),
Some(&self.inner.cache.dns_txt),
)
.await
{
Some(rcpts) => {
if !rcpts.is_empty() {
rcpts
} else {
trc::event!(
OutgoingReport(OutgoingReportEvent::UnauthorizedReportingAddress),
SpanId = span_id,
Url = report
.rua
.into_iter()
.map(|u| trc::Value::String(u.into()))
.collect::<Vec<_>>(),
);
return Ok(());
}
}
None => {
trc::event!(
OutgoingReport(OutgoingReportEvent::ReportingAddressValidationError),
SpanId = span_id,
Url = report
.rua
.into_iter()
.map(|u| trc::Value::String(u.into()))
.collect::<Vec<_>>(),
);
return Ok(());
}
};
// Serialize report
let config = &self.core.smtp.report.dmarc_aggregate;
let from_addr = self
.eval_if(
&config.address,
&RecipientDomain::new(report.domain.as_str()),
span_id,
)
.await
.unwrap_or_else(|| "MAILER-DAEMON@localhost".to_compact_string());
let mut message = Vec::with_capacity(2048);
let _ = mail_auth::report::Report::from(report.report).write_rfc5322(
&self
.eval_if(
&self.core.smtp.report.submitter,
&RecipientDomain::new(report.domain.as_str()),
span_id,
)
.await
.unwrap_or_else(|| "localhost".to_compact_string()),
(
self.eval_if(
&config.name,
&RecipientDomain::new(report.domain.as_str()),
span_id,
)
.await
.unwrap_or_else(|| "Mail Delivery Subsystem".to_compact_string())
.as_str(),
from_addr.as_str(),
),
rua.iter().map(|a| a.as_str()),
&mut message,
);
// Send report
self.send_report(
&from_addr,
rua.iter(),
message,
&config.sign,
false,
span_id,
)
.await;
Ok(())
}
async fn schedule_dmarc(&self, event: Box<DmarcEvent>) {
let object_id = ObjectType::DmarcInternalReport.to_id();
let policy_hash = event.dmarc_record.to_hash();
let pk = ValueClass::Registry(RegistryClass::PrimaryKey {
object_id: object_id.into(),
index_id: Property::Domain.to_id(),
key: KeySerializer::new(event.domain.len() + U64_LEN)
.write(&event.domain)
.write(policy_hash)
.finalize(),
});
let mut rety_count = 0;
loop {
// Find the report by domain name
let mut batch = BatchBuilder::new();
let report = match self
.store()
.get_value::<ObjectIdVersioned>(ValueKey::from(pk.clone()))
.await
{
Ok(Some(object_id_v)) => {
match self
.store()
.get_value::<DmarcInternalReport>(ValueKey::from(ValueClass::Registry(
RegistryClass::Item {
object_id,
item_id: object_id_v.object_id.id().id(),
},
)))
.await
{
Ok(Some(report)) => Some((object_id_v, report)),
Ok(None) => {
trc::event!(
OutgoingReport(OutgoingReportEvent::NotFound),
Id = object_id_v.object_id.id().id(),
CausedBy = trc::location!(),
Details = "Failed to find DMARC report for domain"
);
return;
}
Err(err) => {
trc::error!(
err.caused_by(trc::location!())
.details("Failed to query registry for DMARC report")
);
return;
}
}
}
Ok(None) => None,
Err(err) => {
trc::error!(
err.caused_by(trc::location!())
.details("Failed to query registry for DMARC report")
);
return;
}
};
// Create report if missing
let config = &self.core.smtp.report.dmarc_aggregate;
let (item_id, mut report) = if let Some((mut object_id_v, report)) = report {
batch.assert_value(pk.clone(), AssertValue::U32(object_id_v.version));
object_id_v.version += 1;
batch.set(pk.clone(), object_id_v.serialize());
(object_id_v.object_id.id().id(), report)
} else {
let item_id = self.inner.data.queue_id_gen.generate();
let date_range_begin = UTCDateTime::now();
let date_range_end = UTCDateTime::from_timestamp(
date_range_begin.timestamp() + event.interval.as_secs() as i64,
);
let policy =
PolicyPublished::from_record(event.domain.clone(), &event.dmarc_record);
let report = DmarcInternalReport {
created_at: date_range_begin,
deliver_at: date_range_end,
domain: event.domain.clone(),
report: DmarcReport {
report_id: format!("{}_{policy_hash}", date_range_begin.timestamp()),
date_range_begin,
date_range_end,
email: self
.eval_if(
&config.address,
&RecipientDomain::new(event.domain.as_str()),
event.span_id,
)
.await
.unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()),
extra_contact_info: self
.eval_if::<String, _>(
&config.contact_info,
&RecipientDomain::new(event.domain.as_str()),
event.span_id,
)
.await,
org_name: self
.eval_if::<String, _>(
&config.org_name,
&RecipientDomain::new(event.domain.as_str()),
event.span_id,
)
.await
.unwrap_or_default(),
policy_adkim: policy.adkim.into(),
policy_aspf: policy.aspf.into(),
policy_disposition: policy.p.into(),
policy_domain: policy.domain,
policy_failure_reporting_options: match event.dmarc_record.fo {
dmarc::Report::All => vec![FailureReportingOption::All],
dmarc::Report::Any => vec![FailureReportingOption::Any],
dmarc::Report::Dkim => vec![FailureReportingOption::DkimFailure],
dmarc::Report::Spf => vec![FailureReportingOption::SpfFailure],
dmarc::Report::DkimSpf => vec![
FailureReportingOption::DkimFailure,
FailureReportingOption::SpfFailure,
],
}
.into(),
policy_subdomain_disposition: policy.sp.into(),
policy_np: policy.np.into(),
policy_discovery_method: policy.discovery_method.into(),
policy_testing_mode: policy.testing,
policy_version: None,
version: 1.0.into(),
..Default::default()
},
policy_identifier: policy_hash,
rua: Map::new(
event
.dmarc_record
.rua()
.iter()
.map(|u| u.uri.clone())
.collect(),
),
};
report.write_ops(&mut batch, item_id, true);
(item_id, report)
};
// Add record
let mut record = DmarcReportRecord::from(event.report_record.clone());
if let Some(idx) = report
.report
.records
.0
.inner
.iter()
.position(|d| d.value.eq_except_count(&record))
{
report.report.records.0.inner[idx].value.count += 1;
} else {
record.count = 1;
report.report.records.push(record);
}
// Write entry
let report_bytes = report.to_pickled_vec();
let max_report_size = self
.eval_if(
&config.max_size,
&RecipientDomain::new(&event.domain),
event.span_id,
)
.await
.unwrap_or(5 * 1024 * 1024);
if max_report_size != 0 && report_bytes.len() > max_report_size {
trc::event!(
OutgoingReport(OutgoingReportEvent::MaxSizeExceeded),
SpanId = event.span_id,
Domain = event.domain.clone(),
Details = report_bytes.len(),
Limit = max_report_size,
);
return;
}
batch.set(
ValueClass::Registry(RegistryClass::Item { object_id, item_id }),
report_bytes,
);
match self.core.storage.data.write(batch.build_all()).await {
Ok(_) => {
break;
}
Err(err) => {
if err.is_assertion_failure() && rety_count < 3 {
rety_count += 1;
continue;
}
trc::error!(
err.caused_by(trc::location!())
.details("Failed to write DMARC report")
);
break;
}
}
}
}
}
+208
View File
@@ -0,0 +1,208 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::core::Session;
use ahash::AHashMap;
use common::USER_AGENT;
use mail_auth::report::{
ActionDisposition, AuthFailureType, DeliveryResult, DmarcResult, Feedback, FeedbackType,
Report, tlsrpt::TlsReport,
};
use std::{collections::hash_map::Entry, time::SystemTime};
use store::write::now;
use tokio::io::{AsyncRead, AsyncWrite};
use trc::IncomingReportEvent;
impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
pub fn new_auth_failure(&self, ft: AuthFailureType, rejected: bool) -> Feedback<'_> {
Feedback::new(FeedbackType::AuthFailure)
.with_auth_failure(ft)
.with_arrival_date(
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |d| d.as_secs()) as i64,
)
.with_source_ip(self.data.remote_ip)
.with_reporting_mta(&self.hostname)
.with_user_agent(USER_AGENT)
.with_delivery_result(if rejected {
DeliveryResult::Reject
} else {
DeliveryResult::Unspecified
})
}
pub fn is_report(&self) -> bool {
let analysis = &self.server.core.smtp.report.analysis;
self.data
.rcpt_to
.iter()
.any(|addr| analysis.is_report_address(addr.report_address()))
}
}
pub(crate) trait LogReport {
fn log(&self);
}
impl LogReport for Report {
fn log(&self) {
let mut dmarc_pass = 0;
let mut dmarc_quarantine = 0;
let mut dmarc_reject = 0;
let mut dmarc_none = 0;
let mut dkim_pass = 0;
let mut dkim_fail = 0;
let mut dkim_none = 0;
let mut spf_pass = 0;
let mut spf_fail = 0;
let mut spf_none = 0;
for record in self.records() {
let count = std::cmp::min(record.count(), 1);
match record.action_disposition() {
ActionDisposition::Pass => {
dmarc_pass += count;
}
ActionDisposition::Quarantine => {
dmarc_quarantine += count;
}
ActionDisposition::Reject => {
dmarc_reject += count;
}
ActionDisposition::None | ActionDisposition::Unspecified => {
dmarc_none += count;
}
}
match record.dmarc_dkim_result() {
DmarcResult::Pass => {
dkim_pass += count;
}
DmarcResult::Fail => {
dkim_fail += count;
}
DmarcResult::Unspecified => {
dkim_none += count;
}
}
match record.dmarc_spf_result() {
DmarcResult::Pass => {
spf_pass += count;
}
DmarcResult::Fail => {
spf_fail += count;
}
DmarcResult::Unspecified => {
spf_none += count;
}
}
}
trc::event!(
IncomingReport(
if (dmarc_reject + dmarc_quarantine + dkim_fail + spf_fail) > 0 {
IncomingReportEvent::DmarcReportWithWarnings
} else {
IncomingReportEvent::DmarcReport
}
),
RangeFrom = trc::Value::Timestamp(self.date_range_begin()),
RangeTo = trc::Value::Timestamp(self.date_range_end()),
Domain = self.domain().to_string(),
From = self.email().to_string(),
Id = self.report_id().to_string(),
DmarcPass = dmarc_pass,
DmarcQuarantine = dmarc_quarantine,
DmarcReject = dmarc_reject,
DmarcNone = dmarc_none,
DkimPass = dkim_pass,
DkimFail = dkim_fail,
DkimNone = dkim_none,
SpfPass = spf_pass,
SpfFail = spf_fail,
SpfNone = spf_none,
);
}
}
impl LogReport for TlsReport {
fn log(&self) {
for policy in self.policies.iter().take(5) {
let mut details = AHashMap::with_capacity(policy.failure_details.len());
for failure in &policy.failure_details {
let num_failures = std::cmp::min(1, failure.failed_session_count);
match details.entry(failure.result_type) {
Entry::Occupied(mut e) => {
*e.get_mut() += num_failures;
}
Entry::Vacant(e) => {
e.insert(num_failures);
}
}
}
trc::event!(
IncomingReport(if policy.summary.total_failure > 0 {
IncomingReportEvent::TlsReportWithWarnings
} else {
IncomingReportEvent::TlsReport
}),
RangeFrom =
trc::Value::Timestamp(self.date_range.start_datetime.to_timestamp() as u64),
RangeTo = trc::Value::Timestamp(self.date_range.end_datetime.to_timestamp() as u64),
Domain = policy.policy.policy_domain.clone(),
From = self.contact_info.as_deref().unwrap_or_default().to_string(),
Id = self.report_id.clone(),
Policy = format!("{:?}", policy.policy.policy_type),
TotalSuccesses = policy.summary.total_success,
TotalFailures = policy.summary.total_failure,
Details = format!("{details:?}"),
);
}
}
}
impl LogReport for Feedback<'_> {
fn log(&self) {
trc::event!(
IncomingReport(match self.feedback_type() {
mail_auth::report::FeedbackType::Abuse => IncomingReportEvent::AbuseReport,
mail_auth::report::FeedbackType::AuthFailure =>
IncomingReportEvent::AuthFailureReport,
mail_auth::report::FeedbackType::Fraud => IncomingReportEvent::FraudReport,
mail_auth::report::FeedbackType::NotSpam => IncomingReportEvent::NotSpamReport,
mail_auth::report::FeedbackType::Other => IncomingReportEvent::OtherReport,
mail_auth::report::FeedbackType::Virus => IncomingReportEvent::VirusReport,
}),
RangeFrom = trc::Value::Timestamp(
self.arrival_date()
.map(|d| d as u64)
.unwrap_or_else(|| { now() })
),
Domain = self
.reported_domain()
.iter()
.map(|d| trc::Value::String(d.as_ref().into()))
.collect::<Vec<_>>(),
Hostname = self.reporting_mta().map(|d| trc::Value::String(d.into())),
Url = self
.reported_uri()
.iter()
.map(|d| trc::Value::String(d.as_ref().into()))
.collect::<Vec<_>>(),
RemoteIp = self.source_ip(),
Total = self.incidents(),
Result = format!("{:?}", self.delivery_result()),
Details = self
.authentication_results()
.iter()
.map(|d| trc::Value::String(d.as_ref().into()))
.collect::<Vec<_>>(),
);
}
}
+412
View File
@@ -0,0 +1,412 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use registry::{
schema::{
enums::DmarcActionDisposition,
prelude::{ObjectType, Property},
structs::{
ArfExternalReport, DmarcExternalReport, DmarcInternalReport, Task, TaskDmarcReport,
TaskStatus, TaskTlsReport, TlsExternalReport, TlsInternalReport,
},
},
types::{
EnumImpl, ObjectImpl,
datetime::UTCDateTime,
id::ObjectId,
index::{IndexBuilder, IndexValue},
},
};
use store::{
SerializeInfallible, U64_LEN,
registry::ObjectIdVersioned,
write::{
BatchBuilder, RegistryClass, TaskQueueClass, ValueClass, assert::AssertValue,
key::KeySerializer,
},
xxhash_rust::xxh3::Xxh3,
};
use types::id::Id;
pub trait InternalReportIndex: ObjectImpl {
fn deliver_at(&self) -> UTCDateTime;
fn set_deliver_at(&mut self, at: UTCDateTime);
fn task(&self, item_id: u64) -> Task;
fn primary_key(&self) -> ValueClass;
fn reschedule_ops(
&mut self,
batch: &mut BatchBuilder,
item_id: u64,
revision: u64,
at: UTCDateTime,
) {
let current_deliver_at = self.deliver_at();
if current_deliver_at != at {
let object = Self::OBJECT;
let object_id = object.to_id();
let key = ValueClass::Registry(RegistryClass::Item { object_id, item_id });
self.set_deliver_at(at);
batch
.assert_value(key.clone(), AssertValue::Hash(revision))
.clear(ValueClass::TaskQueue(TaskQueueClass::Due {
id: item_id,
due: current_deliver_at.timestamp() as u64,
}))
.set(
ValueClass::TaskQueue(TaskQueueClass::Due {
id: item_id,
due: at.timestamp() as u64,
}),
object_id.serialize(),
)
.set(key, self.to_pickled_vec());
}
}
fn write_ops(&self, batch: &mut BatchBuilder, item_id: u64, is_set: bool) {
let object = Self::OBJECT;
let object_id = object.to_id();
let pk = self.primary_key();
if is_set {
batch
.assert_value(pk.clone(), ())
.set(
pk,
ObjectIdVersioned {
object_id: ObjectId::new(object, item_id.into()),
version: 0,
}
.serialize(),
)
.schedule_task_with_id(item_id, self.task(item_id));
} else {
batch
.clear(ValueClass::Registry(RegistryClass::Item {
object_id,
item_id,
}))
.clear(pk)
.clear(ValueClass::TaskQueue(TaskQueueClass::Task { id: item_id }))
.clear(ValueClass::TaskQueue(TaskQueueClass::Due {
id: item_id,
due: self.deliver_at().timestamp() as u64,
}));
}
}
}
pub trait ExternalReportIndex: ObjectImpl {
fn text(&self) -> impl Iterator<Item = &str>;
fn tenant_id(&self) -> Option<Id>;
fn expires_at(&self) -> u64;
fn domains(&self) -> impl Iterator<Item = &str>;
fn success_fail_count(&self) -> (u64, u64);
fn unique_key(&self) -> Option<[u8; 16]>;
fn write_ops(&self, batch: &mut BatchBuilder, item_id: u64, is_set: bool) {
let object_id = Self::OBJECT.to_id();
let mut index_builder = IndexBuilder::default();
for text in self.text() {
index_builder.text(Property::Text, text);
}
if let Some(tenant_id) = self.tenant_id() {
index_builder.search(Property::MemberTenantId, tenant_id.id());
}
let (success_count, fail_count) = self.success_fail_count();
index_builder.search(Property::TotalSuccessfulSessions, success_count);
index_builder.search(Property::TotalFailedSessions, fail_count);
index_builder.search(Property::ExpiresAt, self.expires_at());
if let Some(unique_key) = self.unique_key() {
index_builder.unique(Property::ReportId, IndexValue::Bytes(unique_key.to_vec()));
}
batch.registry_index(object_id, item_id, index_builder.keys.iter(), is_set);
let key = ValueClass::Registry(RegistryClass::Item { object_id, item_id });
if is_set {
batch.set(key, self.to_pickled_vec());
} else {
batch.clear(key);
}
}
}
impl InternalReportIndex for DmarcInternalReport {
fn deliver_at(&self) -> UTCDateTime {
self.deliver_at
}
fn set_deliver_at(&mut self, at: UTCDateTime) {
self.deliver_at = at;
}
fn task(&self, item_id: u64) -> Task {
Task::DmarcReport(TaskDmarcReport {
report_id: item_id.into(),
status: TaskStatus::at(self.deliver_at.timestamp()),
})
}
fn primary_key(&self) -> ValueClass {
ValueClass::Registry(RegistryClass::PrimaryKey {
object_id: ObjectType::DmarcInternalReport.to_id().into(),
index_id: Property::Domain.to_id(),
key: KeySerializer::new(self.domain.len() + U64_LEN)
.write(self.domain.as_str())
.write(self.policy_identifier)
.finalize(),
})
}
}
impl InternalReportIndex for TlsInternalReport {
fn deliver_at(&self) -> UTCDateTime {
self.deliver_at
}
fn set_deliver_at(&mut self, at: UTCDateTime) {
self.deliver_at = at;
}
fn task(&self, item_id: u64) -> Task {
Task::TlsReport(TaskTlsReport {
report_id: item_id.into(),
status: TaskStatus::at(self.deliver_at.timestamp()),
})
}
fn primary_key(&self) -> ValueClass {
ValueClass::Registry(RegistryClass::PrimaryKey {
object_id: ObjectType::TlsInternalReport.to_id().into(),
index_id: Property::Domain.to_id(),
key: self.domain.as_bytes().to_vec(),
})
}
}
impl ExternalReportIndex for ArfExternalReport {
fn domains(&self) -> impl Iterator<Item = &str> {
let report = &self.report;
report
.reported_domains
.iter()
.filter_map(|s| non_empty(s))
.chain(
[report.dkim_domain.as_deref()]
.into_iter()
.flatten()
.filter_map(non_empty),
)
}
fn text(&self) -> impl Iterator<Item = &str> {
let report = &self.report;
report
.reported_domains
.iter()
.filter_map(|s| non_empty(s))
.chain(
[
report.dkim_domain.as_deref(),
report.reporting_mta.as_deref(),
report.original_mail_from.as_deref(),
report.original_rcpt_to.as_deref(),
]
.into_iter()
.flatten()
.filter_map(non_empty),
)
.chain(non_empty(&self.from))
}
fn tenant_id(&self) -> Option<Id> {
self.member_tenant_id
}
fn expires_at(&self) -> u64 {
self.expires_at.timestamp() as u64
}
fn success_fail_count(&self) -> (u64, u64) {
(self.report.incidents, 0)
}
fn unique_key(&self) -> Option<[u8; 16]> {
None
}
}
impl ExternalReportIndex for DmarcExternalReport {
fn domains(&self) -> impl Iterator<Item = &str> {
let report = &self.report;
non_empty(&report.policy_domain)
.into_iter()
.filter_map(non_empty)
}
fn text(&self) -> impl Iterator<Item = &str> {
let report = &self.report;
non_empty(&report.email)
.into_iter()
.filter_map(non_empty)
.chain(non_empty(&report.policy_domain))
.chain(report.records.iter().flat_map(|r| {
r.envelope_to
.as_deref()
.into_iter()
.filter_map(non_empty)
.chain(non_empty(&r.envelope_from))
.chain(non_empty(&r.header_from))
.chain(r.dkim_results.iter().filter_map(|d| non_empty(&d.domain)))
.chain(r.spf_results.iter().filter_map(|s| non_empty(&s.domain)))
}))
.chain(non_empty(&self.from))
}
fn tenant_id(&self) -> Option<Id> {
self.member_tenant_id
}
fn expires_at(&self) -> u64 {
self.expires_at.timestamp() as u64
}
fn success_fail_count(&self) -> (u64, u64) {
let mut success_count = 0;
let mut fail_count = 0;
for record in self.report.records.iter() {
if record.evaluated_disposition == DmarcActionDisposition::Pass {
success_count += std::cmp::min(record.count, 1);
} else {
fail_count += std::cmp::min(record.count, 1);
}
}
(success_count, fail_count)
}
fn unique_key(&self) -> Option<[u8; 16]> {
let report = &self.report;
Some(report_key(
[
report.org_name.as_str(),
report.policy_domain.as_str(),
report.report_id.as_str(),
],
report.date_range_begin,
report.date_range_end,
))
}
}
impl ExternalReportIndex for TlsExternalReport {
fn domains(&self) -> impl Iterator<Item = &str> {
let report = &self.report;
report
.policies
.iter()
.flat_map(|p| non_empty(&p.policy_domain).into_iter())
}
fn text(&self) -> impl Iterator<Item = &str> {
let report = &self.report;
report
.policies
.iter()
.flat_map(|p| {
non_empty(&p.policy_domain)
.into_iter()
.chain(p.mx_hosts.iter().filter_map(|s| non_empty(s)))
.chain(p.failure_details.iter().flat_map(|fd| {
non_empty_opt(&fd.receiving_mx_hostname)
.into_iter()
.chain(non_empty_opt(&fd.receiving_mx_helo))
}))
})
.chain(non_empty(&self.from))
}
fn tenant_id(&self) -> Option<Id> {
self.member_tenant_id
}
fn expires_at(&self) -> u64 {
self.expires_at.timestamp() as u64
}
fn success_fail_count(&self) -> (u64, u64) {
let mut success_count = 0;
let mut fail_count = 0;
for policy in self.report.policies.iter() {
success_count += std::cmp::min(policy.total_successful_sessions, 1);
fail_count += std::cmp::min(policy.total_failed_sessions, 1);
}
(success_count, fail_count)
}
fn unique_key(&self) -> Option<[u8; 16]> {
let report = &self.report;
Some(report_key(
[
report.organization_name.as_deref().unwrap_or_default(),
report.report_id.as_str(),
],
report.date_range_start,
report.date_range_end,
))
}
}
fn report_key<const N: usize>(fields: [&str; N], from: UTCDateTime, to: UTCDateTime) -> [u8; 16] {
let mut hasher = Xxh3::new();
for field in fields {
hasher.update(field.as_bytes());
hasher.update(&[0u8]);
}
hasher.update(&(from.timestamp() as u64).to_be_bytes());
hasher.update(&(to.timestamp() as u64).to_be_bytes());
hasher.digest128().to_be_bytes()
}
#[inline(always)]
fn non_empty(s: &str) -> Option<&str> {
if s.is_empty() { None } else { Some(s) }
}
#[inline(always)]
fn non_empty_opt(s: &Option<String>) -> Option<&str> {
s.as_deref().filter(|s| !s.is_empty())
}
+73
View File
@@ -0,0 +1,73 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::config::smtp::report::AggregateFrequency;
use mail_parser::DateTime;
use std::time::SystemTime;
pub mod analysis;
pub mod dkim;
pub mod dmarc;
pub mod inbound;
pub mod index;
pub mod scheduler;
pub mod send;
pub mod spf;
pub mod tls;
pub trait AggregateTimestamp {
fn to_timestamp(&self) -> u64;
fn to_timestamp_(&self, dt: DateTime) -> u64;
fn as_secs(&self) -> u64;
fn due(&self) -> u64;
}
impl AggregateTimestamp for AggregateFrequency {
fn to_timestamp(&self) -> u64 {
self.to_timestamp_(DateTime::from_timestamp(
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |d| d.as_secs()) as i64,
))
}
fn to_timestamp_(&self, mut dt: DateTime) -> u64 {
(match self {
AggregateFrequency::Hourly => {
dt.minute = 0;
dt.second = 0;
dt.to_timestamp()
}
AggregateFrequency::Daily => {
dt.hour = 0;
dt.minute = 0;
dt.second = 0;
dt.to_timestamp()
}
AggregateFrequency::Weekly => {
let dow = dt.day_of_week();
dt.hour = 0;
dt.minute = 0;
dt.second = 0;
dt.to_timestamp() - (86400 * dow as i64)
}
AggregateFrequency::Never => dt.to_timestamp(),
}) as u64
}
fn as_secs(&self) -> u64 {
match self {
AggregateFrequency::Hourly => 3600,
AggregateFrequency::Daily => 86400,
AggregateFrequency::Weekly => 7 * 86400,
AggregateFrequency::Never => 0,
}
}
fn due(&self) -> u64 {
self.to_timestamp() + self.as_secs()
}
}
+29
View File
@@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{dmarc::DmarcReporting, tls::TlsReporting};
use common::{BuildServer, Inner, ipc::ReportingEvent};
use std::sync::Arc;
use tokio::sync::mpsc;
pub trait SpawnReport {
fn spawn(self, core: Arc<Inner>);
}
impl SpawnReport for mpsc::Receiver<ReportingEvent> {
fn spawn(mut self, inner: Arc<Inner>) {
tokio::spawn(async move {
while let Some(event) = self.recv().await {
let server = inner.build_server();
match event {
ReportingEvent::Dmarc(event) => server.schedule_dmarc(event).await,
ReportingEvent::Tls(event) => server.schedule_tls(event).await,
ReportingEvent::Stop => break,
}
}
});
}
}
+150
View File
@@ -0,0 +1,150 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
inbound::dkim::DkimSign,
queue::{
MessageSource,
spool::{QueueParams, SmtpSpool},
},
};
use common::{Server, expr::if_block::IfBlock, ipc::ReportingEvent};
pub trait MtaReportSend: Sync + Send {
fn is_local_report_domain(
&self,
domain: &str,
session_id: u64,
) -> impl Future<Output = bool> + Send;
fn send_report(
&self,
from_addr: &str,
rcpts: impl Iterator<Item = impl AsRef<str> + Sync + Send> + Sync + Send,
report: Vec<u8>,
sign_config: &IfBlock,
deliver_now: bool,
parent_session_id: u64,
) -> impl Future<Output = ()> + Send;
fn send_autogenerated(
&self,
from_addr: impl AsRef<str> + Sync + Send,
rcpts: impl Iterator<Item = impl AsRef<str> + Sync + Send> + Sync + Send,
raw_message: Vec<u8>,
sign_config: Option<&IfBlock>,
parent_session_id: u64,
) -> impl Future<Output = ()> + Send;
fn schedule_report(
&self,
report: impl Into<ReportingEvent> + Sync + Send,
) -> impl Future<Output = ()> + Send;
}
impl MtaReportSend for Server {
async fn is_local_report_domain(&self, domain: &str, session_id: u64) -> bool {
match self.domain(domain).await {
Ok(domain) => domain.is_some(),
Err(err) => {
trc::error!(
err.caused_by(trc::location!())
.span_id(session_id)
.details("Failed to lookup local domain")
);
false
}
}
}
async fn send_report(
&self,
from_addr: &str,
rcpts: impl Iterator<Item = impl AsRef<str> + Sync + Send> + Sync + Send,
report: Vec<u8>,
sign_config: &IfBlock,
deliver_now: bool,
parent_session_id: u64,
) {
// Build message
let mut message = self.new_message(from_addr, MessageSource::Report, parent_session_id);
for rcpt_ in rcpts {
message.add_expanded_recipient(rcpt_.as_ref(), self).await;
}
// Schedule delivery at a random time between now and the next 3 hours
if !deliver_now {
#[cfg(not(feature = "test_mode"))]
{
use common::config::smtp::queue::QueueExpiry;
use rand::RngExt;
let delivery_time = rand::rng().random_range(0u64..10800u64);
for rcpt in &mut message.message.recipients {
rcpt.retry.due += delivery_time;
rcpt.notify.due += delivery_time;
if let QueueExpiry::Ttl(expires) = &mut rcpt.expires {
*expires += delivery_time;
}
}
}
}
// Queue message
let dkim_signers = self
.eval_signers(sign_config, &message.message, parent_session_id)
.await;
message
.queue(
QueueParams::new(&report, parent_session_id, self).with_dkim_signers(dkim_signers),
)
.await;
}
async fn send_autogenerated(
&self,
from_addr: impl AsRef<str> + Sync + Send,
rcpts: impl Iterator<Item = impl AsRef<str> + Sync + Send> + Sync + Send,
raw_message: Vec<u8>,
sign_config: Option<&IfBlock>,
parent_session_id: u64,
) {
// Build message
let mut message = self.new_message(
from_addr.as_ref(),
MessageSource::Autogenerated,
parent_session_id,
);
for rcpt in rcpts {
message.add_expanded_recipient(rcpt, self).await;
}
// Queue message
let dkim_signers = if let Some(sign_config) = sign_config {
self.eval_signers(sign_config, &message.message, parent_session_id)
.await
} else {
None
};
message
.queue(
QueueParams::new(&raw_message, parent_session_id, self)
.with_dkim_signers(dkim_signers),
)
.await;
}
async fn schedule_report(&self, report: impl Into<ReportingEvent> + Sync + Send) {
if self.inner.ipc.report_tx.send(report.into()).await.is_err() {
trc::event!(
Server(trc::ServerEvent::ThreadError),
CausedBy = trc::location!(),
Details = "Failed to send event to ReportScheduler"
);
}
}
}
+101
View File
@@ -0,0 +1,101 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{core::Session, reporting::send::MtaReportSend};
use common::network::SessionStream;
use mail_auth::{AuthenticationResults, SpfOutput, report::AuthFailureType};
use registry::schema::structs::Rate;
use trc::OutgoingReportEvent;
impl<T: SessionStream> Session<T> {
pub async fn send_spf_report(
&self,
rcpt: &str,
rate: &Rate,
rejected: bool,
output: &SpfOutput,
) {
// Throttle recipient
if !self.throttle_rcpt(rcpt, rate, "spf").await {
trc::event!(
OutgoingReport(OutgoingReportEvent::SpfRateLimited),
SpanId = self.data.session_id,
To = rcpt.to_string(),
Limit = vec![
trc::Value::from(rate.count),
trc::Value::from(rate.period.into_inner())
],
);
return;
}
// Generate report
let config = &self.server.core.smtp.report.spf;
let from_addr = self
.server
.eval_if(&config.address, self, self.data.session_id)
.await
.unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string());
let mut report = Vec::with_capacity(128);
self.new_auth_failure(AuthFailureType::Spf, rejected)
.with_authentication_results(
if let Some(mail_from) = &self.data.mail_from {
AuthenticationResults::new(&self.hostname).with_spf_mailfrom_result(
output,
self.data.remote_ip,
&mail_from.address,
&self.data.helo_domain,
)
} else {
AuthenticationResults::new(&self.hostname).with_spf_ehlo_result(
output,
self.data.remote_ip,
&self.data.helo_domain,
)
}
.to_string(),
)
.with_spf_dns(format!("txt : {} : v=SPF1", output.domain())) // TODO use DNS record
.write_rfc5322(
(
self.server
.eval_if(&config.name, self, self.data.session_id)
.await
.unwrap_or_else(|| "Mailer Daemon".to_string())
.as_str(),
from_addr.as_str(),
),
rcpt,
&self
.server
.eval_if(&config.subject, self, self.data.session_id)
.await
.unwrap_or_else(|| "SPF Report".to_string()),
&mut report,
)
.ok();
trc::event!(
OutgoingReport(OutgoingReportEvent::SpfReport),
SpanId = self.data.session_id,
To = rcpt.to_string(),
From = from_addr.to_string(),
);
// Send report
self.server
.send_report(
&from_addr,
[rcpt].into_iter(),
report,
&config.sign,
true,
self.data.session_id,
)
.await;
}
}
+493
View File
@@ -0,0 +1,493 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::AggregateTimestamp;
use crate::{
queue::RecipientDomain,
reporting::{index::InternalReportIndex, send::MtaReportSend},
};
use common::{
Server, USER_AGENT,
config::smtp::{
report::AggregateFrequency,
resolver::{Mode, MxPattern, TlsaMatching},
},
ipc::{TlsEvent, ToHash},
};
use mail_auth::{
flate2::{Compression, write::GzEncoder},
mta_sts::{ReportUri, TlsRpt},
report::tlsrpt::{FailureDetails, PolicyDetails},
};
use registry::{
schema::{
enums::TlsPolicyType,
prelude::{ObjectType, Property},
structs::{TlsFailureDetails, TlsInternalReport, TlsReport, TlsReportPolicy},
},
types::{EnumImpl, ObjectImpl, datetime::UTCDateTime},
};
use reqwest::header::CONTENT_TYPE;
use std::fmt::Write;
use std::{future::Future, sync::Arc, time::Duration};
use store::{
SerializeInfallible, ValueKey,
registry::ObjectIdVersioned,
write::{BatchBuilder, RegistryClass, ValueClass, assert::AssertValue},
};
use trc::{AddContext, OutgoingReportEvent};
#[derive(Debug, Clone)]
pub struct TlsRptOptions {
pub record: Arc<TlsRpt>,
pub interval: AggregateFrequency,
}
#[derive(Debug, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, serde::Serialize)]
pub struct TlsFormat {
pub rua: Vec<ReportUri>,
pub policy: PolicyDetails,
pub records: Vec<Option<FailureDetails>>,
}
#[cfg(feature = "test_mode")]
pub static TLS_HTTP_REPORT: parking_lot::Mutex<Vec<u8>> = parking_lot::Mutex::new(Vec::new());
pub trait TlsReporting: Sync + Send {
fn send_tls_aggregate_report(
&self,
report_id: u64,
) -> impl Future<Output = trc::Result<()>> + Send;
fn schedule_tls(&self, event: Box<TlsEvent>) -> impl Future<Output = ()> + Send;
}
impl TlsReporting for Server {
async fn send_tls_aggregate_report(&self, item_id: u64) -> trc::Result<()> {
let object_id = ObjectType::TlsInternalReport.to_id();
let key = ValueClass::Registry(RegistryClass::Item { object_id, item_id });
let Some(report) = self
.store()
.get_value::<TlsInternalReport>(ValueKey::from(key.clone()))
.await
.caused_by(trc::location!())?
else {
return Ok(());
};
// Delete report
let mut batch = BatchBuilder::new();
batch.clear(key).clear(RegistryClass::PrimaryKey {
object_id: object_id.into(),
index_id: Property::Domain.to_id(),
key: report.domain.as_bytes().to_vec(),
});
self.core
.storage
.data
.write(batch.build_all())
.await
.caused_by(trc::location!())?;
let domain_name = report.domain.as_str();
let event_from = report.report.date_range_start.timestamp() as u64;
let event_to = report.report.date_range_end.timestamp() as u64;
let span_id = self.inner.data.span_id_gen.generate();
trc::event!(
OutgoingReport(OutgoingReportEvent::TlsAggregate),
SpanId = span_id,
ReportId = event_from,
Domain = domain_name.to_string(),
RangeFrom = trc::Value::Timestamp(event_from),
RangeTo = trc::Value::Timestamp(event_to),
);
// Generate report
let exported_report = mail_auth::report::tlsrpt::TlsReport::from(report.report);
let json = exported_report.to_json();
let mut e = GzEncoder::new(Vec::with_capacity(json.len()), Compression::default());
let json = match std::io::Write::write_all(&mut e, json.as_bytes()).and_then(|_| e.finish())
{
Ok(report) => report,
Err(err) => {
trc::event!(
OutgoingReport(OutgoingReportEvent::SubmissionError),
SpanId = span_id,
Reason = err.to_string(),
Details = "Failed to compress report"
);
return Ok(());
}
};
// Try delivering report over HTTP
for uri in report.http_rua.as_slice() {
{
#[cfg(feature = "test_mode")]
if uri == "https://127.0.0.1/tls" {
TLS_HTTP_REPORT.lock().extend_from_slice(&json);
return Ok(());
}
match self
.core
.smtp
.tls_report_client
.post(uri)
.timeout(Duration::from_secs(2 * 60))
.header(reqwest::header::USER_AGENT, USER_AGENT)
.header(CONTENT_TYPE, "application/tlsrpt+gzip")
.body(json.to_vec())
.send()
.await
{
Ok(response) => {
if response.status().is_success() {
trc::event!(
OutgoingReport(OutgoingReportEvent::HttpSubmission),
SpanId = span_id,
Url = uri.to_string(),
Code = response.status().as_u16(),
);
return Ok(());
} else {
trc::event!(
OutgoingReport(OutgoingReportEvent::SubmissionError),
SpanId = span_id,
Url = uri.to_string(),
Code = response.status().as_u16(),
Details = "Invalid HTTP response"
);
}
}
Err(err) => {
trc::event!(
OutgoingReport(OutgoingReportEvent::SubmissionError),
SpanId = span_id,
Url = uri.to_string(),
Reason = err.to_string(),
Details = "HTTP submission error"
);
}
}
}
}
// Deliver report over SMTP
if !report.mail_rua.is_empty() {
let config = &self.core.smtp.report.tls;
let from_addr = self
.eval_if(&config.address, &RecipientDomain::new(domain_name), span_id)
.await
.unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string());
let mut message = Vec::with_capacity(2048);
let _ = exported_report.write_rfc5322_from_bytes(
domain_name,
&self
.eval_if(
&self.core.smtp.report.submitter,
&RecipientDomain::new(domain_name),
span_id,
)
.await
.unwrap_or_else(|| "localhost".to_string()),
(
self.eval_if(&config.name, &RecipientDomain::new(domain_name), span_id)
.await
.unwrap_or_else(|| "Mail Delivery Subsystem".to_string())
.as_str(),
from_addr.as_str(),
),
report.mail_rua.iter().map(|v| v.as_str()),
&json,
&mut message,
);
// Send report
self.send_report(
&from_addr,
report.mail_rua.iter().map(|v| v.as_str()),
message,
&config.sign,
false,
span_id,
)
.await;
} else {
trc::event!(
OutgoingReport(OutgoingReportEvent::NoRecipientsFound),
SpanId = span_id,
);
}
Ok(())
}
async fn schedule_tls(&self, event: Box<TlsEvent>) {
let object_id = ObjectType::TlsInternalReport.to_id();
let pk = ValueClass::Registry(RegistryClass::PrimaryKey {
object_id: object_id.into(),
index_id: Property::Domain.to_id(),
key: event.domain.as_bytes().to_vec(),
});
let mut rety_count = 0;
let policy_hash = event.policy.to_hash();
loop {
// Find the report by domain name
let mut batch = BatchBuilder::new();
let report = match self
.store()
.get_value::<ObjectIdVersioned>(ValueKey::from(pk.clone()))
.await
{
Ok(Some(object_id_v)) => {
match self
.store()
.get_value::<TlsInternalReport>(ValueKey::from(ValueClass::Registry(
RegistryClass::Item {
object_id,
item_id: object_id_v.object_id.id().id(),
},
)))
.await
{
Ok(Some(report)) => Some((object_id_v, report)),
Ok(None) => {
trc::event!(
OutgoingReport(OutgoingReportEvent::NotFound),
Id = object_id_v.object_id.id().id(),
CausedBy = trc::location!(),
Details = "Failed to find TLS report for domain"
);
return;
}
Err(err) => {
trc::error!(
err.caused_by(trc::location!())
.details("Failed to query registry for TLS report")
);
return;
}
}
}
Ok(None) => None,
Err(err) => {
trc::error!(
err.caused_by(trc::location!())
.details("Failed to query registry for TLS report")
);
return;
}
};
// Create report if missing
let config = &self.core.smtp.report.tls;
let (item_id, mut report) = if let Some((mut object_id_v, report)) = report {
batch.assert_value(pk.clone(), AssertValue::U32(object_id_v.version));
object_id_v.version += 1;
batch.set(pk.clone(), object_id_v.serialize());
(object_id_v.object_id.id().id(), report)
} else {
let item_id = self.inner.data.queue_id_gen.generate();
let date_range_start = UTCDateTime::now();
let date_range_end = UTCDateTime::from_timestamp(
date_range_start.timestamp() + event.interval.as_secs() as i64,
);
let report = TlsInternalReport {
created_at: date_range_start,
deliver_at: date_range_end,
domain: event.domain.clone(),
report: TlsReport {
report_id: format!("{}_{policy_hash}", date_range_start.timestamp()),
organization_name: self
.eval_if::<String, _>(
&config.org_name,
&RecipientDomain::new(&event.domain),
event.span_id,
)
.await
.clone(),
contact_info: self
.eval_if::<String, _>(
&config.contact_info,
&RecipientDomain::new(&event.domain),
event.span_id,
)
.await
.clone(),
date_range_end,
date_range_start,
policies: Default::default(),
},
..Default::default()
};
report.write_ops(&mut batch, item_id, true);
(item_id, report)
};
let policy = if let Some(policy) = report
.policy_identifiers
.as_slice()
.iter()
.position(|id| *id == policy_hash)
.and_then(|idx| report.report.policies.0.inner.get_mut(idx))
{
&mut policy.value
} else {
// Create policy
let mut policy = TlsReportPolicy {
policy_type: TlsPolicyType::NoPolicyFound,
policy_domain: report.domain.clone(),
..Default::default()
};
match &event.policy {
common::ipc::PolicyType::Tlsa(tlsa) => {
policy.policy_type = TlsPolicyType::Tlsa;
if let Some(tlsa) = tlsa {
for entry in &tlsa.entries {
policy.policy_strings.push(format!(
"{} {} {} {}",
if entry.is_end_entity { 3 } else { 2 },
i32::from(entry.is_spki),
match entry.matching {
TlsaMatching::Full => 0,
TlsaMatching::Sha256 => 1,
TlsaMatching::Sha512 => 2,
},
entry.data.iter().fold(
String::with_capacity(64),
|mut s, b| {
write!(s, "{b:02X}").ok();
s
}
)
));
}
}
}
common::ipc::PolicyType::Sts(sts) => {
policy.policy_type = TlsPolicyType::Sts;
if let Some(sts) = sts {
policy.policy_strings.push("version: STSv1".to_string());
policy.policy_strings.push(format!(
"mode: {}",
match sts.mode {
Mode::Enforce => "enforce",
Mode::Testing => "testing",
Mode::None => "none",
}
));
policy
.policy_strings
.push(format!("max_age: {}", sts.max_age));
for mx in &sts.mx {
let mx = match mx {
MxPattern::Equals(mx) => mx.to_string(),
MxPattern::StartsWith(mx) => format!("*.{mx}"),
};
policy.policy_strings.push(format!("mx: {mx}"));
policy.mx_hosts.push(mx);
}
}
}
_ => (),
}
for rua in &event.tls_record.rua {
match rua {
ReportUri::Mail(mail) => {
report.mail_rua.push(mail.clone());
}
ReportUri::Http(uri) => {
report.http_rua.push(uri.clone());
}
}
}
report.policy_identifiers.push(policy_hash);
report.report.policies.push(policy);
&mut report.report.policies.0.inner.last_mut().unwrap().value
};
// Add failure details
if let Some(mut failure) = event.failure.clone().map(TlsFailureDetails::from) {
if let Some(idx) = policy
.failure_details
.0
.inner
.iter()
.position(|d| d.value.eq_except_count(&failure))
{
policy.failure_details.0.inner[idx]
.value
.failed_session_count += 1;
} else {
failure.failed_session_count = 1;
policy.failure_details.push(failure);
}
policy.total_failed_sessions += 1;
} else {
policy.total_successful_sessions += 1;
}
// Write entry
let report_bytes = report.to_pickled_vec();
let max_report_size = self
.eval_if(
&config.max_size,
&RecipientDomain::new(&event.domain),
event.span_id,
)
.await
.unwrap_or(5 * 1024 * 1024);
if max_report_size != 0 && report_bytes.len() > max_report_size {
trc::event!(
OutgoingReport(OutgoingReportEvent::MaxSizeExceeded),
SpanId = event.span_id,
Domain = event.domain.clone(),
Details = report_bytes.len(),
Limit = max_report_size,
);
return;
}
batch.set(
ValueClass::Registry(RegistryClass::Item { object_id, item_id }),
report_bytes,
);
match self.core.storage.data.write(batch.build_all()).await {
Ok(_) => {
break;
}
Err(err) => {
if err.is_assertion_failure() && rety_count < 3 {
rety_count += 1;
continue;
}
trc::error!(
err.caused_by(trc::location!())
.details("Failed to write TLS report")
);
break;
}
}
}
}
}
+125
View File
@@ -0,0 +1,125 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use sieve::Envelope;
use smtp_proto::{
MAIL_BY_NOTIFY, MAIL_BY_RETURN, MAIL_BY_TRACE, MAIL_RET_FULL, MAIL_RET_HDRS, RCPT_NOTIFY_DELAY,
RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS,
};
use utils::DomainPart;
use crate::core::{SessionAddress, SessionData};
impl SessionData {
pub fn apply_envelope_modification(&mut self, envelope: Envelope, value: String) {
match envelope {
Envelope::From => {
let (address, address_lcase, domain) = if value.contains('@') {
let address_lcase = value.to_lowercase();
let domain = address_lcase.domain_part().into();
(value, address_lcase, domain)
} else if value.is_empty() {
(String::new(), String::new(), String::new())
} else {
return;
};
if let Some(mail_from) = &mut self.mail_from {
mail_from.address = address;
mail_from.address_lcase = address_lcase;
mail_from.domain = domain;
} else {
self.mail_from = SessionAddress {
address,
address_lcase,
domain,
flags: 0,
dsn_info: None,
}
.into();
}
}
Envelope::To => {
if value.contains('@') {
let address_lcase = value.to_lowercase();
let domain = address_lcase.domain_part().into();
if let Some(rcpt_to) = self.rcpt_to.last_mut() {
rcpt_to.address = value;
rcpt_to.address_lcase = address_lcase;
rcpt_to.domain = domain;
} else {
self.rcpt_to.push(SessionAddress {
address: value,
address_lcase,
domain,
flags: 0,
dsn_info: None,
});
}
}
}
Envelope::ByMode => {
if let Some(mail_from) = &mut self.mail_from {
mail_from.flags &= !(MAIL_BY_NOTIFY | MAIL_BY_RETURN);
if value == "N" {
mail_from.flags |= MAIL_BY_NOTIFY;
} else if value == "R" {
mail_from.flags |= MAIL_BY_RETURN;
}
}
}
Envelope::ByTrace => {
if let Some(mail_from) = &mut self.mail_from {
if value == "T" {
mail_from.flags |= MAIL_BY_TRACE;
} else {
mail_from.flags &= !MAIL_BY_TRACE;
}
}
}
Envelope::Notify => {
if let Some(rcpt_to) = self.rcpt_to.last_mut() {
rcpt_to.flags &= !(RCPT_NOTIFY_DELAY
| RCPT_NOTIFY_FAILURE
| RCPT_NOTIFY_SUCCESS
| RCPT_NOTIFY_NEVER);
if value == "NEVER" {
rcpt_to.flags |= RCPT_NOTIFY_NEVER;
} else {
for value in value.split(',') {
match value.trim() {
"SUCCESS" => rcpt_to.flags |= RCPT_NOTIFY_SUCCESS,
"FAILURE" => rcpt_to.flags |= RCPT_NOTIFY_FAILURE,
"DELAY" => rcpt_to.flags |= RCPT_NOTIFY_DELAY,
_ => (),
}
}
}
}
}
Envelope::Ret => {
if let Some(mail_from) = &mut self.mail_from {
mail_from.flags &= !(MAIL_RET_FULL | MAIL_RET_HDRS);
if value == "FULL" {
mail_from.flags |= MAIL_RET_FULL;
} else if value == "HDRS" {
mail_from.flags |= MAIL_RET_HDRS;
}
}
}
Envelope::Orcpt => {
if let Some(rcpt_to) = self.rcpt_to.last_mut() {
rcpt_to.dsn_info = value.into();
}
}
Envelope::Envid => {
if let Some(mail_from) = &mut self.mail_from {
mail_from.dsn_info = value.into();
}
}
Envelope::ByTimeAbsolute | Envelope::ByTimeRelative => (),
}
}
}
+435
View File
@@ -0,0 +1,435 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::queue::{
MessageSource,
quota::HasQueueQuota,
spool::{QueueParams, SmtpSpool},
};
use common::{Server, config::smtp::queue::QueueExpiry, scripts::plugins::PluginContext};
use mail_parser::{Encoding, Message, MessagePart, PartType};
use sieve::{
Event, Input, MatchAs, Recipient, Sieve,
compiler::grammar::actions::action_redirect::{ByMode, ByTime, Notify, NotifyItem, Ret},
};
use smtp_proto::{
MAIL_BY_TRACE, MAIL_RET_FULL, MAIL_RET_HDRS, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE,
RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS,
};
use std::{future::Future, sync::Arc, time::Instant};
use trc::SieveEvent;
use super::{ScriptModification, ScriptParameters, ScriptResult};
pub trait RunScript: Sync + Send {
fn run_script(
&self,
script_id: String,
script: Arc<Sieve>,
params: ScriptParameters<'_>,
) -> impl Future<Output = ScriptResult> + Send;
}
impl RunScript for Server {
async fn run_script(
&self,
script_id: String,
script: Arc<Sieve>,
params: ScriptParameters<'_>,
) -> ScriptResult {
// Create filter instance
let time = Instant::now();
let mut instance = self
.core
.sieve
.trusted_runtime
.filter_parsed(params.message.unwrap_or_else(|| Message {
parts: vec![MessagePart {
headers: vec![],
is_encoding_problem: false,
body: PartType::Text("".into()),
encoding: Encoding::None,
offset_header: 0,
offset_body: 0,
offset_end: 0,
}],
raw_message: b""[..].into(),
..Default::default()
}))
.with_vars_env(params.variables)
.with_envelope_list(params.envelope)
.with_user_address(&params.from_addr)
.with_user_full_name(&params.from_name);
if let Some(spam_status) = params.spam_status {
instance.set_spam_status(spam_status);
}
let mut input = Input::script("__script", script);
let mut messages: Vec<Vec<u8>> = Vec::new();
let session_id = params.session_id;
let mut reject_reason = None;
let mut modifications = vec![];
let mut keep_id = usize::MAX;
// Start event loop
while let Some(result) = instance.run(input) {
match result {
Ok(event) => match event {
Event::IncludeScript { name, optional } => {
if let Some(script) = self.core.sieve.trusted_script(name.as_str()) {
input = Input::script(name, script.clone());
} else if optional {
input = false.into();
} else {
trc::event!(
Sieve(SieveEvent::ScriptNotFound),
Id = script_id.clone(),
SpanId = session_id,
Details = name.as_str().to_string(),
);
break;
}
}
Event::ListContains {
lists,
values,
match_as,
} => {
input = false.into();
'outer: for list in lists {
if let Some(store) = self.get_lookup_store(&list) {
for value in &values {
if let Ok(true) = store
.key_exists(if !matches!(match_as, MatchAs::Lowercase) {
value.clone()
} else {
value.to_lowercase()
})
.await
{
input = true.into();
break 'outer;
}
}
} else {
trc::event!(
Sieve(SieveEvent::ListNotFound),
Id = script_id.clone(),
SpanId = session_id,
Details = list,
);
}
}
}
Event::Function { id, arguments } => {
input = self
.core
.run_plugin(
id,
PluginContext {
session_id,
server: self,
message: instance.message(),
modifications: &mut modifications,
access_token: params.access_token,
arguments,
},
)
.await;
}
Event::Keep { message_id, .. } => {
keep_id = message_id;
input = true.into();
}
Event::Discard => {
keep_id = usize::MAX - 1;
input = true.into();
}
Event::Reject { reason, .. } => {
reject_reason = reason.into();
input = true.into();
}
Event::SendMessage {
recipient,
notify,
return_of_content,
by_time,
message_id,
} => {
// Build message
let mut message = self.new_message(
params.return_path.as_str(),
MessageSource::Autogenerated,
session_id,
);
match recipient {
Recipient::Address(rcpt) => {
message.expand_and_add_recipient(rcpt, self).await;
}
Recipient::Group(rcpt_list) => {
for rcpt in rcpt_list {
message.expand_and_add_recipient(rcpt, self).await;
}
}
Recipient::List(list) => {
trc::event!(
Sieve(SieveEvent::NotSupported),
Id = script_id.clone(),
SpanId = session_id,
Details = list,
Reason = "Sending to lists is not supported.",
);
}
}
// Set notify flags
let mut flags = 0;
match notify {
Notify::Never => {
flags = RCPT_NOTIFY_NEVER;
}
Notify::Items(items) => {
for item in items {
flags |= match item {
NotifyItem::Success => RCPT_NOTIFY_SUCCESS,
NotifyItem::Failure => RCPT_NOTIFY_FAILURE,
NotifyItem::Delay => RCPT_NOTIFY_DELAY,
};
}
}
Notify::Default => (),
}
if flags > 0 {
for rcpt in &mut message.message.recipients {
rcpt.flags |= flags;
}
}
// Set ByTime flags
match by_time {
ByTime::Relative {
rlimit,
mode,
trace,
} => {
if trace {
message.message.flags |= MAIL_BY_TRACE;
}
match mode {
ByMode::Notify => {
for domain in &mut message.message.recipients {
domain.notify.due += rlimit;
}
}
ByMode::Return => {
for domain in &mut message.message.recipients {
domain.notify.due += rlimit;
}
}
ByMode::Default => (),
}
}
ByTime::Absolute {
alimit,
mode,
trace,
} => {
if trace {
message.message.flags |= MAIL_BY_TRACE;
}
match mode {
ByMode::Notify => {
for domain in &mut message.message.recipients {
domain.notify.due = alimit as u64;
}
}
ByMode::Return => {
let expires =
(alimit as u64).saturating_sub(message.message.created);
if expires > 0 {
for domain in &mut message.message.recipients {
domain.expires = QueueExpiry::Ttl(expires);
}
}
}
ByMode::Default => (),
}
}
ByTime::None => (),
};
// Set ret
match return_of_content {
Ret::Full => {
message.message.flags |= MAIL_RET_FULL;
}
Ret::Hdrs => {
message.message.flags |= MAIL_RET_HDRS;
}
Ret::Default => (),
}
// Queue message
let is_forward = message_id == 0;
let raw_message = if !is_forward {
messages.get(message_id - 1).map(|m| m.as_slice())
} else {
instance.message().raw_message().into()
};
if let Some(raw_message) = raw_message.filter(|m| !m.is_empty()) {
if let Some(metadata) = self.has_quota(&mut message).await {
let dkim_signers = if let Some(sign_domain) = &params.sign_domain {
match self.dkim_signers(sign_domain).await {
Ok(signers) => signers,
Err(err) => {
trc::error!(
err.details("Failed to obtain DKIM signers")
.caused_by(trc::location!())
);
None
}
}
} else {
None
};
message
.queue(
QueueParams::new(raw_message, session_id, self)
.with_dkim_signers(dkim_signers)
.with_raw_headers_opt(
params.headers.filter(|_| is_forward),
)
.with_original_raw_message(
instance.message().raw_message(),
)
.with_metadata(metadata),
)
.await;
} else {
trc::event!(
Sieve(SieveEvent::QuotaExceeded),
SpanId = session_id,
Id = script_id.clone(),
From = message.message.return_path,
To = message
.message
.recipients
.into_iter()
.map(|r| trc::Value::from(r.address().to_string()))
.collect::<Vec<_>>(),
);
}
}
input = true.into();
}
Event::CreatedMessage { message, .. } => {
messages.push(message);
input = true.into();
}
Event::SetEnvelope { envelope, value } => {
modifications.push(ScriptModification::SetEnvelope {
name: envelope,
value,
});
input = true.into();
}
unsupported => {
trc::event!(
Sieve(SieveEvent::NotSupported),
Id = script_id.clone(),
SpanId = session_id,
Reason = "Unsupported event",
Details = format!("{unsupported:?}"),
);
break;
}
},
Err(err) => {
trc::event!(
Sieve(SieveEvent::RuntimeError),
Id = script_id.clone(),
SpanId = session_id,
Reason = err.to_string(),
);
break;
}
}
}
// Keep id
// 0 = use original message
// MAX = implicit keep
// MAX - 1 = discard message
if keep_id == 0 {
trc::event!(
Sieve(SieveEvent::ActionAccept),
SpanId = session_id,
Id = script_id,
Elapsed = time.elapsed(),
);
ScriptResult::Accept { modifications }
} else if let Some(mut reject_reason) = reject_reason {
trc::event!(
Sieve(SieveEvent::ActionReject),
Id = script_id,
SpanId = session_id,
Details = reject_reason.clone(),
Elapsed = time.elapsed(),
);
if !reject_reason.ends_with('\n') {
reject_reason.push_str("\r\n");
}
let mut reject_bytes = reject_reason.as_bytes().iter();
if matches!(reject_bytes.next(), Some(ch) if ch.is_ascii_digit())
&& matches!(reject_bytes.next(), Some(ch) if ch.is_ascii_digit())
&& matches!(reject_bytes.next(), Some(ch) if ch.is_ascii_digit())
&& matches!(reject_bytes.next(), Some(ch) if ch == &b' ' )
{
ScriptResult::Reject(reject_reason)
} else {
ScriptResult::Reject(format!("503 5.5.3 {reject_reason}"))
}
} else if keep_id != usize::MAX - 1 {
if let Some(message) = messages.into_iter().nth(keep_id - 1) {
trc::event!(
Sieve(SieveEvent::ActionAccept),
SpanId = session_id,
Id = script_id,
Elapsed = time.elapsed(),
);
ScriptResult::Replace {
message,
modifications,
}
} else {
trc::event!(
Sieve(SieveEvent::ActionAcceptReplace),
SpanId = session_id,
Id = script_id,
Elapsed = time.elapsed(),
);
ScriptResult::Accept { modifications }
}
} else {
trc::event!(
Sieve(SieveEvent::ActionDiscard),
SpanId = session_id,
Id = script_id,
Elapsed = time.elapsed()
);
ScriptResult::Discard
}
}
}
+175
View File
@@ -0,0 +1,175 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{sync::Arc, time::SystemTime};
use common::network::SessionStream;
use mail_auth::common::resolver::ToReverseName;
use sieve::{Envelope, Sieve, runtime::Variable};
use smtp_proto::*;
use crate::{core::Session, inbound::AuthResult};
use super::{ScriptParameters, ScriptResult, event_loop::RunScript};
impl<T: SessionStream> Session<T> {
pub fn build_script_parameters(&self, stage: &'static str) -> ScriptParameters<'_> {
let (tls_version, tls_cipher) = self.stream.tls_version_and_cipher();
let mut params = ScriptParameters::new()
.set_variable("remote_ip", self.data.remote_ip.to_string())
.set_variable("remote_ip.reverse", self.data.remote_ip.to_reverse_name())
.set_variable("helo_domain", self.data.helo_domain.as_str().to_lowercase())
.set_variable(
"authenticated_as",
self.authenticated_as().unwrap_or_default().to_string(),
)
.set_variable(
"now",
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |d| d.as_secs()),
)
.set_variable(
"asn",
self.data
.asn_geo_data
.asn
.as_ref()
.map(|r| r.id)
.unwrap_or_default(),
)
.set_variable(
"country",
self.data
.asn_geo_data
.country
.as_ref()
.map(|r| r.as_str())
.unwrap_or_default(),
)
.set_variable(
"spf.result",
self.data
.spf_mail_from
.as_ref()
.map(|r| r.result().as_str())
.unwrap_or_default(),
)
.set_variable(
"spf_ehlo.result",
self.data
.spf_ehlo
.as_ref()
.map(|r| r.result().as_str())
.unwrap_or_default(),
)
.set_variable("tls.version", tls_version)
.set_variable("tls.cipher", tls_cipher)
.set_variable("stage", stage);
if let Some(ip_rev) = &self.data.iprev {
params = params.set_variable("iprev.result", ip_rev.result().as_str());
if let Some(ptr) = ip_rev.ptr.as_ref().and_then(|addrs| addrs.first()) {
params = params.set_variable(
"iprev.ptr",
ptr.strip_suffix('.').unwrap_or(ptr).to_lowercase(),
);
}
}
if let Some(mail_from) = &self.data.mail_from {
params
.envelope
.push((Envelope::From, mail_from.address_lcase.to_string().into()));
if let Some(env_id) = &mail_from.dsn_info {
params
.envelope
.push((Envelope::Envid, env_id.as_str().to_lowercase().into()));
}
if stage != "data" {
if let Some(rcpt) = self.data.rcpt_to.last() {
params
.envelope
.push((Envelope::To, rcpt.address_lcase.to_string().into()));
if let Some(orcpt) = &rcpt.dsn_info {
params
.envelope
.push((Envelope::Orcpt, orcpt.as_str().to_lowercase().into()));
}
}
} else {
// Build recipients list
let mut recipients = Vec::with_capacity(self.data.rcpt_to.len());
let mut orcpts = Vec::with_capacity(self.data.rcpt_to.len());
let mut has_orcpts = false;
for rcpt in &self.data.rcpt_to {
recipients.push(Variable::from(rcpt.address_lcase.to_string()));
orcpts.push(match &rcpt.dsn_info {
Some(orcpt) => {
has_orcpts = true;
Variable::from(orcpt.as_str().to_lowercase())
}
None => Variable::default(),
});
}
params.envelope.push((Envelope::To, recipients.into()));
if has_orcpts {
params.envelope.push((Envelope::Orcpt, orcpts.into()));
}
}
if (mail_from.flags & MAIL_RET_FULL) != 0 {
params.envelope.push((Envelope::Ret, "FULL".into()));
} else if (mail_from.flags & MAIL_RET_HDRS) != 0 {
params.envelope.push((Envelope::Ret, "HDRS".into()));
}
if (mail_from.flags & MAIL_BY_NOTIFY) != 0 {
params.envelope.push((Envelope::ByMode, "N".into()));
} else if (mail_from.flags & MAIL_BY_RETURN) != 0 {
params.envelope.push((Envelope::ByMode, "R".into()));
}
if (mail_from.flags & MAIL_BODY_7BIT) != 0 {
params = params.set_variable("param.body", "7bit");
} else if (mail_from.flags & MAIL_BODY_8BITMIME) != 0 {
params = params.set_variable("param.body", "8bitmime");
} else if (mail_from.flags & MAIL_BODY_BINARYMIME) != 0 {
params = params.set_variable("param.body", "binarymime");
}
if (mail_from.flags & MAIL_SMTPUTF8) != 0 {
params = params.set_variable("param.smtputf8", Variable::Integer(1));
}
if (mail_from.flags & MAIL_REQUIRETLS) != 0 {
params = params.set_variable("param.requiretls", Variable::Integer(1));
}
}
params
}
pub async fn run_script(
&self,
script_id: String,
script: Arc<Sieve>,
params: ScriptParameters<'_>,
) -> ScriptResult {
Box::pin(
self.server.run_script(
script_id,
script,
params
.with_session_id(self.data.session_id)
.with_envelope(&self.server, self, self.data.session_id)
.await,
),
)
.await
}
}
+136
View File
@@ -0,0 +1,136 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use ahash::AHashMap;
use common::{
Server, auth::AccessToken, expr::functions::ResolveVariable, scripts::ScriptModification,
};
use mail_parser::Message;
use sieve::{Envelope, SpamStatus, runtime::Variable};
pub mod envelope;
pub mod event_loop;
pub mod exec;
#[derive(Debug, serde::Serialize)]
pub enum ScriptResult {
Accept {
modifications: Vec<ScriptModification>,
},
Replace {
message: Vec<u8>,
modifications: Vec<ScriptModification>,
},
Reject(String),
Discard,
}
pub struct ScriptParameters<'x> {
message: Option<Message<'x>>,
headers: Option<&'x [u8]>,
variables: AHashMap<Cow<'static, str>, Variable>,
envelope: Vec<(Envelope, Variable)>,
from_addr: String,
from_name: String,
return_path: String,
sign_domain: Option<String>,
access_token: Option<&'x AccessToken>,
spam_status: Option<SpamStatus>,
session_id: u64,
}
impl<'x> ScriptParameters<'x> {
pub fn new() -> Self {
ScriptParameters {
variables: AHashMap::with_capacity(10),
envelope: Vec::with_capacity(6),
message: None,
headers: None,
from_addr: Default::default(),
from_name: Default::default(),
return_path: Default::default(),
sign_domain: Default::default(),
access_token: None,
spam_status: None,
session_id: Default::default(),
}
}
pub async fn with_envelope(
mut self,
server: &Server,
vars: &impl ResolveVariable,
session_id: u64,
) -> Self {
for (variable, expr) in [
(&mut self.from_addr, &server.core.sieve.from_addr),
(&mut self.from_name, &server.core.sieve.from_name),
(&mut self.return_path, &server.core.sieve.return_path),
] {
if let Some(value) = server.eval_if(expr, vars, session_id).await {
*variable = value;
}
}
self.sign_domain = server
.eval_if(&server.core.sieve.sign, vars, session_id)
.await;
self
}
pub fn with_message(self, message: Message<'x>) -> Self {
Self {
message: message.into(),
..self
}
}
pub fn with_auth_headers(self, headers: &'x [u8]) -> Self {
Self {
headers: headers.into(),
..self
}
}
pub fn with_spam_status(self, status: SpamStatus) -> Self {
Self {
spam_status: status.into(),
..self
}
}
pub fn set_variable(
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Variable>,
) -> Self {
self.variables.insert(name.into(), value.into());
self
}
pub fn set_envelope(mut self, envelope: Envelope, value: impl Into<Variable>) -> Self {
self.envelope.push((envelope, value.into()));
self
}
pub fn with_access_token(mut self, access_token: &'x AccessToken) -> Self {
self.access_token = Some(access_token);
self
}
pub fn with_session_id(mut self, session_id: u64) -> Self {
self.session_id = session_id;
self
}
}
impl Default for ScriptParameters<'_> {
fn default() -> Self {
Self::new()
}
}