Issue Description
For an EmailSubmission created with a future HOLDUNTIL (FUTURERELEASE, RFC 4865), EmailSubmission/get correctly reports undoStatus: "pending", but EmailSubmission/query with filter: {"undoStatus": "pending"} returns an empty result — and filter: {"undoStatus": "final"} wrongly includes the held submission. RFC 8621 §7.5 defines undoStatus as a standard filter condition, so query and get should agree.
Related (likely same root cause): the EmailSubmission/set create response echoes undoStatus: "final" for a held submission, while a subsequent get reports "pending".
Impact: clients cannot use the standard query filter to list scheduled (held) emails; the only workaround is an unfiltered EmailSubmission/query followed by EmailSubmission/get and client-side filtering on undoStatus.
Expected Behavior
EmailSubmission/query with filter: {"undoStatus": "pending"} returns held/still-queued submissions, consistent with what EmailSubmission/get reports for the same objects — either by storing Pending while the queue message has a future release time (flipping to Final on delivery), or by having query consult queue state the way get does.
Actual Behavior
EmailSubmission/geton the held submission →undoStatus: "pending",sendAtreflects the hold.
EmailSubmission/querywithfilter: {"undoStatus": "pending"}→ids: [].
EmailSubmission/querywithfilter: {"undoStatus": "final"}→ includes the held submission’s id.
Reproduction Steps
- Create a held submission:
["EmailSubmission/set", {
"accountId": "a",
"create": {
"s1": {
"identityId": "...",
"emailId": "...",
"envelope": {
"mailFrom": {"email": "[email protected]", "parameters": {"HOLDUNTIL": "<future epoch>"}},
"rcptTo": [{"email": "[email protected]"}]
}
}
}
}, "0"]
- Call
EmailSubmission/geton the created id →undoStatus: "pending". - Call
EmailSubmission/querywithfilter: {"undoStatus": "pending"}→ emptyids. - Call
EmailSubmission/querywithfilter: {"undoStatus": "final"}→ the held submission’s id appears.
Relevant Log Output
// held submission "br" (sendAt in the future):
EmailSubmission/get ["br"] →
{"id": "br", "emailId": "w2aaaafy", "undoStatus": "pending", "sendAt": "2026-08-10T02:30:00Z"}
EmailSubmission/query filter {"undoStatus": "pending"} →
{"queryState": "syubq", "canCalculateChanges": true, "position": 0, "ids": []}
EmailSubmission/query filter {"undoStatus": "final"} →
{"queryState": "syubq", ..., "ids": [..., "br"]} // wrongly includes the pending submission
Stalwart Version
v0.16.x
Installation Method
Built from source
Database Backend
RocksDB
Blob Storage
RocksDB
Search Engine
Internal
Directory Backend
Internal
Additional Context
Root cause from source (v0.16.14):
crates/jmap/src/submission/set.rs:754— after the message is accepted into the queue, the storedundo_statusis set toUndoStatus::Finalunconditionally, regardless of a futureHOLDUNTIL.crates/jmap/src/submission/get.rs:140-174— get computes the status dynamically: ifqueue_idstill resolves to a live queued message, it reportspending, overriding the stored value. This is why get is correct.crates/jmap/src/submission/query.rs:141-155— query filters on the stored index byte written at create time (crates/email/src/submission/index.rs,undo_status.as_index()='f'), never consulting queue state. Held submissions are therefore indexed asfinal.
Also reproduced on v0.16.16. The behavior is storage-backend-independent. Client: a JMAP integration implementing scheduled send on top of FUTURERELEASE.
I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.
on
I have searched this support forum (open and closed topics) and confirm this is not a duplicate.
on
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