Sieve: match any key, and :regex dates as regular expressions #236
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+10
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user