mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-11-24 06:19:46 +00:00
v0.8.2
This commit is contained in:
parent
7c17496cb4
commit
a9a3925e88
11 changed files with 73 additions and 52 deletions
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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_);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue