You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sieve gets :regex, wearing the same circuit breaker as the rules engine
The :regex match type (draft-ietf-sieve-regex) on header, address,
envelope, body and hasflag, advertised as "regex" over ManageSieve.
The draft never became an RFC, but Dovecot implements it and clients
offer it, which is what a capability name is for.
Semantics: a SEARCH - the pattern may hit anywhere in the value, and a
script anchors with ^ and $ when it means the whole - where the rules
engine's regex criterion is a whole-value match. Case folds under the
default comparator (i;ascii-casemap) and does not under i;octet.
i;ascii-numeric with :regex is refused, as for :contains and :matches.
Two protections, because a script author is exactly as able to write a
catastrophic pattern as a rule author, and this is the delivery
thread:
- Every key is compiled at upload. A pattern that cannot compile is
refused with its line number, instead of being stored and silently
never matching - the failure mode the legacy rules engine had for
years (HM6042).
- Evaluation runs under RuleGuard's budget-and-suspend breaker: 250 ms
per evaluation, five-minute suspension keyed on the pattern text,
reported once per window. SieveRegexMatches is a deliberately
parallel sibling of RegexCriteriaMatches rather than a shared core -
same suspension table, same budget, same error codes, each keeping
its exact log wording, and the rules path character-for-character
unchanged. The header says: if you change one, look at the other.
The breaker test delivers a message whose crafted subject detonates
(a+)+<unique>$ and asserts the thing that matters: the message ARRIVES,
in INBOX, with the suspension reported - a hostile sender must never be
able to turn a filter into mail loss. The pattern carries a per-run
unique tail because suspension outlives a test run and reports only on
insertion; a reused pattern would be answered from the table, report
nothing, and fail the assertion for the wrong reason.
Suite: 1513/1513.
Three things stand out and are worth naming rather than leaving to be inferred.
78
78
**Storage and the administration surface are the best-covered areas**, and the
@@ -448,7 +448,7 @@ the source, not from documentation.
448
448
449
449
### Sieve, ManageSieve and rules
450
450
451
-
50 shipped · 0 underway · 14 not started · 0 deferred
451
+
51 shipped · 0 underway · 13 not started · 0 deferred
452
452
453
453
|| Capability | Detail |
454
454
|:-:|---|---|
@@ -506,7 +506,7 @@ the source, not from documentation.
506
506
| ⬜ | include (RFC 6609) | Not implemented, and refused rather than ignored: `include`, `return` and `global` are not in the known-command allowlist, so a script using them fails at upload with "unknown command". There is no personal/global script namespace in SieveStorage to include from. |
507
507
| ✅ | mailbox / mboxmetadata (RFC 5490) | **The mailbox extension shipped 16 August 2026**: the `mailboxexists` test and `fileinto :create`. mailboxexists is the first test in this engine answered from outside the message - the delivery path hands the evaluator a callback over the recipient's real folder list, using the same lookup fileinto itself uses (`MessageUtilities::FolderExistsForDelivery`, kept beside `MoveToIMAPFolder` so the two cannot drift), and for a public folder the same insert-permission gate, because RFC 5490 3.1 defines "exists" as exists-and-deliverable. A caller with no store to ask (the COM test evaluator) gets false for every name - the safe answer. `:create` was already this server's behaviour - MoveToIMAPFolder has always created missing account folders - so the tag is accepted under its require and the semantics were proven rather than added: an end-to-end test SELECTs the folder the delivery created, over IMAP, from a real client session. Advertised over ManageSieve under the standing rule. Still absent from RFC 5490: the mboxmetadata half (METADATA is not implemented in the IMAP server at all). |
508
508
| ⬜ | Out-of-office scheduling and scope | Only an end date exists — there is no start date, so a future absence cannot be scheduled and must be switched on manually. There is also no domain-level or server-level auto-reply, no separate internal/external message… |
509
-
| ⬜ | regex (draft-ietf-sieve-regex) | Not implemented as a Sieve match type, even though the server already carries a regex engine used by the legacy rules engine (RuleCriteria::MatchesRegEx). Wiring it into MatchValue_ would be small |
509
+
| ✅ | regex (draft-ietf-sieve-regex) | **Shipped 16 August 2026** as the `:regex` match type on header/address/envelope/body/hasflag, advertised as "regex" over ManageSieve. Never became an RFC, but Dovecot implements it and clients offer it. A search (anchor with `^`/`$` to mean the whole value), case-folded under the default comparator and case-sensitive under `i;octet`; `i;ascii-numeric` with `:regex` is refused. Two protections, both tested end to end: every key is COMPILED AT UPLOAD, so a pattern that cannot compile is refused with its line number instead of silently never matching (the failure mode the legacy rules engine had for years); and evaluation runs under the same RuleGuard budget-and-suspend breaker as the legacy regex criterion - a deliberately parallel sibling sharing the suspension table and error codes - so a catastrophic pattern costs one bounded evaluation per five-minute window, never the message. The breaker test delivers a crafted subject against `(a+)+…$` and asserts the message ARRIVES in INBOX with the suspension reported. |
510
510
| ⬜ | reject / ereject (RFC 5429) | Not implemented. **The hazard this row used to describe is gone and the correction matters more than the gap:** neither is in the known-command allowlist and `require "reject"` is refused, so a script that tries to reject mail is rejected AT UPLOAD with a message naming the unsupported extension. The author is told; mail is not silently kept while they believe it is being refused. `SieveSyntax.cs` asserts exactly that script is refused. Implementing them means an SMTP-time refusal for `ereject` and an RFC 3834 bounce for `reject`. |
511
511
| ✅ | relational (RFC 5231) | Not implemented. No :count or :value match types, and no i;ascii-numeric comparator to make them meaningful — SplitArguments recognises only is/contains/matches **Shipped in dc9301a.**|
512
512
| ⬜ | RENAMESCRIPT and UNAUTHENTICATE | Neither implemented; both fall through to NO "Unknown command." A client renaming a script must GETSCRIPT/PUTSCRIPT/DELETESCRIPT by hand |
if (elapsed >= static_cast<ULONGLONG>(EvaluationBudgetMilliseconds) &&
358
+
SuspendPatternAndClaimReport_(pattern))
359
+
{
360
+
String errorMessage = Formatter::Format("A Sieve ':regex' match took {0} ms against a {1} character value, over the {2} ms budget, and will not be evaluated again for {3} seconds. Any sender can make this cost recur once per message, so it is being paid once per pattern instead. Pattern begins: {4}",
// Upload validation compiles every ':regex' key, so reaching this means a
373
+
// script stored before that check existed, or edited on disk. Saying so
374
+
// matters for the same reason it does for a rule: a test that can never
375
+
// match looks exactly like a test that happens not to.
376
+
if (SuspendPatternAndClaimReport_(pattern))
377
+
{
378
+
String errorMessage = Formatter::Format("A Sieve ':regex' key is not a valid regular expression and can never match. The script containing it is active and that test is doing nothing. Pattern: {0}",
String errorMessage = Formatter::Format("A Sieve ':regex' match was abandoned as too complex against a {0} character value, and will not be evaluated again for {1} seconds. Treated as no match. Pattern begins: {2}",
0 commit comments