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
editheader: a script can rewrite the message it is filing
addheader and deleteheader (RFC 5293), advertised over ManageSieve.
addheader inserts at the top of the header by default and at the
bottom under :last; deleteheader removes every instance of a name, the
nth under :index/:last, or only the instances whose value matches
under :is/:contains/:matches with the comparator honoured.
The evaluator only DECIDES. The edits travel in script order in the
structured result - order matters, because "deleteheader X; addheader
X" is a replace and the other way round is not - and
LocalDelivery::ApplySieveHeaderEdits_ rewrites the stored file BEFORE
redirects are queued, so a forwarded copy carries the edits (RFC 5293
2: an edit affects all subsequent actions), then refreshes the
recorded message size so the database row agrees with the file.
Failure handling follows the rules engine's header rewriting: a failed
load or save is reported through RuleGuard::ReportActionFailed, and a
half-parsed file is never written back - MimeLoadResult exists
precisely to tell "could not open" from "parsed half way", and writing
edits onto either would be worse than the edits not happening.
Received and Return-Path are protected AT UPLOAD, with the refusal
naming the header: the trace of how a message travelled is not a
script's to edit (RFC 5293 7). Field names are validated as legal
header names, so "Bad Name" fails with a line number rather than
producing a header no parser will read back.
The tests assert on the DELIVERED BYTES over POP3 - the file the
user's client downloads - including placement above and below existing
fields, with the negative control that a non-matching value pattern
deletes nothing. One honest limit lives in a comment where it bites:
value patterns match an RFC 2047-encoded value in its encoded form,
which can only make a delete NOT happen - the safe direction.
Suite: 1546/1546.
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
-
57 shipped · 0 underway · 7 not started · 0 deferred
451
+
58 shipped · 0 underway · 6 not started · 0 deferred
452
452
453
453
|| Capability | Detail |
454
454
|:-:|---|---|
@@ -498,7 +498,7 @@ the source, not from documentation.
498
498
| ✅ | copy (RFC 3894) | Not implemented. There is no :copy tag on fileinto or redirect, so a redirect always cancels the implicit keep unless an explicit keep is also written **Shipped in dc9301a** - the row above described the state before it. |
499
499
| ✅ | date and index (RFC 5260) | **Both shipped 16 August 2026**, advertised as "date" and "index" over ManageSieve. `date` and `currentdate` extract all thirteen RFC 5260 4.1 date-parts (year through weekday, including julian as Modified Julian Day and the composed iso8601/std11 forms), with real zone arithmetic: the header's wall clock and its zone are parsed SEPARATELY - `Time::GetDateTimeFromMimeHeader` folds them together and cannot express `:originalzone` - so `:zone` re-expresses the same instant and `:originalzone` keeps the header's own clock. The decisive test files on an hour the raw header never states (15:30 +0200 matched as hour 13 under :zone "+0000"), with the negative control that the unconverted hour must NOT also match. One trap found on the way, now recorded in the code: `Time::GetTimeAdjustForTimezone` answers "what do I add to reach UTC" - the NEGATED zone - and the first build shipped "+0200" as "-0200" until the end-to-end test caught it. An unknown date-part and a malformed :zone are refused at upload. `:index`/`:last` select among repeated header fields on the header, address and date tests (not envelope, which has nothing to index into); a missing nth instance matches nothing, not even "". |
500
500
| ✅ | duplicate (RFC 7352) | **Shipped 16 August 2026.** The test in all three identifier spellings - Message-ID by default, `:header` for a named header's value, `:uniqueid` for a literal, each SOURCE-TAGGED before hashing so equal text under different spellings cannot collide (RFC 7352 3) - with `:handle` for independent windows, `:seconds` (default seven days) and `:last` measuring the window from the last occurrence. The seen-store (`SieveDuplicateTracker`) is a deliberate sibling of the vacation tracker - same file format, same SHA-256 key hashing against attacker-supplied Message-IDs forging records, same atomic sibling-write-and-rename - but it FAILS OPEN where vacation fails closed, because the common script discards on "duplicate": a wrong "duplicate" destroys a legitimate message, a wrong "new" delivers a copy twice. Unreadable store, full store (10,000 live records - no eviction, since a sender who controls their own Message-IDs could roll everyone else's records out), failed write: all answer "not a duplicate". Per-account file in the Sieve directory, removed with the account, surviving restarts. A message with no Message-ID is never a duplicate and never tracked. |
501
-
| ⬜ | editheader (RFC 5293) | Not implemented. addheader/deleteheader are not even in the known-command list, so a script using them fails CHECKSCRIPT with "unknown command" |
501
+
| ✅ | editheader (RFC 5293) | **Shipped 16 August 2026.** `addheader` (top of the header by default, bottom under `:last`) and `deleteheader` (every instance, the nth under `:index`/`:last`, or only value-matching instances under `:is`/`:contains`/`:matches` with the comparator honoured), advertised over ManageSieve. The evaluator only DECIDES - the edits travel in script order in the structured result, and `LocalDelivery::ApplySieveHeaderEdits_` rewrites the stored file BEFORE redirects are queued, so a forwarded copy carries them (RFC 5293 2: an edit affects all subsequent actions), then refreshes the recorded message size. A failed load or write is reported through `RuleGuard::ReportActionFailed`, the same visibility the rules engine's header rewriting gets, and a half-parsed file is never written back. Received and Return-Path are protected AT UPLOAD: the trace of how a message travelled is not a script's to edit (RFC 5293 7), and the refusal names the header. Field names are validated as legal header names. End-to-end tests assert on the DELIVERED BYTES over POP3 - placement above/below existing fields included - with the negative control that a non-matching value pattern deletes nothing. One honest limit, in a comment where it bites: value patterns match an RFC 2047-encoded value in its encoded form, which can only make a delete not happen. |
502
502
| ⬜ | enotify (RFC 5435) | Not implemented, and refused rather than ignored: `notify` is not in the known-command allowlist, so a script using it fails at upload with "unknown command". There are no notification methods, no valid_notify_method test and no NOTIFY capability advertised over ManageSieve. |
503
503
| ✅ | envelope (RFC 5228 §5.4) and body (RFC 5173) | **Both shipped.** `envelope` landed in dc9301a (the SMTP envelope reaches the evaluator, so `envelope "to"` works without the Delivered-To header). `body` landed 15 August 2026 with all three RFC 5173 transforms: `:raw` (the undecoded body, MIME structure ignored), `:text` (the default - the decoded text of every text/* part) and `:content` (the decoded parts whose MIME type matches, where "image" matches every subtype and "" matches every part). Transfer encoding and charset are decoded before matching, which is the point: a filter written against plain text keeps working when the sender's client base64-encodes the part. The MIME walk is depth-capped at 20 because it runs on the delivery thread. Advertised in the ManageSieve capability line under the standing rule - only once the delivery-side step exists - with seven end-to-end tests in `SieveBodyDelivery.cs` asserting **where a delivered message was filed** rather than what the evaluator reported, two of them negative controls (a non-matching body must stay in INBOX; a message with no body must match nothing, not even the empty key). |
504
504
| ✅ | ihave (RFC 5463) and environment (RFC 5183) | **Both shipped 16 August 2026.** `ihave` answers from the SAME list `require` validates against (`SieveParser::IsSupportedExtension`), so the two can never disagree - and its dangerous half is handled: an ihave-guarded block may USE the extensions it tested for without a require line (`CollectIhaveGrants_`, scoped to the block, flowing through `allof` but deliberately not `anyof` or `not`, whose truth proves nothing about the block running). Without that grant the test is a trap - reporting an extension available that the script cannot then use. Testing for an extension this server lacks is legal at upload and evaluates false, which is the command's whole purpose. `environment` answers name/version/location(MDA)/phase(during)/host/domain truthfully and reports remote-host/remote-ip as honestly UNKNOWN - the sending client's identity does not reach the evaluator - and an unknown item matches nothing, not even the empty key. End-to-end in `SieveEnvironmentDelivery.cs`, including the grant proven by a script whose guarded block uses the body test with no require for it. |
0 commit comments