@@ -36,11 +36,16 @@ type imapfixture struct {
3636 raw * imapclient.Client
3737}
3838
39- // newIMAPFixture starts a fresh in-memory server with an INBOX and an Archive
40- // mailbox, appends one message to INBOX, and returns a Client bound to it along
41- // with the UID of the seeded message.
39+ // newIMAPFixture starts a fresh IMAP4rev2 in-memory server with an INBOX and
40+ // Archive mailbox, appends one message to INBOX, and returns a Client bound to
41+ // it along with the UID of the seeded message.
4242func newIMAPFixture (t * testing.T ) (* imapfixture , imap.UID ) {
4343 t .Helper ()
44+ return newIMAPFixtureWithCaps (t , imap.CapSet {imap .CapIMAP4rev2 : {}})
45+ }
46+
47+ func newIMAPFixtureWithCaps (t * testing.T , caps imap.CapSet ) (* imapfixture , imap.UID ) {
48+ t .Helper ()
4449
4550 memServer := imapmemserver .New ()
4651 user := imapmemserver .NewUser (testUser , testPass )
@@ -50,8 +55,7 @@ func newIMAPFixture(t *testing.T) (*imapfixture, imap.UID) {
5055 NewSession : func (* imapserver.Conn ) (imapserver.Session , * imapserver.GreetingData , error ) {
5156 return memServer .NewSession (), nil , nil
5257 },
53- // IMAP4rev2 folds in UIDPLUS/MOVE/ESEARCH, giving us COPYUID data.
54- Caps : imap.CapSet {imap .CapIMAP4rev2 : {}},
58+ Caps : caps ,
5559 InsecureAuth : true ,
5660 })
5761
@@ -223,6 +227,52 @@ func TestMoveMessagesActuallyMoves(t *testing.T) {
223227 }
224228}
225229
230+ func TestMoveMessagesDoesNotExpungeUnrelatedDeletedMessage (t * testing.T ) {
231+ fx , uidA := newIMAPFixture (t )
232+ uidB := appendMessage (t , fx .raw , "INBOX" , "second@example.com" )
233+
234+ if err := fx .client .DeleteMessages ("INBOX" , []string {presentSelector (uidA )}, false ); err != nil {
235+ t .Fatalf ("soft delete unrelated message: %v" , err )
236+ }
237+ if err := fx .client .MoveMessages ("INBOX" , []string {presentSelector (uidB )}, "Archive" ); err != nil {
238+ t .Fatalf ("move other message: %v" , err )
239+ }
240+
241+ assertMessageStillInInbox (t , fx .client , uidA , uidB )
242+ }
243+
244+ func TestMoveMessagesFallbackDoesNotExpungeUnrelatedDeletedMessage (t * testing.T ) {
245+ caps := imap.CapSet {imap .CapIMAP4rev1 : {}, imap .CapUIDPlus : {}}
246+ fx , uidA := newIMAPFixtureWithCaps (t , caps )
247+ uidB := appendMessage (t , fx .raw , "INBOX" , "second@example.com" )
248+
249+ if fx .client .client .Caps ().Has (imap .CapMove ) {
250+ t .Fatal ("fixture unexpectedly advertises MOVE; fallback path not exercised" )
251+ }
252+ if err := fx .client .DeleteMessages ("INBOX" , []string {presentSelector (uidA )}, false ); err != nil {
253+ t .Fatalf ("soft delete unrelated message: %v" , err )
254+ }
255+ if err := fx .client .MoveMessages ("INBOX" , []string {presentSelector (uidB )}, "Archive" ); err != nil {
256+ t .Fatalf ("fallback move other message: %v" , err )
257+ }
258+
259+ assertMessageStillInInbox (t , fx .client , uidA , uidB )
260+ }
261+
262+ func assertMessageStillInInbox (t * testing.T , client * Client , wantUID , movedUID imap.UID ) {
263+ t .Helper ()
264+ inbox , err := client .ListMessages (ListOptions {Mailbox : "INBOX" , Limit : 50 })
265+ if err != nil {
266+ t .Fatalf ("ListMessages INBOX: %v" , err )
267+ }
268+ for _ , message := range inbox {
269+ if imap .UID (message .UID ) == wantUID {
270+ return
271+ }
272+ }
273+ t .Errorf ("moving UID %d expunged unrelated deleted UID %d" , movedUID , wantUID )
274+ }
275+
226276// --- Security -----------------------------------------------------------
227277
228278// TestMissingUIDDoesNotMutateMailbox ensures a failed (no-match) delete leaves
0 commit comments