(geronimo-mail) 05/05: GERONIMO-6905 - IMAP response codes in brackets (PERMANENTFLAGS, UIDVALIDITY, UIDNEXT, UNSEEN) are never parsed

[email protected] Sat, 18 Jul 2026 19:12:31 +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 beafafb7720ceb78cafbe70aaac36bab15da1a4e
Author: Richard Zowalla <[email protected]>
AuthorDate: Sat Jul 18 21:10:28 2026 +0200

    GERONIMO-6905 - IMAP response codes in brackets (PERMANENTFLAGS, UIDVALIDITY, UIDNEXT, UNSEEN) are never parsed
---
 .../store/imap/connection/IMAPMailboxStatus.java   |   4 +-
 .../store/imap/connection/IMAPResponseStream.java  |  17 ++--
 .../imap/IMAPPermanentFlagsRegressionTest.java     |  94 +++++++++++++++++++
 .../imap/connection/IMAPOkResponseParsingTest.java | 100 +++++++++++++++++++++
 geronimo-mail_2.1_tck/src/tck/geronimo.jtx         |   1 -
 5 files changed, 207 insertions(+), 9 deletions(-)

diff --git a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/connection/IMAPMailboxStatus.java b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/connection/IMAPMailboxStatus.java
index 3d6646f..b12e7b4 100644
--- a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/connection/IMAPMailboxStatus.java
+++ b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/connection/IMAPMailboxStatus.java
@@ -163,8 +163,8 @@ public class IMAPMailboxStatus {
             }
             // untagged unseen response
             else if (source.isKeyword("UNSEEN")) {
-                List arguments = source.getStatus(); 
-                uidValidity = ((Token)arguments.get(0)).getInteger(); 
+                List arguments = source.getStatus();
+                unseenMessages = ((Token)arguments.get(0)).getInteger();
             }
         }
     }
diff --git a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/connection/IMAPResponseStream.java b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/connection/IMAPResponseStream.java
index 221031c..f6f0a44 100644
--- a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/connection/IMAPResponseStream.java
+++ b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/main/java/org/apache/geronimo/mail/store/imap/connection/IMAPResponseStream.java
@@ -239,7 +239,11 @@ public class IMAPResponseStream {
      * @return An IMAPResponse instance for this message.
      */
     private IMAPResponse parseUntaggedOkResponse(byte [] data, IMAPResponseTokenizer tokenizer) throws MessagingException {
-        Token token = tokenizer.peek();
+        // NB: '[' and ']' are only delimiters in the expanded delimiter set, so all
+        // tokens of the response code section must be read in that mode.  With the
+        // default atom delimiters "[PERMANENTFLAGS" would come back as a single
+        // ATOM token and the response code would never be recognized.
+        Token token = tokenizer.peek(false, true);
         // we might have an optional value here
         if (token.getType() != '[') {
             // this has no tagging item, so there's nothing to be processed
@@ -247,8 +251,8 @@ public class IMAPResponseStream {
             return new IMAPOkResponse("OK", null, tokenizer.getRemainder(), data);
         }
         // skip over the "[" token
-        tokenizer.next();
-        token = tokenizer.next();
+        tokenizer.next(false, true);
+        token = tokenizer.next(false, true);
         String keyword = token.getValue();
 
         // Permanent flags gets special handling
@@ -259,10 +263,11 @@ public class IMAPResponseStream {
         ArrayList arguments = new ArrayList();
 
         // strip off all of the argument tokens until the "]" list terminator.
-        token = tokenizer.next();
-        while (token.getType() != ']') {
+        // guard against EOF in case the terminator is missing from the response.
+        token = tokenizer.next(false, true);
+        while (token.getType() != ']' && token.getType() != Token.EOF) {
             arguments.add(token);
-            token = tokenizer.next();
+            token = tokenizer.next(false, true);
         }
         // this has a tagged keyword and arguments that will be processed later.
         return new IMAPOkResponse(keyword, arguments, tokenizer.getRemainder(), data);
diff --git a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/IMAPPermanentFlagsRegressionTest.java b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/IMAPPermanentFlagsRegressionTest.java
new file mode 100644
index 0000000..3063d91
--- /dev/null
+++ b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/IMAPPermanentFlagsRegressionTest.java
@@ -0,0 +1,94 @@
+/**
+ *  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.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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Regression test for the respcode-bracket-parsing defect surfaced by the
+ * Jakarta Mail TCK (Folder#getPermanentFlags_Test): the '* OK
+ * [PERMANENTFLAGS ...]', '* OK [UIDVALIDITY ...]' etc. response codes sent
+ * during SELECT collapsed into generic OK responses because '[' was not a
+ * delimiter in the tokenizer's default atom mode, so getPermanentFlags()
+ * always returned null and the open-time UID metadata was lost.
+ */
+public class IMAPPermanentFlagsRegressionTest 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 = IMAPPermanentFlagsRegressionTest.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 testPermanentFlagsAndUidValidityAfterOpen() throws Exception {
+        start();
+        server.createUserMailbox("test1");
+        server.appendToUserMailbox("test1", readMessageResource("/messages/simple.msg"));
+
+        final Store store = connect();
+        try {
+            final IMAPFolder folder = (IMAPFolder) store.getDefaultFolder().getFolder("test1");
+            folder.open(Folder.READ_WRITE);
+            try {
+                final Flags permanentFlags = folder.getPermanentFlags();
+                assertNotNull(permanentFlags,
+                        "getPermanentFlags() must reflect the PERMANENTFLAGS response code from SELECT");
+                assertTrue(permanentFlags.contains(Flags.Flag.SEEN),
+                        "the permanent flags advertised by James must include \\Seen");
+                assertTrue(permanentFlags.contains(Flags.Flag.DELETED),
+                        "the permanent flags advertised by James must include \\Deleted");
+
+                assertTrue(folder.getUIDValidity() > 0,
+                        "getUIDValidity() must be populated from the UIDVALIDITY response code");
+            } finally {
+                folder.close(false);
+            }
+        } finally {
+            store.close();
+        }
+    }
+}
diff --git a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/connection/IMAPOkResponseParsingTest.java b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/connection/IMAPOkResponseParsingTest.java
new file mode 100644
index 0000000..4ea0167
--- /dev/null
+++ b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/connection/IMAPOkResponseParsingTest.java
@@ -0,0 +1,100 @@
+/**
+ *  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.connection;
+
+import java.io.ByteArrayInputStream;
+
+import jakarta.mail.Flags;
+
+import org.apache.geronimo.mail.store.imap.connection.IMAPResponseTokenizer.Token;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Regression tests for the respcode-bracket-parsing defect: '[' is not a
+ * delimiter in the tokenizer's default atom mode, so "[PERMANENTFLAGS"
+ * arrived as a single ATOM token and every '* OK [keyword args]' response
+ * code collapsed into a generic OK response.  getPermanentFlags() therefore
+ * always returned null and the open-time UIDVALIDITY/UIDNEXT/UNSEEN metadata
+ * was silently lost.
+ */
+public class IMAPOkResponseParsingTest {
+
+    private IMAPResponse parse(final String line) throws Exception {
+        final IMAPResponseStream stream =
+                new IMAPResponseStream(new ByteArrayInputStream(line.getBytes("ISO8859-1")));
+        return stream.readResponse();
+    }
+
+    @Test
+    public void testPlainOkResponseHasOkKeyword() throws Exception {
+        final IMAPOkResponse response = (IMAPOkResponse) parse("* OK ready to serve\r\n");
+        assertTrue(response.isKeyword("OK"));
+        assertEquals("ready to serve", response.getMessage().trim());
+    }
+
+    @Test
+    public void testUidValidityResponseCodeIsRecognized() throws Exception {
+        final IMAPOkResponse response = (IMAPOkResponse) parse("* OK [UIDVALIDITY 137045278] UIDs valid\r\n");
+        assertTrue(response.isKeyword("UIDVALIDITY"),
+                "the response code inside the brackets must become the response keyword");
+        assertEquals(137045278L, ((Token) response.getStatus().get(0)).getLong());
+    }
+
+    @Test
+    public void testUidNextResponseCodeIsRecognized() throws Exception {
+        final IMAPOkResponse response = (IMAPOkResponse) parse("* OK [UIDNEXT 4] Predicted next UID\r\n");
+        assertTrue(response.isKeyword("UIDNEXT"));
+        assertEquals(4L, ((Token) response.getStatus().get(0)).getLong());
+    }
+
+    @Test
+    public void testUnseenResponseCodeIsRecognized() throws Exception {
+        final IMAPOkResponse response = (IMAPOkResponse) parse("* OK [UNSEEN 3] Message 3 is first unseen\r\n");
+        assertTrue(response.isKeyword("UNSEEN"));
+        assertEquals(3, ((Token) response.getStatus().get(0)).getInteger());
+    }
+
+    @Test
+    public void testPermanentFlagsResponseCodeIsRecognized() throws Exception {
+        final IMAPPermanentFlagsResponse response = (IMAPPermanentFlagsResponse)
+                parse("* OK [PERMANENTFLAGS (\\Answered \\Deleted \\Seen \\*)] Limited\r\n");
+        assertTrue(response.isKeyword("PERMANENTFLAGS"));
+        assertTrue(response.flags.contains(Flags.Flag.SEEN));
+        assertTrue(response.flags.contains(Flags.Flag.ANSWERED));
+        assertTrue(response.flags.contains(Flags.Flag.DELETED));
+        assertTrue(response.flags.contains(Flags.Flag.USER),
+                "the \\* wildcard must map to the USER flag");
+    }
+
+    @Test
+    public void testMailboxStatusMergesOkResponseCodes() throws Exception {
+        final IMAPMailboxStatus status = new IMAPMailboxStatus();
+        status.mergeStatus((IMAPOkResponse) parse("* OK [UIDVALIDITY 137045278] UIDs valid\r\n"));
+        status.mergeStatus((IMAPOkResponse) parse("* OK [UIDNEXT 4] Predicted next UID\r\n"));
+        status.mergeStatus((IMAPOkResponse) parse("* OK [UNSEEN 3] first unseen\r\n"));
+
+        assertEquals(137045278L, status.uidValidity);
+        assertEquals(4L, status.uidNext);
+        // regression: the UNSEEN branch used to assign uidValidity instead
+        assertEquals(3, status.unseenMessages);
+        assertEquals(137045278L, status.uidValidity,
+                "merging UNSEEN must not clobber the uidValidity value");
+    }
+}
diff --git a/geronimo-mail_2.1_tck/src/tck/geronimo.jtx b/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
index 945b564..1788637 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/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
 javasoft/sqe/tests/jakarta/mail/internet/MimeBodyPart/testlist.html#isMimeType_Test