Fix Sieve: add Received headers to auto-generated messages and detect loops

This commit is contained in:
Maurus Decimus
2026-07-12 15:08:17 +02:00
parent f7e82c3fbc
commit b8122c8e0f
2 changed files with 53 additions and 4 deletions

View File

@@ -17,6 +17,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If
- JMAP: Return RFC-3339-conformant UTCDate literals in capabilities: min `0001-01-01T00:00:00Z`, max `9999-12-31T23:59:59Z`.
- Reject invalid duration values (e.g. `1h30m`).
- Branding: Custom logos for domains do not work.
- Sieve: add `Received` headers to auto-generated messages and detect loops.
## [0.16.12] - 2026-07-06

View File

@@ -13,8 +13,11 @@ use crate::{
ingest::{EmailIngest, IngestEmail, IngestSource, IngestedEmail},
},
};
use common::{Server, auth::AccessToken, scripts::plugins::PluginContext};
use mail_parser::MessageParser;
use common::{
Server, auth::AccessToken, expr::functions::EmptyResolver, scripts::plugins::PluginContext,
};
use mail_builder::headers::date::Date;
use mail_parser::{HeaderName, MessageParser};
use sieve::{Envelope, Event, Input, Mailbox, Recipient, Sieve, SpamStatus};
use std::{borrow::Cow, sync::Arc};
use std::{future::Future, str::FromStr};
@@ -26,7 +29,7 @@ use store::{
AlignedBytes, Archive, ArchiveVersion, Archiver, BatchBuilder, BlobLink, BlobOp, ValueClass,
},
};
use trc::{AddContext, SieveEvent};
use trc::{AddContext, SieveEvent, SmtpEvent};
use types::{
blob_hash::BlobHash,
collection::Collection,
@@ -106,6 +109,20 @@ impl SieveScriptIngest for Server {
);
};
let received_headers = message
.headers()
.iter()
.filter(|header| matches!(header.name, HeaderName::Received))
.count();
let max_received_headers = self
.eval_if(
&self.core.smtp.session.data.max_received_headers,
&EmptyResolver,
session_id,
)
.await
.unwrap_or(50);
// Obtain mailboxIds
let account_id = access_token.account_id();
let mut cache = self
@@ -387,6 +404,18 @@ impl SieveScriptIngest for Server {
} => {
input = true.into();
if let Some(message) = messages.get(message_id) {
if received_headers >= max_received_headers {
trc::event!(
Smtp(SmtpEvent::LoopDetected),
From = mail_from.clone(),
Total = received_headers,
Limit = max_received_headers,
SpanId = session_id,
);
continue;
}
let recipients: Vec<String> = match recipient {
Recipient::Address(rcpt) => vec![rcpt],
Recipient::Group(rcpts) => rcpts,
@@ -408,10 +437,19 @@ impl SieveScriptIngest for Server {
SpanId = session_id
);
let mut raw_message =
Vec::with_capacity(160 + message.raw_message.len());
write_received_header(
&mut raw_message,
&self.core.network.server_name,
session_id,
);
raw_message.extend_from_slice(message.raw_message.as_ref());
autogenerated.push(AutogeneratedMessage {
sender_address: mail_from.clone(),
recipients,
message: message.raw_message.to_vec(),
message: raw_message,
});
do_redirect = true;
} else {
@@ -750,6 +788,16 @@ impl SieveScriptIngest for Server {
}
}
fn write_received_header(buf: &mut Vec<u8>, hostname: &str, id: u64) {
buf.extend_from_slice(b"Received: from localhost (localhost [127.0.0.1])\r\n\tby ");
buf.extend_from_slice(hostname.as_bytes());
buf.extend_from_slice(b" (Stalwart SMTP) with LMTP id ");
buf.extend_from_slice(format!("{id:X}").as_bytes());
buf.extend_from_slice(b";\r\n\t");
buf.extend_from_slice(Date::now().to_rfc822().as_bytes());
buf.extend_from_slice(b"\r\n");
}
pub struct CompiledScript {
pub script: Sieve,
pub name: String,