You are viewing a plain text version of this content. The canonical link for it is here.
Posted to scm@geronimo.apache.org by rz...@apache.org on 2026/07/18 18:48:59 UTC
(geronimo-mail) branch main updated (dfe8613 -> 38a455a)
This is an automated email from the ASF dual-hosted git repository.
rzo1 pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/geronimo-mail.git
from dfe8613 GERONIMO-6895 - Folder.fetch() fails on RFC-strict servers - header names emitted outside the BODY.PEEK[HEADER.FIELDS] section brackets
new 1412a87 Ignore the .claude tooling directory
new 51c2a9a GERONIMO-6896 - getContentLanguage() throws NPE for IMAP messages whose BODYSTRUCTURE has no language field
new f099579 GERONIMO-6897 - Folder.create() sends the uninitialized hierarchy separator, creating a mailbox named "name\0"
new e4ab6c1 GERONIMO-6898 - store.getDefaultFolder().list() returns empty when the server hierarchy delimiter is not '/'
new b5bb2d2 GERONIMO-6899 - Single-message Message.setFlags() never fires MessageChangedEvent
new f1cecee GERONIMO-6900 - Folder.close(false) loses \Deleted messages - use UNSELECT when the server supports RFC 3691
new 38a455a GERONIMO-6901 - Failed Folder.open() leaks the pooled connection, breaks Store.close() and hides FolderNotFoundException
The 7 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.gitignore | 1 +
.../geronimo/mail/store/imap/IMAPFolder.java | 122 ++++++++--
.../geronimo/mail/store/imap/IMAPMessage.java | 9 +-
.../apache/geronimo/mail/store/imap/IMAPStore.java | 4 +
.../mail/store/imap/connection/IMAPConnection.java | 14 ++
.../mail/store/imap/IMAPTckRegressionTest.java | 261 +++++++++++++++++++++
geronimo-mail_2.1_tck/src/tck/geronimo.jtx | 15 --
7 files changed, 393 insertions(+), 33 deletions(-)
create mode 100644 geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/IMAPTckRegressionTest.java
(geronimo-mail) 03/07: GERONIMO-6897 - Folder.create() sends the uninitialized hierarchy separator, creating a mailbox named "name\0"
Posted by rz...@apache.org.
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 f09957901b3d63af6b8cc20ebeddaeff8ee34590
Author: Richard Zowalla <rz...@apache.org>
AuthorDate: Sat Jul 18 20:48:28 2026 +0200
GERONIMO-6897 - Folder.create() sends the uninitialized hierarchy separator, creating a mailbox named "name\0"
---
.../geronimo/mail/store/imap/IMAPFolder.java | 20 +++++++++++---
.../mail/store/imap/IMAPTckRegressionTest.java | 31 ++++++++++++++++++++++
geronimo-mail_2.1_tck/src/tck/geronimo.jtx | 3 ---
3 files changed, 48 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 f228d24..d94d6e8 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
@@ -326,9 +326,19 @@ public class IMAPFolder extends Folder implements UIDFolder, IMAPUntaggedRespons
List responses = connection.list("", fullname);
IMAPListResponse info = findListResponse(responses, fullname);
- // if we didn't get any hits, then we just assume a reasonable default.
+ // if we didn't get any hits (e.g., the folder does not exist yet),
+ // ask the server for the hierarchy delimiter directly. RFC 3501
+ // defines the special form LIST "" "" as returning the hierarchy
+ // delimiter and the root name without matching any mailboxes.
if (info == null) {
- separator = '/';
+ List delimiterResponses = connection.list("", "");
+ if (!delimiterResponses.isEmpty()) {
+ separator = ((IMAPListResponse)delimiterResponses.get(0)).separator;
+ }
+ // no useful answer from the server, assume a reasonable default.
+ if (separator == '\0') {
+ separator = '/';
+ }
}
else {
separator = info.separator;
@@ -384,7 +394,11 @@ public class IMAPFolder extends Folder implements UIDFolder, IMAPUntaggedRespons
// if this folder is expected to only hold additional folders, we need to
// add a separator on to the end when we create this.
if ((newType & HOLDS_MESSAGES) == 0) {
- newPath = fullname + separator;
+ // NB: we must use getSeparator() here, not the raw field. For folders
+ // obtained from the default folder the separator is still UNDETERMINED
+ // ('\0'), and appending that raw character would send a corrupted
+ // (modified-UTF-7 encoded) mailbox name to the server.
+ newPath = fullname + getSeparator();
}
try {
// go create this
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 667f94f..57c11d8 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
@@ -27,7 +27,9 @@ import jakarta.mail.Store;
import org.apache.geronimo.mail.testserver.AbstractProtocolTest;
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.assertTrue;
/**
* Regression tests for a set of IMAP provider defects surfaced by the
@@ -62,6 +64,35 @@ public class IMAPTckRegressionTest extends AbstractProtocolTest {
server.appendToUserMailbox(name, readMessageResource("/messages/simple.msg"));
}
+ /**
+ * Folder#create_Test/delete_Test/getType_Test: a top-level folder obtained
+ * via the default folder has an UNDETERMINED separator; create() used to
+ * append the raw '\0' separator to the mailbox name on the wire
+ * ("CREATE topdog&AAA-"), so the folder was created under a mangled name
+ * and create() reported failure. The James hierarchy delimiter is '.',
+ * not the '/' fallback, so the separator must be obtained from the server.
+ */
+ @Test
+ public void testCreateDeleteGetTypeRoundTrip() throws Exception {
+ start();
+
+ final Store store = connect();
+ try {
+ final Folder folder = store.getDefaultFolder().getFolder("topdog");
+ assertFalse(folder.exists(), "folder must not exist before creation");
+
+ assertTrue(folder.create(Folder.HOLDS_FOLDERS), "create must succeed");
+ assertTrue(folder.exists(), "created folder must exist under its own name");
+ assertTrue((folder.getType() & Folder.HOLDS_FOLDERS) != 0,
+ "created folder must be able to hold folders");
+
+ assertTrue(folder.delete(false), "delete must succeed");
+ assertFalse(folder.exists(), "deleted folder must no longer exist");
+ } finally {
+ 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 0cbef10..9a534c2 100644
--- a/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
+++ b/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
@@ -43,10 +43,7 @@ javasoft/sqe/tests/jakarta/mail/Folder/testlist.html#appendMessages_Test
javasoft/sqe/tests/jakarta/mail/event/FolderEvent/testlist.html#addMsgChangeList_Test
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#create_Test
-javasoft/sqe/tests/jakarta/mail/Folder/testlist.html#delete_Test
javasoft/sqe/tests/jakarta/mail/Folder/testlist.html#getPermanentFlags_Test
-javasoft/sqe/tests/jakarta/mail/Folder/testlist.html#getType_Test
javasoft/sqe/tests/jakarta/mail/Folder/testlist.html#list_Test
javasoft/sqe/tests/jakarta/mail/Folder/testlist.html#listSubscribed_Test
javasoft/sqe/tests/jakarta/mail/internet/InternetAddress/testlist.html#unicode_Test
(geronimo-mail) 04/07: GERONIMO-6898 - store.getDefaultFolder().list() returns empty when the server hierarchy delimiter is not '/'
Posted by rz...@apache.org.
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 e4ab6c1fe983197c54367d19c840d4b0d2e07dac
Author: Richard Zowalla <rz...@apache.org>
AuthorDate: Sat Jul 18 20:48:28 2026 +0200
GERONIMO-6898 - store.getDefaultFolder().list() returns empty when the server hierarchy delimiter is not '/'
---
.../geronimo/mail/store/imap/IMAPFolder.java | 8 +++++--
.../mail/store/imap/IMAPTckRegressionTest.java | 26 ++++++++++++++++++++++
geronimo-mail_2.1_tck/src/tck/geronimo.jtx | 2 --
3 files changed, 32 insertions(+), 4 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 d94d6e8..62bebeb 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
@@ -1594,8 +1594,12 @@ public class IMAPFolder extends Folder implements UIDFolder, IMAPUntaggedRespons
*/
protected synchronized Folder[] filterFolders(String pattern, boolean subscribed) throws MessagingException {
IMAPConnection connection = getConnection();
- // this is used to filter out our own folder from the search
- String root = fullname + getSeparator();
+ // this is used to filter out our own folder from the search.
+ // NB: for the root (default) folder the fullname is empty and the
+ // reference must be the empty string as well. Using the hard-coded
+ // root separator (e.g. "/") as the reference does not match anything
+ // on servers using a different hierarchy delimiter (James uses '.').
+ String root = fullname.length() == 0 ? "" : fullname + getSeparator();
List responses = null;
try {
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 57c11d8..6cb5727 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
@@ -93,6 +93,32 @@ public class IMAPTckRegressionTest extends AbstractProtocolTest {
}
}
+ /**
+ * Folder#list_Test/listSubscribed_Test: listing the default folder used to
+ * issue LIST / "%" (the hard-coded root separator as reference), which
+ * matches nothing on servers with a '.' hierarchy delimiter such as James.
+ * The reference for the root folder must be the empty string.
+ */
+ @Test
+ public void testDefaultFolderListIsNonEmpty() throws Exception {
+ start();
+ createMailboxWithMessage("test1");
+
+ final Store store = connect();
+ try {
+ final Folder[] folders = store.getDefaultFolder().list("%");
+ assertTrue(folders.length > 0, "default folder list(\"%\") must return at least INBOX");
+
+ // also exercise the LSUB flavour of the same code path
+ final Folder test1 = store.getDefaultFolder().getFolder("test1");
+ test1.setSubscribed(true);
+ final Folder[] subscribed = store.getDefaultFolder().listSubscribed("%");
+ assertTrue(subscribed.length > 0, "default folder listSubscribed(\"%\") must find the subscribed folder");
+ } finally {
+ 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 9a534c2..c00fe5a 100644
--- a/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
+++ b/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
@@ -44,8 +44,6 @@ javasoft/sqe/tests/jakarta/mail/event/FolderEvent/testlist.html#addMsgChangeList
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/Folder/testlist.html#list_Test
-javasoft/sqe/tests/jakarta/mail/Folder/testlist.html#listSubscribed_Test
javasoft/sqe/tests/jakarta/mail/internet/InternetAddress/testlist.html#unicode_Test
javasoft/sqe/tests/jakarta/mail/internet/MimeBodyPart/testlist.html#attachFile_saveFile_Test
javasoft/sqe/tests/jakarta/mail/internet/MimeBodyPart/testlist.html#isMimeType_Test
(geronimo-mail) 02/07: GERONIMO-6896 - getContentLanguage() throws NPE for IMAP messages whose BODYSTRUCTURE has no language field
Posted by rz...@apache.org.
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 51c2a9af39024002ea6da7a7ab17506c39d36608
Author: Richard Zowalla <rz...@apache.org>
AuthorDate: Sat Jul 18 20:48:28 2026 +0200
GERONIMO-6896 - getContentLanguage() throws NPE for IMAP messages whose BODYSTRUCTURE has no language field
---
.../geronimo/mail/store/imap/IMAPMessage.java | 3 +-
.../mail/store/imap/IMAPTckRegressionTest.java | 91 ++++++++++++++++++++++
geronimo-mail_2.1_tck/src/tck/geronimo.jtx | 1 -
3 files changed, 93 insertions(+), 2 deletions(-)
diff --git a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPMessage.java b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPMessage.java
index 7fa8f11..722bfb0 100644
--- a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPMessage.java
+++ b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPMessage.java
@@ -550,7 +550,8 @@ public class IMAPMessage extends MimeMessage {
public String[] getContentLanguage() throws MessagingException {
loadBodyStructure();
- if (!bodyStructure.languages.isEmpty()) {
+ // the BODYSTRUCTURE response may not have included any language information at all
+ if (bodyStructure.languages != null && !bodyStructure.languages.isEmpty()) {
return (String[])bodyStructure.languages.toArray(new String[bodyStructure.languages.size()]);
}
return null;
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
new file mode 100644
index 0000000..667f94f
--- /dev/null
+++ b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/IMAPTckRegressionTest.java
@@ -0,0 +1,91 @@
+/**
+ * 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.Folder;
+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.assertNull;
+
+/**
+ * Regression tests for a set of IMAP provider defects surfaced by the
+ * Jakarta Mail TCK when run against Apache James.
+ */
+public class IMAPTckRegressionTest 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 = IMAPTckRegressionTest.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();
+ }
+
+ protected void createMailboxWithMessage(final String name) throws Exception {
+ server.createUserMailbox(name);
+ server.appendToUserMailbox(name, readMessageResource("/messages/simple.msg"));
+ }
+
+ /**
+ * MimeMessage#getContentLanguage_Test: a message whose BODYSTRUCTURE
+ * carries no language information used to trigger a NullPointerException;
+ * the spec requires a null return instead.
+ */
+ @Test
+ public void testGetContentLanguageWithoutLanguagesReturnsNull() throws Exception {
+ start();
+ createMailboxWithMessage("test1");
+
+ final Store store = connect();
+ try {
+ final Folder folder = store.getDefaultFolder().getFolder("test1");
+ folder.open(Folder.READ_ONLY);
+ try {
+ final jakarta.mail.internet.MimeMessage msg =
+ (jakarta.mail.internet.MimeMessage) folder.getMessage(1);
+ assertNull(msg.getContentLanguage(),
+ "getContentLanguage must return null when the message has no Content-Language");
+ } 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 844f991..0cbef10 100644
--- a/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
+++ b/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
@@ -54,7 +54,6 @@ javasoft/sqe/tests/jakarta/mail/internet/MimeBodyPart/testlist.html#attachFile_s
javasoft/sqe/tests/jakarta/mail/internet/MimeBodyPart/testlist.html#isMimeType_Test
javasoft/sqe/tests/jakarta/mail/internet/MimeBodyPart/testlist.html#setContentLanguage_Test
javasoft/sqe/tests/jakarta/mail/internet/MimeBodyPart/testlist.html#setFileNameEncoded_Test
-javasoft/sqe/tests/jakarta/mail/internet/MimeMessage/testlist.html#getContentLanguage_Test
javasoft/sqe/tests/jakarta/mail/internet/MimeMessage/testlist.html#setFileNameEncoded_Test
javasoft/sqe/tests/jakarta/mail/internet/MimeMessage/testlist.html#setSentDate_Test
javasoft/sqe/tests/jakarta/mail/internet/MimeMessage/testlist.html#unicode_Test
(geronimo-mail) 06/07: GERONIMO-6900 - Folder.close(false) loses \Deleted messages - use UNSELECT when the server supports RFC 3691
Posted by rz...@apache.org.
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 f1ceceedf80ea4633b04493d8792ac27096c4427
Author: Richard Zowalla <rz...@apache.org>
AuthorDate: Sat Jul 18 20:48:28 2026 +0200
GERONIMO-6900 - Folder.close(false) loses \Deleted messages - use UNSELECT when the server supports RFC 3691
---
.../geronimo/mail/store/imap/IMAPFolder.java | 22 +++++++++-----
.../mail/store/imap/connection/IMAPConnection.java | 14 +++++++++
.../mail/store/imap/IMAPTckRegressionTest.java | 35 ++++++++++++++++++++++
geronimo-mail_2.1_tck/src/tck/geronimo.jtx | 7 -----
4 files changed, 64 insertions(+), 14 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 1309793..a362a78 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
@@ -737,15 +737,23 @@ public class IMAPFolder extends Folder implements UIDFolder, IMAPUntaggedRespons
// The CLOSE operation depends on what mode was used to select the mailbox.
// If we're open in READ-WRITE mode, we used a SELECT operation. When CLOSE
// is issued, any deleted messages will be expunged. If we've been asked not
- // to expunge the messages, we have a problem. The solution is to reselect the
- // mailbox using EXAMINE, which will not expunge messages when closed.
- if (mode == READ_WRITE && !expunge) {
- // we can ignore the result...we're just switching modes.
- currentConnection.openMailbox(fullname, true);
+ // to expunge the messages, we have a problem. If the server supports the
+ // UNSELECT extension (RFC 3691), that deselects the mailbox without an
+ // implicit expunge. Otherwise we fall back to reselecting the mailbox
+ // using EXAMINE before issuing CLOSE. NB: the fallback is not reliable
+ // on all servers (e.g., Apache James expunges on CLOSE even after EXAMINE).
+ if (mode == READ_WRITE && !expunge && currentConnection.hasCapability("UNSELECT")) {
+ currentConnection.unselectMailbox();
}
+ else {
+ if (mode == READ_WRITE && !expunge) {
+ // we can ignore the result...we're just switching modes.
+ currentConnection.openMailbox(fullname, true);
+ }
- // have this close the selected mailbox
- currentConnection.closeMailbox();
+ // have this close the selected mailbox
+ currentConnection.closeMailbox();
+ }
}
currentConnection.removeResponseHandler(this);
// we need to release the connection to the Store once we're closed
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 97deeba..9351115 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
@@ -459,6 +459,20 @@ public class IMAPConnection extends MailConnection {
}
+ /**
+ * Deselect a mailbox without expunging any messages flagged
+ * \Deleted (RFC 3691). Only usable when the server advertises
+ * the UNSELECT capability.
+ *
+ * @exception MessagingException
+ */
+ public void unselectMailbox() throws MessagingException {
+ // We can just send the command and generally ignore the
+ // status response.
+ sendCommand("UNSELECT");
+ }
+
+
/**
* Authenticate with the server, if necessary (or possible).
*
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 40ef672..b9bfc46 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
@@ -164,6 +164,41 @@ public class IMAPTckRegressionTest extends AbstractProtocolTest {
}
}
+ /**
+ * Folder#appendMessages_Test (unit 3): close(false) must not expunge
+ * messages flagged \Deleted. The EXAMINE+CLOSE trick used for
+ * close-without-expunge does not work on James (it expunges on CLOSE
+ * anyway); the server advertises UNSELECT which must be used instead.
+ */
+ @Test
+ public void testCloseWithoutExpungeKeepsDeletedMessages() throws Exception {
+ start();
+ createMailboxWithMessage("test1");
+ server.appendToUserMailbox("test1", readMessageResource("/messages/simple.msg"));
+
+ final Store store = connect();
+ try {
+ final Folder folder = store.getDefaultFolder().getFolder("test1");
+ folder.open(Folder.READ_WRITE);
+ assertTrue(folder.getMessageCount() == 2, "test setup must provide two messages");
+ folder.getMessage(1).setFlag(Flags.Flag.DELETED, true);
+ folder.close(false);
+
+ final Folder reopened = store.getDefaultFolder().getFolder("test1");
+ reopened.open(Folder.READ_ONLY);
+ try {
+ assertTrue(reopened.getMessageCount() == 2,
+ "close(false) must not expunge messages flagged \\Deleted");
+ assertTrue(reopened.getMessage(1).isSet(Flags.Flag.DELETED),
+ "the \\Deleted flag must survive close(false)");
+ } finally {
+ reopened.close(false);
+ }
+ } finally {
+ 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 460fa6b..95b54cc 100644
--- a/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
+++ b/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
@@ -29,13 +29,6 @@
SignatureTest.html
-# Folder.close(false) must not expunge: the client issues CLOSE, which per
-# RFC 3501 always expunges \Deleted messages; not expunging requires UNSELECT
-# (or dropping the selection another way). Surfaced once the APPEND deadlock
-# was fixed (appendMessages_Test unit 3 appends \Deleted-flagged messages and
-# expects them to survive close(false)).
-javasoft/sqe/tests/jakarta/mail/Folder/testlist.html#appendMessages_Test
-
# Remaining baseline failures (284 passed / 31 failed after the
# IMAPFolder.renameTo fix removed a ~90-test cascade). Distinct defects:
# folder create/delete (topdog), permanent flags, list/listSubscribed,
(geronimo-mail) 07/07: GERONIMO-6901 - Failed Folder.open() leaks the pooled connection, breaks Store.close() and hides FolderNotFoundException
Posted by rz...@apache.org.
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 <rz...@apache.org>
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
(geronimo-mail) 05/07: GERONIMO-6899 - Single-message Message.setFlags() never fires MessageChangedEvent
Posted by rz...@apache.org.
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 b5bb2d2faa17cc11b0189fc0c5af1276776cbcad
Author: Richard Zowalla <rz...@apache.org>
AuthorDate: Sat Jul 18 20:48:28 2026 +0200
GERONIMO-6899 - Single-message Message.setFlags() never fires MessageChangedEvent
---
.../geronimo/mail/store/imap/IMAPFolder.java | 14 +++++++
.../geronimo/mail/store/imap/IMAPMessage.java | 6 +++
.../mail/store/imap/IMAPTckRegressionTest.java | 45 ++++++++++++++++++++++
geronimo-mail_2.1_tck/src/tck/geronimo.jtx | 1 -
4 files changed, 65 insertions(+), 1 deletion(-)
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 62bebeb..1309793 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
@@ -1753,6 +1753,20 @@ public class IMAPFolder extends Folder implements UIDFolder, IMAPUntaggedRespons
}
+ /**
+ * Notify the message changed listeners that the flags of a
+ * message contained in the folder have been updated. Used by
+ * IMAPMessage.setFlags(), where the untagged FETCH reply to the
+ * STORE command is consumed by the connection, so the generic
+ * unsolicited-response handling never sees a flag update.
+ *
+ * @param m The message whose flags were changed.
+ */
+ void notifyMessageFlagsChanged(Message m) {
+ notifyMessageChangedListeners(MessageChangedEvent.FLAGS_CHANGED, m);
+ }
+
+
/**
* Retrieve the connection attached to this folder. Throws an
* exception if we don't have an active connection.
diff --git a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPMessage.java b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPMessage.java
index 722bfb0..d18881e 100644
--- a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPMessage.java
+++ b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/IMAPMessage.java
@@ -199,6 +199,12 @@ public class IMAPMessage extends MimeMessage {
releaseConnection(connection);
}
}
+
+ // the untagged FETCH response to the STORE command was consumed above,
+ // so the folder never sees it as an unsolicited flag update. Notify the
+ // listeners directly. This is done outside the folder lock, matching the
+ // bulk Folder.setFlags() behavior.
+ ((IMAPFolder)folder).notifyMessageFlagsChanged(this);
}
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 6cb5727..40ef672 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
@@ -20,9 +20,16 @@ import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.mail.Flags;
import jakarta.mail.Folder;
+import jakarta.mail.Message;
import jakarta.mail.Session;
import jakarta.mail.Store;
+import jakarta.mail.event.MessageChangedEvent;
+import jakarta.mail.event.MessageChangedListener;
import org.apache.geronimo.mail.testserver.AbstractProtocolTest;
import org.junit.jupiter.api.Test;
@@ -119,6 +126,44 @@ public class IMAPTckRegressionTest extends AbstractProtocolTest {
}
}
+ /**
+ * event/FolderEvent#addMsgChangeList_Test: a MessageChangedListener must
+ * fire when a flag is changed through a single message (Message.setFlags).
+ * The untagged FETCH reply to the STORE command is consumed inside
+ * IMAPConnection.setFlags, so the folder must notify the listeners itself.
+ */
+ @Test
+ public void testMessageChangedListenerFiresOnSingleMessageSetFlag() throws Exception {
+ start();
+ createMailboxWithMessage("test1");
+
+ final Store store = connect();
+ try {
+ final Folder folder = store.getDefaultFolder().getFolder("test1");
+ folder.open(Folder.READ_WRITE);
+ try {
+ final CountDownLatch changed = new CountDownLatch(1);
+ folder.addMessageChangedListener(new MessageChangedListener() {
+ public void messageChanged(final MessageChangedEvent e) {
+ if (e.getMessageChangeType() == MessageChangedEvent.FLAGS_CHANGED) {
+ changed.countDown();
+ }
+ }
+ });
+
+ final Message msg = folder.getMessage(1);
+ msg.setFlag(Flags.Flag.ANSWERED, true);
+
+ assertTrue(changed.await(10, TimeUnit.SECONDS),
+ "a MessageChangedEvent must be delivered for a single-message setFlag");
+ } finally {
+ folder.close(false);
+ }
+ } finally {
+ 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 c00fe5a..460fa6b 100644
--- a/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
+++ b/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
@@ -40,7 +40,6 @@ javasoft/sqe/tests/jakarta/mail/Folder/testlist.html#appendMessages_Test
# 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/event/FolderEvent/testlist.html#addMsgChangeList_Test
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
(geronimo-mail) 01/07: Ignore the .claude tooling directory
Posted by rz...@apache.org.
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 1412a875c1e8cf148df173309ea52b09de5922ad
Author: Richard Zowalla <rz...@apache.org>
AuthorDate: Sat Jul 18 20:48:28 2026 +0200
Ignore the .claude tooling directory
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index a6afba0..4bab0f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@ target
*.vscode
*~
dependency-reduced-pom.xml
+.claude/