JMAP Email/copy retries create duplicate Emails instead of returning alreadyExists

Issue Description

Repeating an identical cross-account Email/copy creates a duplicate Email in Stalwart. This is unsafe when retrying after a lost response: the first copy may have succeeded, but retrying creates another.

Expected Behavior

Return alreadyExists with the first copy’s existingId, as permitted by RFC 8621 §4.7.

Cyrus explicitly implements and tests this behavior:

Actual Behavior

Repeating the same Email/copy creates another destination Email with a new ID.

Reproduction Steps

  1. Create source and destination accounts and a destination mailbox.
  2. Copy one Email between the accounts using Email/copy.
  3. Repeat the same Email/copy in a new request.
  4. Query the destination account and observe two copied Emails.

Relevant Log Output

No server error is emitted. Both requests return a successful created response with different Email IDs.

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

PostgreSQL

Blob Storage

S3-compatible

Search Engine

PostgreSQL

Directory Backend

OIDC

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

Confirmed by inspecting the current Email/copy implementation: it does not perform any dedup or idempotency check before copying. The copy_message function in crates/email/src/message/copy.rs only returns CopyMessageError::NotFound or CopyMessageError::OverQuota as failure conditions, there is no lookup for an existing copy with the same source message and no alreadyExists SetError returned anywhere in the copy path (crates/jmap/src/email/copy.rs). Each successful Email/copy call unconditionally ingests a new message and returns a new id, so a retried request will indeed create a duplicate rather than returning the id of the previously created copy.

This matches your reproduction: the behavior you’re describing is accurate for the current codebase, and there’s no existing mechanism implementing the RFC 8621 §4.7 alreadyExists semantics for Email/copy. This is a genuine gap versus the spec’s recommended idempotency handling, not something covered/handled in current docs.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

IMAP related too: I’ve triggered this with mass email deletions… across accounts (sorta MailMate issue ;( )

The Claude’s response:

This is a Stalwart bug worth filing. RFC 6851 MOVE has no ALREADYEXISTS failure mode — if the destination already holds an identical message, the server should either complete the move (dedup silently, expunge source) or at minimum not hard-fail the whole UID set. Failing the batch guarantees exactly this client retry-storm. You’re on 1.0.0 per the ID response; check their tracker — and given your track record with containerlab/FRR reports, this one’s cleanly reproducible: COPY a message to a folder, then MOVE the same source message to that folder. This is a Stalwart bug worth filing. RFC 6851 MOVE has no ALREADYEXISTS failure mode — if the destination already holds an identical message, the server should either complete the move (dedup silently, expunge source) or at minimum not hard-fail the whole UID set. Failing the batch guarantees exactly this client retry-storm. You’re on 1.0.0 per the ID response; check their tracker — and given your track record with containerlab/FRR reports, this one’s cleanly reproducible: COPY a message to a folder, then MOVE the same source message to that folder.

Log from MailMate showing similar
21:37:28 S: H0 OK [CAPABILITY IMAP4rev2 IMAP4rev1 ENABLE SASL-IR LITERAL+ ID UTF8=ACCEPT JMAPACCESS IDLE NAMESPACE CHILDREN MULTIAPPEND BINARY UNSELECT ACL UIDPLUS ESEARCH WITHIN SEARCHRES SORT THREAD=REFERENCES LIST-EXTENDED LIST-STATUS ESORT SORT=DISPLAY SPECIAL-USE CREATE-SPECIAL-USE MOVE CONDSTORE QRESYNC UNAUTHENTICATE STATUS=SIZE OBJECTID+ PREVIEW RIGHTS=texk QUOTA QUOTA=RES-STORAGE] Authentication successful
21:37:28 C: H1 CAPABILITY
21:37:28 S: * CAPABILITY IMAP4rev2 IMAP4rev1 ENABLE SASL-IR LITERAL+ ID UTF8=ACCEPT JMAPACCESS IDLE NAMESPACE CHILDREN MULTIAPPEND BINARY UNSELECT ACL UIDPLUS ESEARCH WITHIN SEARCHRES SORT THREAD=REFERENCES LIST-EXTENDED LIST-STATUS ESORT SORT=DISPLAY SPECIAL-USE CREATE-SPECIAL-USE MOVE CONDSTORE QRESYNC UNAUTHENTICATE STATUS=SIZE OBJECTID+ PREVIEW RIGHTS=texk QUOTA QUOTA=RES-STORAGE
21:37:28 S: H1 OK CAPABILITY completed
21:37:28 C: H2 ID ("name" "MailMate" "version" "r7024" "vendor" "Freron Software" "contact" "[email protected]")
21:37:28 S: * ID ("name" "Stalwart" "version" "1.0.0" "vendor" "Stalwart Labs LLC" "support-url" "https://stalw.art")
21:37:28 S: H2 OK ID completed
21:37:28 Selecting mailbox: Shared Folders/[email protected]/Deleted Items
21:37:28 C: H3 LIST "Shared Folders" ""
21:37:28 S: * LIST (\NoSelect) "/" "Shared Folders"
21:37:28 S: H3 OK LIST completed
21:37:28 C: H4 SELECT "Shared Folders/[email protected]/Deleted Items" (CONDSTORE)
21:37:28 S: * 2276 EXISTS
21:37:28 S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft)
21:37:28 S: * 0 RECENT
21:37:28 S: * OK [PERMANENTFLAGS (\Deleted \Seen \Answered \Flagged \Draft \*)] All allowed
21:37:28 S: * OK [UIDVALIDITY 542570925] UIDs valid
21:37:28 S: * OK [UIDNEXT 64587] Next predicted UID
21:37:28 S: * OK [HIGHESTMODSEQ 85193] Highest Modseq
21:37:28 S: H4 OK [READ-WRITE] SELECT completed
21:37:28 C: H5 NOOP
21:37:28 S: H5 OK NOOP completed
21:37:28 Performing full synchronization.
21:37:28 C: H6 UID FETCH 1:* (UID)
21:37:28 S: * 1 FETCH (UID 53621)
21:37:28 S: * 2 FETCH (UID 53622)
21:37:28 S: * 3 FETCH (UID 53623)
21:37:28 S: * 4 FETCH (UID 53624)
21:37:28 S: * 5 FETCH (UID 53625)
21:37:28 S: * 6 FETCH (UID 53626)
21:37:28 S: * 7 FETCH (UID 53627)
21:37:28 S: * 8 FETCH (UID 53628)
21:37:28 S: * 9 FETCH (UID 53629)
21:37:28 S: * 10 FETCH (UID 53630)
21:37:28 S: * 2268 FETCH (UID 64578)
21:37:28 S: * 2269 FETCH (UID 64579)
21:37:28 S: * 2270 FETCH (UID 64580)
21:37:28 S: * 2271 FETCH (UID 64581)
21:37:28 S: * 2272 FETCH (UID 64582)
21:37:28 S: * 2273 FETCH (UID 64583)
21:37:28 S: * 2274 FETCH (UID 64584)
21:37:28 S: * 2275 FETCH (UID 64585)
21:37:28 S: * 2276 FETCH (UID 64586)
21:37:28 S: H6 OK UID FETCH completed
21:37:28 C: H7 UID FETCH 64587:* (INTERNALDATE)
21:37:28 S: * 2276 FETCH (UID 64586 INTERNALDATE "11-Jul-2026 12:11:57 +0000")
21:37:28 S: H7 OK UID FETCH completed
21:37:28 C: H8 UID FETCH 1:64586 (UID FLAGS) (CHANGEDSINCE 85193)
21:37:28 S: * OK [HIGHESTMODSEQ 85193] Highest Modseq
21:37:28 S: H8 OK UID FETCH completed
21:37:28 C: H9 NOOP
21:37:28 S: H9 OK NOOP completed
21:37:28 C: H10 UID MOVE 55857:55860,55863,55867,56079:56083 "Deleted Items"
21:37:28 S: H10 NO [ALREADYEXISTS] Message already exists in destination mailbox
21:37:28 Error: Mailbox unexpectedly already exists. Command: 'H10 UID MOVE 55857:55860,55863,55867,56079:56083 "Deleted Items"'.
21:37:28 Error code: 1006
21:37:28 Failed action (1007). Reset observed read/write timeouts: 8/8

@hvjunk Fixed in v0.16.17 (yet to be released).