Sieve: match any key, and :regex dates as regular expressions #236

Merged
jcoffey-dev merged 1 commits from fix/sieve-match-bugs into main 2026-10-29 18:54:03 +00:00
4 changed files with 78 additions and 9 deletions

No files matched your search

@@ -395,4 +395,52 @@ if header :regex "x-h" "${p}" { discard; }
);
}
}
// inbuxa: a test is true when any key matches (RFC 5229, 5260), whatever
// the keys' order, and :regex on a date part is a regular expression,
// not a wildcard pattern
#[test]
fn sieve_tests_match_any_key_and_regex_dates() {
let runtime = sieve::Runtime::new();
let matched = |test: &str| {
let script = format!(
"require [\"fileinto\", \"variables\", \"date\", \"regex\"];\nif {test} {{ fileinto \"matched\"; }}\n"
);
let script = std::sync::Arc::new(
sieve::Compiler::new()
.compile(script.as_bytes())
.unwrap_or_else(|err| panic!("{test}: {err}")),
);
let message = "Date: Tue, 27 Oct 2026 09:30:00 +0000\r\nSubject: hi\r\n\r\nbody\r\n";
let mut ctx = runtime.filter(message.as_bytes());
let mut input = sieve::Input::script("test", script);
let mut filed = false;
while let Some(event) = ctx.run(input) {
if let sieve::Event::FileInto { folder, .. } = event.unwrap() {
filed |= folder == "matched";
}
input = sieve::Input::True;
}
filed
};
for test in [
r#"string :is "a" ["a", "b"]"#,
r#"string :is "a" ["b", "a"]"#,
r#"string :is ["x", "a"] ["a", "b"]"#,
r#"date :regex "date" "year" "^20[0-9]{2}$""#,
r#"date :regex "date" "month" ["^1[12]$", "^10$"]"#,
r#"date :regex "date" "month" "1.""#,
r#"currentdate :regex "year" "^[0-9]{4}$""#,
] {
assert!(matched(test), "{test} should match");
}
for test in [
r#"string :is "a" ["b", "c"]"#,
r#"date :regex "date" "year" "^19[0-9]{2}$""#,
r#"date :regex "date" "month" "^0[1-9]$""#,
r#"currentdate :regex "year" "^[0-9]{2}$""#,
] {
assert!(!matched(test), "{test} should not match");
}
}
}
+10
View File
@@ -50,6 +50,16 @@ Every changed file carries the AGPL 5(a) notice in its header.
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.
- `src/runtime/tests/test_string.rs`: `string` is true when any key matches
any source. Upstream's `break` on a match only left the loop over sources,
so the next key overwrote the result: `string :is "a" ["a", "b"]` was
false while `["b", "a"]` was true.
- `src/runtime/tests/test_date.rs`: `date :regex` and `currentdate :regex`
compare with the regex comparator (through the same size limit and
cache); upstream compared them as `:matches` wildcard patterns, so
`"^20[0-9]{2}$"` never matched a year. The
`sieve_tests_match_any_key_and_regex_dates` test in `crates/common`
covers both.
- `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.)
+15 -7
View File
@@ -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;
@@ -94,7 +96,7 @@ impl TestDate {
if let Some(dt) = ctx.find_dates(header) {
let date_part =
self.date_part.eval(self.zone.eval(dt.as_ref()).as_ref());
for key in &key_list {
for (key, pattern) in key_list.iter().zip(self.key_list.iter()) {
if match &self.match_type {
MatchType::Is => self.comparator.is(&date_part.as_str(), key),
MatchType::Contains => self
@@ -113,9 +115,12 @@ impl TestDate {
&mut captured_values,
)
}
MatchType::Regex(capture_positions) => self.comparator.matches(
// inbuxa: :regex compared as a wildcard pattern
MatchType::Regex(capture_positions) => self.comparator.regex(
&ctx.regex_cache,
pattern,
key,
&date_part,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
@@ -182,8 +187,8 @@ impl TestCurrentDate {
}),
);
for key in &self.key_list {
let key = ctx.eval_value(key);
for pattern in &self.key_list {
let key = ctx.eval_value(pattern);
if match &self.match_type {
MatchType::Is => self.comparator.is(&date_part.as_str(), &key),
@@ -200,9 +205,12 @@ impl TestCurrentDate {
*capture_positions,
&mut captured_values,
),
MatchType::Regex(capture_positions) => self.comparator.matches(
// inbuxa: :regex compared as a wildcard pattern
MatchType::Regex(capture_positions) => self.comparator.regex(
&ctx.regex_cache,
pattern,
&key,
&date_part,
key.to_string().as_ref(),
*capture_positions,
&mut captured_values,
),
+5 -2
View File
@@ -61,7 +61,10 @@ impl TestString {
let mut captured_values = Vec::new();
let sources = ctx.eval_values(&self.source);
for pattern in &self.key_list {
// inbuxa: any key matching any source makes the test true
// (RFC 5229); a plain break only left the inner loop, and the
// next key overwrote the result
'keys: for pattern in &self.key_list {
let key = ctx.eval_value(pattern);
for source in &sources {
if !empty_is_null || !source.is_empty() {
@@ -95,7 +98,7 @@ impl TestString {
};
if result {
break;
break 'keys;
}
}
}