fix(imap): error instead of silent success when STORE/COPY/MOVE target UIDs aren't present - #15
Conversation
… nothing DeleteMessages, CopyMessages, and MoveMessages reported success even when the target UIDs were not present in the selected mailbox. A STORE against absent UIDs is a valid no-op per RFC 3501 (no error), and many servers (Proton Bridge included) accept a COPY of a non-matching UID set without error. The code discarded both responses, so e.g. removing a label the message didn't have printed "Label removed from 1 message(s)" while changing nothing. - DeleteMessages: drain the FETCH responses the server streams from STORE and error when zero messages were modified. - CopyMessages: inspect the COPYUID data and error when both SourceUIDs and DestUIDs are empty. - MoveMessages: apply both checks to its inline COPY + STORE. No extra IMAP round-trips: the affected count and COPYUID data are already in the response streams. SetFlagsMultiple is intentionally left unchanged — the server returns zero affected items both for absent UIDs and for a flag that is already set, so that case needs a different approach. Closes bscott#11 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Ran this branch through the CI steps locally, since Actions does not appear to have run on the PR. All three checks from One data point on the COPY path, from running pm-cli against Proton Bridge daily in an automated triage pipeline. Bridge does return COPYUID for a copy that matches nothing, so The affected count in We had independently written a partial version of this fix (COPY guard only, no affected count) after hitting the same class in production. This one is more complete, so we are dropping ours and tracking this instead. |
Summary
DeleteMessages,CopyMessages, andMoveMessagesininternal/imap/client.goreported success even when the target UIDs were notpresent in the selected mailbox. Removing a label an email didn't have (a COPY
to the wrong
Labels/*folder, or a STORE on an absent UID) printed e.g.Label removed from 1 message(s)while nothing actually changed.Root cause, as described in the issue:
error; the streamed FETCH responses (one per modified message) were never
counted.
servers (Proton Bridge included); the
CopyData(SourceUIDs/DestUIDs)was discarded.
Fix
DeleteMessages: drainstoreCmd.Next()and error when the affected countis zero.
CopyMessages: error whenCopyData.SourceUIDsandDestUIDsare bothempty.
MoveMessages: apply both checks before expunging the source.No extra IMAP round-trips — the affected count and COPYUID data are already in
the response streams.
SetFlagsMultipleis intentionally left unchanged (perthe issue): a server returns zero affected items both for an absent UID and for
a flag that is already set, so distinguishing them needs a different approach.
Tests
Added
internal/imap/client_affected_test.go, which drives a real in-memoryIMAP server (go-imap's
imapmemserver, no new module dependency) end-to-end,organized into all seven categories:
mailbox.
affected count adds no extra/blocking round-trip.
a false success.
silent success (the memserver rejects an empty COPY at the protocol level;
Proton Bridge returns an empty COPYUID set — both paths are handled).
integration path); explicit skip placeholder retained.
go build ./... && go vet ./... && go test ./...all green.Closes #11
🤖 Generated with Claude Code