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
+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
}
}
}