Sieve: don't answer auto-submitted mail #235
No files matched your search
@@ -348,4 +348,51 @@ if header :regex "x-h" "${p}" { discard; }
|
||||
let many = run(1000);
|
||||
assert!(many < one * 10, "1 header: {one:?}, 1000 headers: {many:?}");
|
||||
}
|
||||
|
||||
// inbuxa: vacation and notify don't answer mail a program sent (RFC 3834),
|
||||
// or two autoresponders answer each other
|
||||
#[test]
|
||||
fn sieve_auto_submitted_mail_gets_no_reply() {
|
||||
let runtime = sieve::Runtime::new();
|
||||
let sends = |script: &str, header: &str| {
|
||||
let script =
|
||||
std::sync::Arc::new(sieve::Compiler::new().compile(script.as_bytes()).unwrap());
|
||||
let message = format!(
|
||||
"From: [email protected]\r\nTo: [email protected]\r\n{header}Subject: hi\r\n\r\nbody\r\n"
|
||||
);
|
||||
let mut ctx = runtime
|
||||
.filter(message.as_bytes())
|
||||
.with_envelope(sieve::Envelope::From, "[email protected]")
|
||||
.with_envelope(sieve::Envelope::To, "[email protected]")
|
||||
.with_user_address("[email protected]");
|
||||
let mut input = sieve::Input::script("test", script);
|
||||
let mut sent = false;
|
||||
while let Some(event) = ctx.run(input) {
|
||||
input = match event.unwrap() {
|
||||
sieve::Event::SendMessage { .. } => {
|
||||
sent = true;
|
||||
sieve::Input::True
|
||||
}
|
||||
sieve::Event::DuplicateId { .. } => sieve::Input::False,
|
||||
_ => sieve::Input::True,
|
||||
};
|
||||
}
|
||||
sent
|
||||
};
|
||||
for script in [
|
||||
"require \"vacation\";\nvacation \"away\";\n",
|
||||
"require \"enotify\";\nnotify \"mailto:[email protected]\";\n",
|
||||
] {
|
||||
assert!(sends(script, ""), "{script}");
|
||||
assert!(sends(script, "Auto-Submitted: no\r\n"), "{script}");
|
||||
assert!(
|
||||
!sends(script, "Auto-Submitted: auto-replied\r\n"),
|
||||
"{script}"
|
||||
);
|
||||
assert!(
|
||||
!sends(script, "auto-submitted: auto-generated\r\n"),
|
||||
"{script}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+8
@@ -42,6 +42,14 @@ Every changed file carries the AGPL 5(a) notice in its header.
|
||||
in `context.rs` cover it; they run from this directory with `cargo test
|
||||
--offline --target-dir ../../target/sieve-rs-tests scan_limit` (the crate's
|
||||
`test_suite` and `parse_rfc` need upstream's `tests/`, which isn't kept).
|
||||
- `src/runtime/actions/action_vacation.rs`, `action_notify.rs`: vacation
|
||||
and notify stay silent for mail with `Auto-Submitted` other than `no`
|
||||
(RFC 3834). Upstream looked for the header as `HeaderName::Other`, but
|
||||
mail-parser 0.11 parses it as `HeaderName::AutoSubmitted`, so the check
|
||||
never ran and both answered other programs' mail, autoresponders included.
|
||||
Upstream 0.7.3 fails its own svtests "No reply to auto-submitted mail" and
|
||||
"Notifying on automated messages" for this reason. The
|
||||
`sieve_auto_submitted_mail_gets_no_reply` test in `crates/common` covers it.
|
||||
- `Cargo.toml`: the example target is dropped, with the top-level
|
||||
`examples/` and `tests/`, which the server never builds. (`src/` keeps its
|
||||
own `tests` modules: the crate declares them.)
|
||||
|
||||
+6
-7
@@ -2,6 +2,8 @@
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*
|
||||
* Modified by Coffey Labs in 2026 for INBUXA.
|
||||
*/
|
||||
|
||||
use mail_builder::headers::{date::Date, message_id::generate_message_id_header};
|
||||
@@ -15,18 +17,15 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use super::action_vacation::MAX_SUBJECT_LEN;
|
||||
use super::action_vacation::{MAX_SUBJECT_LEN, is_auto_submitted};
|
||||
|
||||
impl Notify {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) {
|
||||
// Do not notify on Auto-Submitted messages
|
||||
for header in &ctx.message.parts[0].headers {
|
||||
if matches!(&header.name, HeaderName::Other(name) if name.eq_ignore_ascii_case("Auto-Submitted"))
|
||||
&& header
|
||||
.value
|
||||
.as_text()
|
||||
.is_none_or(|v| !v.eq_ignore_ascii_case("no"))
|
||||
{
|
||||
// inbuxa: mail-parser names Auto-Submitted itself; matching
|
||||
// HeaderName::Other never found it
|
||||
if header.name == HeaderName::AutoSubmitted && is_auto_submitted(&header.value) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
+16
-9
@@ -2,6 +2,8 @@
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*
|
||||
* Modified by Coffey Labs in 2026 for INBUXA.
|
||||
*/
|
||||
|
||||
use std::borrow::Cow;
|
||||
@@ -23,6 +25,14 @@ use crate::{
|
||||
|
||||
pub(crate) const MAX_SUBJECT_LEN: usize = 256;
|
||||
|
||||
// inbuxa: RFC 3834: anything but "no" means a program sent the message, and
|
||||
// a program's mail is not answered
|
||||
pub(crate) fn is_auto_submitted(value: &HeaderValue) -> bool {
|
||||
value
|
||||
.as_text()
|
||||
.is_none_or(|v| !v.eq_ignore_ascii_case("no"))
|
||||
}
|
||||
|
||||
impl TestVacation {
|
||||
pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
|
||||
let mut from = String::new();
|
||||
@@ -101,16 +111,13 @@ impl TestVacation {
|
||||
HeaderName::Received => {
|
||||
received_count += 1;
|
||||
}
|
||||
// inbuxa: mail-parser names Auto-Submitted itself, so it never
|
||||
// arrived as HeaderName::Other and the check never ran
|
||||
HeaderName::AutoSubmitted if is_auto_submitted(&header.value) => {
|
||||
return TestResult::Bool(false);
|
||||
}
|
||||
HeaderName::Other(header_name) => {
|
||||
if header_name.eq_ignore_ascii_case("Auto-Submitted") {
|
||||
if header
|
||||
.value
|
||||
.as_text()
|
||||
.is_none_or(|v| !v.eq_ignore_ascii_case("no"))
|
||||
{
|
||||
return TestResult::Bool(false);
|
||||
}
|
||||
} else if header_name.eq_ignore_ascii_case("X-Auto-Response-Suppress") {
|
||||
if header_name.eq_ignore_ascii_case("X-Auto-Response-Suppress") {
|
||||
if header.value.as_text().is_some_and(|v| {
|
||||
v.to_ascii_lowercase()
|
||||
.split(',')
|
||||
|
||||
Reference in new issue
Block a user