(geronimo-mail) 02/05: GERONIMO-6902 - Folder.expunge() always returns an empty array - EXPUNGE responses extracted under the wrong keyword

[email protected] Sat, 18 Jul 2026 19:12:28 +0000
Newsgroups gmane.comp.java.geronimo.cvs
Message-ID <[email protected]>
This is an automated email from the ASF dual-hosted git repository.

rzo1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/geronimo-mail.git

commit b78b1e61c21e9d6dbc22aafdefe6211e1fe1015c
Author: Richard Zowalla <[email protected]>
AuthorDate: Sat Jul 18 21:10:28 2026 +0200

    GERONIMO-6902 - Folder.expunge() always returns an empty array - EXPUNGE responses extracted under the wrong keyword
---
 .../mail/store/imap/connection/IMAPConnection.java |   8 +-
 .../mail/store/imap/IMAPExpungeRegressionTest.java | 107 +++++++++++++++++++++
 geronimo-mail_2.1_tck/src/tck/geronimo.jtx         |   1 -
 3 files changed, 113 insertions(+), 3 deletions(-)

diff --git a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/connection/IMAPConnection.java b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/connection/IMAPConnection.java
index 9351115..1d1192f 100644
--- a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/connection/IMAPConnection.java
+++ b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/connection/IMAPConnection.java
@@ -1359,8 +1359,12 @@ public class IMAPConnection extends MailConnection {
     public synchronized List expungeMailbox() throws MessagingException {
         // send the message, and make sure we got an OK response
         sendCommand("EXPUNGE");
-        // extract all of the expunged responses and return.
-        return extractResponses("EXPUNGED");
+        // extract all of the expunged responses and return.  The parser queues
+        // the untagged "* nn EXPUNGE" responses under the "EXPUNGE" keyword, so
+        // that is the keyword we must extract here.  Extracting them prevents the
+        // pending-response handler from processing the same expunge events a
+        // second time.
+        return extractResponses("EXPUNGE");
     }
 
     public int[] searchMailbox(SearchTerm term) throws MessagingException {
diff --git a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/IMAPExpungeRegressionTest.java b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/IMAPExpungeRegressionTest.java
new file mode 100644
index 0000000..84024ba
--- /dev/null
+++ b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/IMAPExpungeRegressionTest.java
@@ -0,0 +1,107 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.geronimo.mail.store.imap;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.Properties;
+
+import jakarta.mail.Flags;
+import jakarta.mail.Folder;
+import jakarta.mail.Message;
+import jakarta.mail.Session;
+import jakarta.mail.Store;
+
+import org.apache.geronimo.mail.testserver.AbstractProtocolTest;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Regression test for the expunge-keyword-typo defect surfaced by the
+ * Jakarta Mail TCK (exception#msgRemoveExp_Test):
+ * IMAPConnection.expungeMailbox() extracted the untagged expunge responses
+ * with the keyword "EXPUNGED", but the parser queues them under "EXPUNGE".
+ * As a result Folder.expunge() always returned an empty array and the
+ * unextracted responses were double-processed by the pending-response
+ * handler.
+ */
+public class IMAPExpungeRegressionTest extends AbstractProtocolTest {
+
+    protected Store connect() throws Exception {
+        final Properties props = new Properties();
+        props.setProperty("mail.imap.port", String.valueOf(imapConf.getListenerPort()));
+        props.setProperty("mail.debug", "true");
+        final Session session = Session.getInstance(props);
+        final Store store = session.getStore("imap");
+        store.connect("127.0.0.1", "serveruser", "serverpass");
+        return store;
+    }
+
+    protected byte[] readMessageResource(final String name) throws Exception {
+        final ByteArrayOutputStream bout = new ByteArrayOutputStream();
+        try (InputStream in = IMAPExpungeRegressionTest.class.getResourceAsStream(name)) {
+            final byte[] buf = new byte[4096];
+            int n;
+            while ((n = in.read(buf)) != -1) {
+                bout.write(buf, 0, n);
+            }
+        }
+        return bout.toByteArray();
+    }
+
+    @Test
+    public void testExpungeReturnsExpungedMessages() throws Exception {
+        start();
+        server.createUserMailbox("test1");
+        final byte[] message = readMessageResource("/messages/simple.msg");
+        server.appendToUserMailbox("test1", message);
+        server.appendToUserMailbox("test1", message);
+        server.appendToUserMailbox("test1", message);
+
+        final Store store = connect();
+        try {
+            final Folder folder = store.getDefaultFolder().getFolder("test1");
+            folder.open(Folder.READ_WRITE);
+            try {
+                assertEquals(3, folder.getMessageCount(), "test setup must provide three messages");
+
+                // flag two of the three messages for deletion
+                folder.getMessage(1).setFlag(Flags.Flag.DELETED, true);
+                folder.getMessage(3).setFlag(Flags.Flag.DELETED, true);
+
+                final Message[] expunged = folder.expunge();
+                assertNotNull(expunged, "expunge() must never return null");
+                assertEquals(2, expunged.length,
+                        "expunge() must return exactly the messages removed from the folder");
+                for (final Message m : expunged) {
+                    assertTrue(m.isExpunged(), "returned messages must be marked expunged");
+                }
+
+                // the message cache must have been updated exactly once per expunge
+                assertEquals(1, folder.getMessageCount(),
+                        "one message must remain after expunging two of three");
+            } finally {
+                folder.close(false);
+            }
+        } finally {
+            store.close();
+        }
+    }
+}
diff --git a/geronimo-mail_2.1_tck/src/tck/geronimo.jtx b/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
index bc723e6..945b564 100644
--- a/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
+++ b/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
@@ -33,7 +33,6 @@ SignatureTest.html
 # IMAPFolder.renameTo fix removed a ~90-test cascade). Distinct defects:
 # folder create/delete (topdog), permanent flags, list/listSubscribed,
 # several MIME encoding/decoding and unicode behaviours.
-javasoft/sqe/tests/jakarta/mail/exception/testlist.html#msgRemoveExp_Test
 javasoft/sqe/tests/jakarta/mail/Folder/testlist.html#getPermanentFlags_Test
 javasoft/sqe/tests/jakarta/mail/internet/InternetAddress/testlist.html#unicode_Test
 javasoft/sqe/tests/jakarta/mail/internet/MimeBodyPart/testlist.html#attachFile_saveFile_Test