This is an automated email from the ASF dual-hosted git repository.

hqtran pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git


The following commit(s) were added to refs/heads/master by this push:
     new 9da46f92ac [FIX] jakarta.mail.internet.ParseException: Unbalanced 
quoted string (#3002)
9da46f92ac is described below

commit 9da46f92acdef837aab8231e875d58b6098d5ffc
Author: Benoit TELLIER <[email protected]>
AuthorDate: Mon Apr 13 05:14:58 2026 +0200

    [FIX] jakarta.mail.internet.ParseException: Unbalanced quoted string (#3002)
---
 .../james/server/core/MimeMessageWrapper.java      |  15 ++-
 .../org/apache/james/server/core/MailImplTest.java |  27 +++++
 .../james/server/core/MimeMessageWrapperTest.java  | 109 +++++++++++++++++++++
 3 files changed, 146 insertions(+), 5 deletions(-)

diff --git 
a/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageWrapper.java
 
b/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageWrapper.java
index 091959ebce..2e6f2e64bc 100644
--- 
a/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageWrapper.java
+++ 
b/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageWrapper.java
@@ -65,7 +65,6 @@ public class MimeMessageWrapper extends MimeMessage 
implements Disposable {
      */
     public static final String USE_MEMORY_COPY = "james.message.usememorycopy";
     private static final int UNKNOWN = -1;
-    private static final int HEADER_BODY_SEPARATOR_SIZE = 2;
 
     /**
      * Can provide an input stream to the data
@@ -332,10 +331,16 @@ public class MimeMessageWrapper extends MimeMessage 
implements Disposable {
                 if (!isHeaderModified()) {
                     myHeaders = parsedHeaders;
                 } else {
-                    // The headers was modified so we need to call 
saveChanges() just to be sure
-                    // See JAMES-1320
-                    if (!saved) {
-                        saveChanges();
+                    // Headers were modified via setHeader/addHeader, which 
already updated
+                    // this.headers (checkModifyHeaders ensures it is loaded 
from source first).
+                    // Calling saveChanges() here would trigger 
updateHeaders() on every MIME
+                    // body part, which re-parses Content-Type parameters and 
fails on malformed
+                    // MIME structures (e.g. unbalanced quoted strings). Since 
we are in the fast
+                    // path (body is unmodified), the body-derived headers 
have not changed and
+                    // saveChanges() is not needed to reflect them.
+                    // See JAMES-1320 (checkModifyHeaders already guarantees 
headers != null)
+                    if (headers == null) {
+                        loadHeaders();
                     }
                     myHeaders = headers;
                 }
diff --git 
a/server/container/core/src/test/java/org/apache/james/server/core/MailImplTest.java
 
b/server/container/core/src/test/java/org/apache/james/server/core/MailImplTest.java
index bb78345026..641b39e19c 100644
--- 
a/server/container/core/src/test/java/org/apache/james/server/core/MailImplTest.java
+++ 
b/server/container/core/src/test/java/org/apache/james/server/core/MailImplTest.java
@@ -246,6 +246,33 @@ public class MailImplTest extends ContractMailTest {
             .contains("abc: def");
     }
 
+    @Test
+    void duplicateShouldNotThrowOnMalformedMimeWithModifiedHeaders() throws 
Exception {
+        // Reproduces the spooler reprocessing loop: a mail with malformed 
MIME (unbalanced quoted
+        // string in a body-part Content-Type) that also has a pre-modified 
header (e.g. Received
+        // added by the SMTP handler). Before the fix, MailImpl.duplicate() 
would call saveChanges()
+        // on the original MimeMessageWrapper which triggered MIME body-part 
parsing and threw
+        // ParseException: Unbalanced quoted string.
+        MailImpl mail = MailImpl.builder()
+            .name(MailUtil.newId())
+            .sender("sender@localhost")
+            .addRecipients("recipient@localhost")
+            .mimeMessage(new 
MimeMessageWrapper(MimeMessageInputStreamSource.create("test",
+                
ClassLoaderUtils.getSystemResourceAsSharedStream("invalid.eml"))))
+            .build();
+
+        // Simulate the Received header added by the SMTP handler (sets 
headersModified = true)
+        mail.getMessage().addHeader("Received", "from mx.example.com by 
james.example.com");
+
+        // Must not throw ParseException
+        MailImpl duplicate = MailImpl.duplicate(mail);
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        duplicate.getMessage().writeTo(out);
+        assertThat(new String(out.toByteArray(), StandardCharsets.US_ASCII))
+            .contains("Received: from mx.example.com by james.example.com");
+    }
+
     @Test
     void setAttributeShouldThrowOnNullAttributeName() {
         MailImpl mail = newMail();
diff --git 
a/server/container/core/src/test/java/org/apache/james/server/core/MimeMessageWrapperTest.java
 
b/server/container/core/src/test/java/org/apache/james/server/core/MimeMessageWrapperTest.java
index bc96f14fe9..7bba53b35d 100644
--- 
a/server/container/core/src/test/java/org/apache/james/server/core/MimeMessageWrapperTest.java
+++ 
b/server/container/core/src/test/java/org/apache/james/server/core/MimeMessageWrapperTest.java
@@ -404,4 +404,113 @@ public class MimeMessageWrapperTest extends 
MimeMessageFromStreamTest {
             IOUtils.consume(wrapper.getMessageInputStream()));
         LifecycleUtil.dispose(wrapper);
     }
+
+    @Test
+    void writeToShouldNotThrowOnMalformedMimeWhenHeadersModified() throws 
Exception {
+        // A multipart message with an unbalanced quoted string in a body 
part's Content-Type
+        // name parameter — the kind of malformed MIME that causes 
ParseException in saveChanges().
+        String malformedMime = "MIME-Version: 1.0\r\n" +
+            "From: [email protected]\r\n" +
+            "To: [email protected]\r\n" +
+            "Subject: test\r\n" +
+            "Content-Type: multipart/mixed; boundary=\"boundary\"\r\n" +
+            "\r\n" +
+            "--boundary\r\n" +
+            "Content-Type: image/png;\r\n" +
+            "    name=\"Outlook-Icon\r\n" +
+            "\r\n" +
+            "Desc.png\"\r\n" +
+            "Content-Transfer-Encoding: base64\r\n" +
+            "\r\n" +
+            "dGVzdA==\r\n" +
+            "--boundary--\r\n";
+
+        MimeMessageWrapper wrapper = new MimeMessageWrapper(
+            MimeMessageInputStreamSource.create("test", new 
SharedByteArrayInputStream(malformedMime.getBytes())));
+
+        // Simulate what James does: add a Received header before duplicating 
the mail.
+        // This sets headersModified = true, causing writeTo() to previously 
call saveChanges()
+        // which would fail with ParseException on the malformed Content-Type.
+        wrapper.addHeader("Received", "from mx.example.com");
+
+        // writeTo() must not throw even though the MIME body part has a 
malformed Content-Type
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        wrapper.writeTo(out);
+
+        assertThat(out.toString()).contains("Received: from mx.example.com");
+        LifecycleUtil.dispose(wrapper);
+    }
+
+    @Test
+    void writeToFastPathShouldReflectAddedHeaderWithoutExplicitSaveChanges() 
throws Exception {
+        mw.addHeader("X-James-Test", "added-value");
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        mw.writeTo(out);
+
+        assertThat(out.toString()).contains("X-James-Test: added-value");
+    }
+
+    @Test
+    void writeToFastPathShouldReflectSetHeader() throws Exception {
+        // Subject is already set in setUp() as "foo"; replace it
+        mw.setHeader("Subject", "replaced-subject");
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        mw.writeTo(out);
+
+        String result = out.toString();
+        assertThat(result).contains("Subject: replaced-subject");
+        assertThat(result).doesNotContain("Subject: foo");
+    }
+
+    @Test
+    void writeToFastPathShouldReflectRemovedHeader() throws Exception {
+        // Subject header is present in the original message
+        assertThat(mw.getHeader("Subject")).isNotNull();
+
+        mw.removeHeader("Subject");
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        mw.writeTo(out);
+
+        assertThat(out.toString()).doesNotContain("Subject:");
+    }
+
+    @Test
+    void writeToFastPathShouldPreserveBodyWhenHeadersModified() throws 
Exception {
+        mw.addHeader("X-James-Test", "some-value");
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        mw.writeTo(out);
+
+        // Body must be preserved exactly
+        assertThat(out.toString()).endsWith(body);
+    }
+
+    @Test
+    void writeToFastPathShouldPreserveUnmodifiedHeaders() throws Exception {
+        // Only add a new header; the original "Subject: foo" must survive
+        mw.addHeader("X-James-Extra", "extra");
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        mw.writeTo(out);
+
+        String result = out.toString();
+        assertThat(result).contains("Subject: foo");
+        assertThat(result).contains("X-James-Extra: extra");
+    }
+
+    @Test
+    void writeToFastPathShouldReflectMultipleAddedHeadersForSameName() throws 
Exception {
+        mw.addHeader("Received", "from relay1.example.com");
+        mw.addHeader("Received", "from relay2.example.com");
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        mw.writeTo(out);
+
+        String result = out.toString();
+        assertThat(result).contains("Received: from relay1.example.com");
+        assertThat(result).contains("Received: from relay2.example.com");
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to