Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,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
@@ -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()]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 ¯os.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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user