Found while investigating #28. This is a data-loss path, not a style issue.
The interaction
DeleteMessages with permanent=false marks messages \Deleted and deliberately does not expunge (internal/imap/client.go:548):
// Expunge if permanent
if permanent {
if err := c.client.Expunge().Close(); err != nil {
MoveMessages then issues an unqualified EXPUNGE (internal/imap/client.go:651):
if err := c.client.Expunge().Close(); err != nil {
Per RFC 3501, EXPUNGE permanently removes every message in the selected mailbox carrying \Deleted — not only the ones this move just marked.
Consequence
pm-cli mail delete 123 # soft: marks \Deleted, no expunge
pm-cli mail move 456 -d Archive # expunges 123 as a side effect, permanently
Message 123 is destroyed by a command that never named it. The same applies to any message left \Deleted by another client sharing the mailbox.
mail archive and mail batch's move/archive operations all route through MoveMessages, so they carry the same behavior.
Fix
Adopting native MOVE (#28) removes the explicit EXPUNGE entirely and resolves this. If the COPY fallback is kept for any reason, it must use UIDExpunge(uidSet) when UIDPLUS is available — which is exactly what imapclient's own fallback does, and what our hand-rolled version omits.
Bridge advertises UIDPLUS, so UIDEXPUNGE is available in practice.
Verification status
Identified by code inspection and RFC semantics; not reproduced against a live server. The Bridge account used for 0.2.6 validation was removed afterward, so I could not confirm how Bridge maps \Deleted — it may translate soft deletes to a Trash move rather than leaving the flag set, which would narrow real-world exposure. That should be checked before deciding severity, but the unqualified EXPUNGE is wrong regardless of how Bridge behaves, and pm-cli should not depend on a server-specific mapping to be safe.
Reproducible against imapmemserver without a live account: mark one message \Deleted, move a different one, assert the first still exists.
Found while investigating #28. This is a data-loss path, not a style issue.
The interaction
DeleteMessageswithpermanent=falsemarks messages\Deletedand deliberately does not expunge (internal/imap/client.go:548):MoveMessagesthen issues an unqualified EXPUNGE (internal/imap/client.go:651):Per RFC 3501,
EXPUNGEpermanently removes every message in the selected mailbox carrying\Deleted— not only the ones this move just marked.Consequence
Message 123 is destroyed by a command that never named it. The same applies to any message left
\Deletedby another client sharing the mailbox.mail archiveandmail batch'smove/archiveoperations all route throughMoveMessages, so they carry the same behavior.Fix
Adopting native MOVE (#28) removes the explicit EXPUNGE entirely and resolves this. If the COPY fallback is kept for any reason, it must use
UIDExpunge(uidSet)when UIDPLUS is available — which is exactly whatimapclient's own fallback does, and what our hand-rolled version omits.Bridge advertises UIDPLUS, so
UIDEXPUNGEis available in practice.Verification status
Identified by code inspection and RFC semantics; not reproduced against a live server. The Bridge account used for 0.2.6 validation was removed afterward, so I could not confirm how Bridge maps
\Deleted— it may translate soft deletes to a Trash move rather than leaving the flag set, which would narrow real-world exposure. That should be checked before deciding severity, but the unqualified EXPUNGE is wrong regardless of how Bridge behaves, and pm-cli should not depend on a server-specific mapping to be safe.Reproducible against
imapmemserverwithout a live account: mark one message\Deleted, move a different one, assert the first still exists.