(geronimo-mail) 03/05: GERONIMO-6903 - IMAP response parser drops the byte following a bare CR, corrupting the stream

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

    GERONIMO-6903 - IMAP response parser drops the byte following a bare CR, corrupting the stream
---
 .../store/imap/connection/IMAPResponseStream.java  | 10 ++-
 .../imap/connection/IMAPResponseStreamTest.java    | 94 ++++++++++++++++++++++
 2 files changed, 103 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/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 cc5ac04..221031c 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
@@ -295,8 +295,8 @@ public class IMAPResponseStream {
      * @exception IOException
      */
     public void readBuffer() throws MessagingException {
+        int ch = nextByte();
         while (true) {
-            int ch = nextByte();
             // potential end of line?  Check the next character, and if it is an end of line,
             // we need to do literal processing.
             if (ch == '\r') {
@@ -307,9 +307,17 @@ public class IMAPResponseStream {
                     checkLiteral();
                     return;
                 }
+                // a bare CR that is not part of a CRLF sequence.  The CR is part of
+                // the response data, so write it out, then reprocess the look-ahead
+                // byte on the next loop iteration (it might itself be a CR starting
+                // a real CRLF line terminator, so it must not be consumed here).
+                out.write(ch);
+                ch = next;
+                continue;
             }
             // write this to the buffer.
             out.write(ch);
+            ch = nextByte();
         }
     }
 
diff --git a/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/connection/IMAPResponseStreamTest.java b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/connection/IMAPResponseStreamTest.java
new file mode 100644
index 0000000..a1ae347
--- /dev/null
+++ b/geronimo-mail_2.1_impl/geronimo-mail_2.1_provider/src/test/java/org/apache/geronimo/mail/store/imap/connection/IMAPResponseStreamTest.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.connection;
+
+import java.io.ByteArrayInputStream;
+import java.io.UnsupportedEncodingException;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for the low level response line reader.  Mainly a regression
+ * test for the resp-cr-drop defect: a bare CR followed by a non-LF byte used
+ * to silently drop the look-ahead byte, corrupting the stream (and a CR CR LF
+ * sequence could prevent line termination entirely).
+ */
+public class IMAPResponseStreamTest {
+
+    private IMAPResponseStream streamFor(final String data) throws UnsupportedEncodingException {
+        return new IMAPResponseStream(new ByteArrayInputStream(data.getBytes("ISO8859-1")));
+    }
+
+    private byte[] bytes(final String data) throws UnsupportedEncodingException {
+        return data.getBytes("ISO8859-1");
+    }
+
+    @Test
+    public void testPlainLineIsReadWithoutTerminator() throws Exception {
+        final IMAPResponseStream stream = streamFor("* OK ready\r\n");
+        assertArrayEquals(bytes("* OK ready"), stream.readData());
+    }
+
+    @Test
+    public void testBareCarriageReturnDropsNoBytes() throws Exception {
+        // regression: the byte after a bare CR was silently discarded
+        final IMAPResponseStream stream = streamFor("a\rZb\r\n");
+        assertArrayEquals(bytes("a\rZb"), stream.readData());
+    }
+
+    @Test
+    public void testCarriageReturnBeforeLineTerminatorIsKept() throws Exception {
+        // regression: CR CR LF used to leave the line unterminated because the
+        // second CR was consumed as look-ahead and never re-examined
+        final IMAPResponseStream stream = streamFor("a\r\r\nnext\r\n");
+        assertArrayEquals(bytes("a\r"), stream.readData());
+        assertArrayEquals(bytes("next"), stream.readData());
+    }
+
+    @Test
+    public void testConsecutiveBareCarriageReturns() throws Exception {
+        final IMAPResponseStream stream = streamFor("a\r\rb\r\n");
+        assertArrayEquals(bytes("a\r\rb"), stream.readData());
+    }
+
+    @Test
+    public void testLiteralProcessingStillWorks() throws Exception {
+        // a literal marker at the end of the line continues onto the next line;
+        // the literal content may contain CR and LF bytes that must be kept verbatim
+        final IMAPResponseStream stream = streamFor("* 1 FETCH (BODY[] {6}\r\nX\rY\nZW)\r\n");
+        assertArrayEquals(bytes("* 1 FETCH (BODY[] {6}\r\nX\rY\nZW)"), stream.readData());
+    }
+
+    @Test
+    public void testMultipleLinesAreSplitCorrectly() throws Exception {
+        final IMAPResponseStream stream = streamFor("first\r\nsecond\r\nthird\r\n");
+        assertArrayEquals(bytes("first"), stream.readData());
+        assertArrayEquals(bytes("second"), stream.readData());
+        assertArrayEquals(bytes("third"), stream.readData());
+    }
+
+    @Test
+    public void testReadResponseParsesTaggedResponse() throws Exception {
+        final IMAPResponseStream stream = streamFor("a1 OK LOGIN completed.\r\n");
+        final IMAPResponse response = stream.readResponse();
+        final IMAPTaggedResponse tagged = (IMAPTaggedResponse) response;
+        assertTrue(tagged.isOK());
+    }
+}