(geronimo-mail) 07/07: GERONIMO-6901 - Failed Folder.open() leaks the pooled connection, breaks Store.close() and hides FolderNotFoundException

[email protected] Sat, 18 Jul 2026 18:49:06 +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 38a455a56b38078fe6fcf4fbf461e9ff89abf3d7
Author: Richard Zowalla <[email protected]>
AuthorDate: Sat Jul 18 20:48:28 2026 +0200

    GERONIMO-6901 - Failed Folder.open() leaks the pooled connection, breaks Store.close() and hides FolderNotFoundException
---
 .../geronimo/mail/store/imap/IMAPFolder.java       | 58 ++++++++++++++++++++--
 .../apache/geronimo/mail/store/imap/IMAPStore.java |  4 ++
 .../mail/store/imap/IMAPTckRegressionTest.java     | 33 ++++++++++++
 geronimo-mail_2.1_tck/src/tck/geronimo.jtx         |  1 -
 4 files changed, 90 insertions(+), 6 deletions(-)

diff --git a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPFolder.java b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPFolder.java
index a362a78..52dfcbc 100644
--- a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPFolder.java
+++ b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPFolder.java
@@ -35,6 +35,7 @@ import jakarta.mail.search.FlagTerm;
 import jakarta.mail.search.SearchTerm;
 
 import org.apache.geronimo.mail.store.imap.connection.*;
+import org.apache.geronimo.mail.util.CommandFailedException;
 
 /**
  * The base IMAP implementation of the javax.mail.Folder
@@ -652,8 +653,28 @@ public class IMAPFolder extends Folder implements UIDFolder, IMAPUntaggedRespons
 
 
             try {
-                // try to open, which gives us a lot of initial mailbox state.
-                IMAPMailboxStatus status = currentConnection.openMailbox(fullname, mode == Folder.READ_ONLY);
+                IMAPMailboxStatus status;
+                try {
+                    // try to open, which gives us a lot of initial mailbox state.
+                    status = currentConnection.openMailbox(fullname, mode == Folder.READ_ONLY);
+                } catch (CommandFailedException e) {
+                    // The SELECT/EXAMINE was answered with a tagged NO.  The most likely
+                    // reason is that the mailbox does not exist (or is not selectable),
+                    // which the spec requires us to report as a FolderNotFoundException.
+                    // Verify via a LIST on the same connection.
+                    listInfo = null;
+                    boolean missing;
+                    try {
+                        missing = !checkExistance(currentConnection);
+                    } catch (MessagingException e2) {
+                        // can't tell...report the original failure below.
+                        missing = false;
+                    }
+                    if (missing) {
+                        throw new FolderNotFoundException(this, "Folder " + fullname + " not found on server");
+                    }
+                    throw e;
+                }
 
                 // not available in the requested mode?
                 if (status.mode != mode) {
@@ -686,9 +707,36 @@ public class IMAPFolder extends Folder implements UIDFolder, IMAPUntaggedRespons
                 folderOpen = true;
                 notifyConnectionListeners(ConnectionEvent.OPENED);
             } finally {
-                // NB:  this doesn't really release this, but it does drive
-                // the processing of any unsolicited responses.
-                releaseConnection(currentConnection);
+                if (folderOpen) {
+                    // NB:  this doesn't really release this, but it does drive
+                    // the processing of any unsolicited responses.
+                    releaseConnection(currentConnection);
+                }
+                else {
+                    // The open failed.  The folder was registered with the Store and the
+                    // connection reserved before the SELECT/EXAMINE was attempted, so we
+                    // must fully unwind here:  drain any pending unsolicited responses,
+                    // deregister as a response handler, and return the connection to the
+                    // pool (which also removes us from the Store's open-folder list).
+                    // Failing to do so leaks the pooled connection and leaves the Store
+                    // thinking this folder is open, so a later Store.close() would call
+                    // Folder.close() on a closed folder and die with IllegalStateException.
+                    IMAPConnection conn = currentConnection;
+                    currentConnection = null;
+                    if (conn != null) {
+                        try {
+                            conn.processPendingResponses();
+                        } catch (MessagingException e) {
+                            // ignore...we're already on a failure path here.
+                        }
+                        conn.removeResponseHandler(this);
+                        try {
+                            ((IMAPStore)store).releaseFolderConnection(this, conn);
+                        } catch (MessagingException e) {
+                            // ignore...we're already on a failure path here.
+                        }
+                    }
+                }
             }
         }
 	}
diff --git a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPStore.java b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPStore.java
index 0502e65..e4a5691 100644
--- a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPStore.java
+++ b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPStore.java
@@ -482,6 +482,10 @@ public class IMAPStore extends Store implements QuotaAwareStore, IMAPUntaggedRes
                 try {
                     folder.close(false);
                 } catch (MessagingException e) {
+                } catch (IllegalStateException e) {
+                    // the folder was never successfully opened (or was already
+                    // closed).  Store.close() must never be aborted by a single
+                    // folder in a bad state.
                 }
             }
         }
diff --git a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/IMAPTckRegressionTest.java b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/IMAPTckRegressionTest.java
index b9bfc46..780219f 100644
--- a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/IMAPTckRegressionTest.java
+++ b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/IMAPTckRegressionTest.java
@@ -25,6 +25,7 @@ import java.util.concurrent.TimeUnit;
 
 import jakarta.mail.Flags;
 import jakarta.mail.Folder;
+import jakarta.mail.FolderNotFoundException;
 import jakarta.mail.Message;
 import jakarta.mail.Session;
 import jakarta.mail.Store;
@@ -36,6 +37,7 @@ import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -199,6 +201,37 @@ public class IMAPTckRegressionTest extends AbstractProtocolTest {
         }
     }
 
+    /**
+     * exception#folderNotFoundExp_Test: opening a nonexistent folder must throw
+     * FolderNotFoundException (not the raw CommandFailedException of the failed
+     * SELECT/EXAMINE).  In addition, the failed open used to leave the folder
+     * registered as open in the Store and leak the pooled connection, so a
+     * subsequent Store.close() blew up with an IllegalStateException.
+     */
+    @Test
+    public void testOpenNonexistentFolderThrowsFolderNotFoundAndStoreStillCloses() throws Exception {
+        start();
+
+        final Store store = connect();
+        try {
+            final Folder folder = store.getDefaultFolder().getFolder("noSuchFolder");
+            assertThrows(FolderNotFoundException.class, () -> folder.open(Folder.READ_ONLY),
+                    "opening a nonexistent folder must throw FolderNotFoundException");
+            assertFalse(folder.isOpen(), "the folder must not claim to be open after a failed open");
+
+            // a folder that really exists must still be openable afterwards,
+            // proving the pooled connection was returned in a usable state
+            createMailboxWithMessage("test1");
+            final Folder good = store.getDefaultFolder().getFolder("test1");
+            good.open(Folder.READ_ONLY);
+            assertTrue(good.getMessageCount() == 1);
+            good.close(false);
+        } finally {
+            // must not throw IllegalStateException because of the failed open
+            store.close();
+        }
+    }
+
     /**
      * MimeMessage#getContentLanguage_Test: a message whose BODYSTRUCTURE
      * carries no language information used to trigger a NullPointerException;
diff --git a/geronimo-mail_2.1_tck/src/tck/geronimo.jtx b/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
index 95b54cc..bc723e6 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#folderNotFoundExp_Test
 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