Issue Description
Email/copy with onSuccessDestroyOriginal: true is the usual way to move mail into another account. When the copy of a record fails with overQuota or alreadyExists, the original is destroyed anyway. The message is then in neither account. Email/copy reports the failure in notCreated, and the implicit Email/set in the same response lists the original under destroyed. The blob is gone too, and a JMAP destroy does not go through Trash, so there is nothing to restore.
With several records in one call, the originals of the copied and of the failed records are all destroyed. A bulk move into a nearly full shared or group account loses every message that did not fit.
The alreadyExists case loses data when the destination already holds a different message with the same Message-ID and subject, for example a list copy and a direct copy of the same post. The source version is destroyed and exists nowhere afterwards.
Failures caught before the copy keep the original: unknown mailboxId (invalidProperties), no mayAddItems on the destination (forbidden), unknown source id (notFound).
This looks like a side effect of the fix for JMAP Email/copy `onSuccessDestroyOriginal` destroys the wrong id (the copy's create-key, not the source Email id), leaving the original behind in v0.16.15. Before that fix the destroy list held the creation key, so nothing was removed. Since then it holds the source id, but the id is still added for failed copies.
Expected Behavior
A record that ends up in notCreated keeps its original.
Actual Behavior
One message, destination account over its disk quota:
["Email/copy", {"fromAccountId": "l", "accountId": "h",
"create": {"c": {"id": "bl2aaaak3", "mailboxIds": {"a": true}}},
"onSuccessDestroyOriginal": true}, "0"]
<- ["Email/copy", {"fromAccountId": "l", "accountId": "h", "oldState": "n", "newState": "n",
"notCreated": {"c": {"type": "overQuota", "description": "Account quota exceeded."}}}, "0"],
["Email/set", {"accountId": "l", "oldState": "sxycq", "newState": "sx2cq",
"destroyed": ["bl2aaaak3"]}, "0"]
["Email/get", {"accountId": "l", "ids": ["bl2aaaak3"]}, "0"]
<- "list": [], "notFound": ["bl2aaaak3"]
The other cases, each with onSuccessDestroyOriginal: true:
case Email/copy original
one message, destination over disk quota overQuota destroyed, blob 404
two messages, room for the small one only created + overQuota both destroyed
identical message already in destination alreadyExists destroyed
different body, same Message-ID and subject alreadyExists destroyed, text exists nowhere
unknown mailboxId invalidProperties kept
destination mailbox without mayAddItems forbidden kept
Reproduction Steps
- User A can add mail to a mailbox in account B: a group account, or a mailbox shared with mayReadItems and mayAddItems.
- As admin, set a small disk quota on account B (10 is enough).
- As user A, import a message into A’s own Inbox.
- As user A, copy it into B’s mailbox with onSuccessDestroyOriginal: true.
- The copy is notCreated with overQuota, and the original is destroyed.
2. ["x:Account/set", {"accountId": "<admin account>", "update": {"<B>": {"quotas/maxDiskQuota": 10}}}, "0"]
4. ["Email/copy", {"fromAccountId": "<A>", "accountId": "<B>",
"create": {"c": {"id": "<email id>", "mailboxIds": {"<B mailbox>": true}}},
"onSuccessDestroyOriginal": true}, "0"]
Stalwart Version
v0.16.x
Installation Method
Built from source
Database Backend
RocksDB
Blob Storage
RocksDB
Search Engine
Internal
Directory Backend
Internal
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