This commit is contained in:
mdecimus 2024-06-22 09:15:45 +02:00
parent 7c17496cb4
commit a9a3925e88
11 changed files with 73 additions and 52 deletions

View file

@ -2,6 +2,27 @@
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
## [0.8.2] - 2024-06-22
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin and spam filter versions.
## Added
- Webhooks support (#480)
- MTA Hooks (like milter but over HTTP)
- Manually train and test spam classifier (#473 #264 #257 #471)
- Allow configuring default mailbox names, roles and subscriptions (#125 #290 #458 #498)
- Include `robots.txt` (#542)
### Changed
- Milter support on all SMTP stages (#183)
- Do not announce `STARTTLS` if the listener does not support it.
### Fixed
- Incoming reports stored in the wrong subspace (#543)
- Return `OK` after a successful ManageSieve SASL authentication flow (#187)
- Case-insensitive search in settings API (#487)
- Fix `session.rcpt.script` default variable name (#502)
## [0.8.1] - 2024-05-23
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin and spam filter versions.

View file

@ -6,9 +6,9 @@ We provide security updates for the following versions of Stalwart Mail Server:
| Version | Supported |
| ------- | ------------------ |
| 0.8.x | :white_check_mark: |
| 0.7.x | :white_check_mark: |
| 0.6.x | :white_check_mark: |
| < 0.5 | :x: |
| < 0.6 | :x: |
## Reporting a Vulnerability

View file

@ -39,7 +39,7 @@ pub struct SessionConfig {
pub mta_sts_policy: Option<Policy>,
pub milters: Vec<Milter>,
pub hooks: Vec<FilterHook>,
pub hooks: Vec<MTAHook>,
}
#[derive(Default, Debug, Clone)]
@ -173,7 +173,7 @@ pub enum MilterVersion {
}
#[derive(Clone)]
pub struct FilterHook {
pub struct MTAHook {
pub enable: IfBlock,
pub url: String,
pub timeout: Duration,
@ -582,7 +582,7 @@ fn parse_milter(config: &mut Config, id: &str, token_map: &TokenMap) -> Option<M
})
}
fn parse_hooks(config: &mut Config, id: &str, token_map: &TokenMap) -> Option<FilterHook> {
fn parse_hooks(config: &mut Config, id: &str, token_map: &TokenMap) -> Option<MTAHook> {
let mut headers = HeaderMap::new();
for (header, value) in config
@ -627,7 +627,7 @@ fn parse_hooks(config: &mut Config, id: &str, token_map: &TokenMap) -> Option<Fi
);
}
Some(FilterHook {
Some(MTAHook {
enable: IfBlock::try_parse(config, ("session.hook", id, "enable"), token_map)
.unwrap_or_else(|| {
IfBlock::new::<()>(format!("session.hook.{id}.enable"), [], "false")

View file

@ -447,18 +447,18 @@ impl<T: SessionStream> Session<T> {
}
};
// Run filter hooks
// Run MTA Hooks
match self
.run_filter_hooks(Stage::Data, (&auth_message).into())
.run_mta_hooks(Stage::Data, (&auth_message).into())
.await
{
Ok(modifications_) => {
if !modifications_.is_empty() {
tracing::debug!(
parent: &self.span,
context = "filter_hook",
context = "mta_hook",
event = "accept",
"FilterHook filter(s) accepted message.");
"MTAHook filter(s) accepted message.");
modifications.retain(|m| !matches!(m, Modification::ReplaceBody { .. }));
modifications.extend(modifications_);

View file

@ -119,10 +119,10 @@ impl<T: SessionStream> Session<T> {
return self.write(message.message.as_bytes()).await;
}
// FilterHook filtering
if let Err(message) = self.run_filter_hooks(Stage::Ehlo, None).await {
// MTAHook filtering
if let Err(message) = self.run_mta_hooks(Stage::Ehlo, None).await {
tracing::info!(parent: &self.span,
context = "filter_hook",
context = "mta_hook",
event = "reject",
domain = &self.data.helo_domain,
reason = message.message.as_ref());

View file

@ -21,21 +21,21 @@
* for more details.
*/
use common::config::smtp::session::FilterHook;
use common::config::smtp::session::MTAHook;
use super::{Request, Response};
pub(super) async fn send_filter_hook_request(
filter_hook: &FilterHook,
pub(super) async fn send_mta_hook_request(
mta_hook: &MTAHook,
request: Request,
) -> Result<Response, String> {
let response = reqwest::Client::builder()
.timeout(filter_hook.timeout)
.danger_accept_invalid_certs(filter_hook.tls_allow_invalid_certs)
.timeout(mta_hook.timeout)
.danger_accept_invalid_certs(mta_hook.tls_allow_invalid_certs)
.build()
.map_err(|err| format!("Failed to create HTTP client: {}", err))?
.post(&filter_hook.url)
.headers(filter_hook.headers.clone())
.post(&mta_hook.url)
.headers(mta_hook.headers.clone())
.body(
serde_json::to_string(&request)
.map_err(|err| format!("Failed to serialize Hook request: {}", err))?,
@ -47,7 +47,7 @@ pub(super) async fn send_filter_hook_request(
if response.status().is_success() {
if response
.content_length()
.map_or(false, |len| len as usize > filter_hook.max_response_size)
.map_or(false, |len| len as usize > mta_hook.max_response_size)
{
return Err(format!(
"Hook response too large ({} bytes)",

View file

@ -23,7 +23,7 @@
use ahash::AHashMap;
use common::{
config::smtp::session::{FilterHook, Stage},
config::smtp::session::{MTAHook, Stage},
listener::SessionStream,
DAEMON_NAME,
};
@ -40,33 +40,33 @@ use crate::{
},
};
use super::{client::send_filter_hook_request, Action, Response};
use super::{client::send_mta_hook_request, Action, Response};
impl<T: SessionStream> Session<T> {
pub async fn run_filter_hooks(
pub async fn run_mta_hooks(
&self,
stage: Stage,
message: Option<&AuthenticatedMessage<'_>>,
) -> Result<Vec<Modification>, FilterResponse> {
let filter_hooks = &self.core.core.smtp.session.hooks;
if filter_hooks.is_empty() {
let mta_hooks = &self.core.core.smtp.session.hooks;
if mta_hooks.is_empty() {
return Ok(Vec::new());
}
let mut modifications = Vec::new();
for filter_hook in filter_hooks {
if !filter_hook.run_on_stage.contains(&stage)
for mta_hook in mta_hooks {
if !mta_hook.run_on_stage.contains(&stage)
|| !self
.core
.core
.eval_if(&filter_hook.enable, self)
.eval_if(&mta_hook.enable, self)
.await
.unwrap_or(false)
{
continue;
}
match self.run_filter_hook(stage, filter_hook, message).await {
match self.run_mta_hook(stage, mta_hook, message).await {
Ok(response) => {
let mut new_modifications = Vec::with_capacity(response.modifications.len());
for modification in response.modifications {
@ -154,12 +154,12 @@ impl<T: SessionStream> Session<T> {
Err(err) => {
tracing::warn!(
parent: &self.span,
filter_hook.url = &filter_hook.url,
context = "filter_hook",
mta_hook.url = &mta_hook.url,
context = "mta_hook",
event = "error",
reason = ?err,
"FilterHook filter failed");
if filter_hook.tempfail_on_error {
"MTAHook filter failed");
if mta_hook.tempfail_on_error {
return Err(FilterResponse::server_failure());
}
}
@ -169,10 +169,10 @@ impl<T: SessionStream> Session<T> {
Ok(modifications)
}
pub async fn run_filter_hook(
pub async fn run_mta_hook(
&self,
stage: Stage,
filter_hook: &FilterHook,
mta_hook: &MTAHook,
message: Option<&AuthenticatedMessage<'_>>,
) -> Result<Response, String> {
// Build request
@ -245,7 +245,7 @@ impl<T: SessionStream> Session<T> {
}),
};
send_filter_hook_request(filter_hook, request).await
send_mta_hook_request(mta_hook, request).await
}
}

View file

@ -179,10 +179,10 @@ impl<T: SessionStream> Session<T> {
return self.write(message.message.as_bytes()).await;
}
// FilterHook filtering
if let Err(message) = self.run_filter_hooks(Stage::Mail, None).await {
// MTAHook filtering
if let Err(message) = self.run_mta_hooks(Stage::Mail, None).await {
tracing::info!(parent: &self.span,
context = "filter_hook",
context = "mta_hook",
event = "reject",
address = &self.data.mail_from.as_ref().unwrap().address,
reason = message.message.as_ref());

View file

@ -143,10 +143,10 @@ impl<T: SessionStream> Session<T> {
return self.write(message.message.as_bytes()).await;
}
// FilterHook filtering
if let Err(message) = self.run_filter_hooks(Stage::Rcpt, None).await {
// MTAHook filtering
if let Err(message) = self.run_mta_hooks(Stage::Rcpt, None).await {
tracing::info!(parent: &self.span,
context = "filter_hook",
context = "mta_hook",
event = "reject",
address = self.data.rcpt_to.last().unwrap().address,
reason = message.message.as_ref());

View file

@ -131,11 +131,11 @@ impl<T: SessionStream> Session<T> {
return false;
}
// FilterHook filtering
if let Err(message) = self.run_filter_hooks(Stage::Connect, None).await {
// MTAHook filtering
if let Err(message) = self.run_mta_hooks(Stage::Connect, None).await {
tracing::debug!(parent: &self.span,
context = "connect",
event = "filter_hook-reject",
event = "mta_hook-reject",
reason = message.message.as_ref());
let _ = self.write(message.message.as_bytes()).await;
return false;

View file

@ -247,7 +247,7 @@ async fn milter_session() {
}
#[tokio::test]
async fn filter_hook_session() {
async fn mta_hook_session() {
// Enable logging
/*let disable = "true";
tracing::subscriber::set_global_default(
@ -258,11 +258,11 @@ async fn filter_hook_session() {
.unwrap();*/
// Configure tests
let tmp_dir = TempDir::new("smtp_filter_hook_test", true);
let tmp_dir = TempDir::new("smtp_mta_hook_test", true);
let mut config = Config::new(tmp_dir.update_config(CONFIG_JMILTER)).unwrap();
let stores = Stores::parse_all(&mut config).await;
let core = Core::parse(&mut config, stores, Default::default()).await;
let _rx = spawn_mock_filter_hook_server();
let _rx = spawn_mock_mta_hook_server();
tokio::time::sleep(Duration::from_millis(100)).await;
let mut inner = Inner::default();
let mut qr = inner.init_test_queue(&core);
@ -785,7 +785,7 @@ async fn accept_milter(
}
}
pub fn spawn_mock_filter_hook_server() -> watch::Sender<bool> {
pub fn spawn_mock_mta_hook_server() -> watch::Sender<bool> {
let (tx, rx) = watch::channel(true);
let tests = Arc::new(
serde_json::from_str::<Vec<HeaderTest>>(
@ -826,7 +826,7 @@ pub fn spawn_mock_filter_hook_server() -> watch::Sender<bool> {
let request = serde_json::from_slice::<Request>(&fetch_body(&mut req, 1024 * 1024).await.unwrap())
.unwrap();
let response = handle_filter_hook(request, tests);
let response = handle_mta_hook(request, tests);
Ok::<_, hyper::Error>(
Resource {
@ -856,7 +856,7 @@ pub fn spawn_mock_filter_hook_server() -> watch::Sender<bool> {
tx
}
fn handle_filter_hook(request: Request, tests: Arc<Vec<HeaderTest>>) -> hooks::Response {
fn handle_mta_hook(request: Request, tests: Arc<Vec<HeaderTest>>) -> hooks::Response {
match request
.envelope
.unwrap()