Your question
Personal JMAP Sieve script never invoked during delivery, even after correctly wiring MtaStageData → SieveSystemScript → include :personal
Version: Stalwart v0.16.19 (Docker)
Summary
Per-account Sieve scripts uploaded and activated via SieveScript/set (JMAP for Sieve, RFC 9661) never execute during real mail delivery — not for external unauthenticated SMTP, not for authenticated local submission. I followed the documented bridge mechanism (MtaStageData.script → a SieveSystemScript containing include :personal) exactly, verified every step via read-back, restarted the server, enabled full TRACE logging, and still see zero Sieve-related log events of any kind during delivery.
What I configured
-
Uploaded a personal script for a test account via
SieveScript/set(blob upload + create +onSuccessActivateScript). Confirmed active viaSieveScript/get:{"id":"e","name":"test-personal","isActive":true}Contents (deliberately simple/unambiguous for testing):
require ["fileinto", "special-use"]; fileinto :specialuse "\\Trash" "Trash"; -
Created a server-level trusted script via
x:SieveSystemScript/set:{"name":"personal-include","isActive":true, "contents":"require [\"include\"];\ninclude :personal :optional \"test-personal\";"} -
Pointed the DATA stage at it via
x:MtaStageData/set:{"update":{"singleton":{"script":{"else":"'personal-include'"}}}}(Note: the
elsevalue must be a Sieve string-literal — single-quoted inside the JSON string — not a bare identifier. An unquoted script name fails with"Invalid variable or constant". This isn’t obvious from the docs and might be worth calling out explicitly on theMtaStageDatareference page.) -
Restarted the server so the config takes effect (confirmed via
x:MtaStageData/getandx:SieveSystemScript/getthat everything persisted correctly both before and after restart).
Test
Sent a real message via unauthenticated external SMTP (MAIL FROM/RCPT TO/DATA on port 25, mimicking a genuine external sender) to the test account. Also tested with an authenticated local submission between two accounts on the same server, to rule out anything specific to the unauthenticated/external path.
Expected: message lands in Trash (the unconditional action in the test script).
Actual: message is delivered normally (message-ingest.ham or message-ingest.spam, depending on Stalwart’s own classifier’s independent decision) into Inbox or Junk — the personal script’s fileinto never applies.
Verified with full tracing
Set the Stdout tracer to level: trace via x:Tracer/set, restarted (again required for the level change to take live effect), sent the test message again. Captured tens of thousands of log lines in the surrounding window — grepped for any event key matching (*sieve*) (Stalwart’s own dotted event-naming convention, e.g. smtp.mail-from, delivery.dsn-success): zero matches. No Sieve-related event of any kind fires — not an error, not a skip notice, nothing.
Things I’ve ruled out
- Config not persisting — confirmed correct via
/getat every step. - Config not reloaded — confirmed via two full container restarts.
include :personal ""(empty string) as a “use whatever’s active” wildcard — per RFC 6609 this looks up a script literally named"", so I switched to referencing the exact script name directly; no change in behavior.- Log level filtering the events out — confirmed at full
tracelevel with no output filters (events: {},eventsPolicy: exclude, i.e. nothing excluded). - Stalwart’s own built-in spam classifier interfering — reproduced with a message that classifier accepts as ham (authenticated local submission) and one it flags as spam (SPF/DMARC-failing external test); identical result either way — the personal script’s action never applies in either case.
Question
Is there an additional requirement beyond MtaStageData.script + include :personal to get a personal/JMAP-managed Sieve script to actually run at delivery time? Or is this a genuine regression/gap in 0.16.19’s DATA-stage → personal-script bridge?
Happy to provide full JMAP request/response logs or a minimal Docker Compose repro if useful.
I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.
on