From 117d9d9fbb83ec67bb3e436bdc7f4bef9eb65567 Mon Sep 17 00:00:00 2001 From: Nathan Le Date: Wed, 23 Sep 2026 15:40:48 -0700 Subject: [PATCH 1/3] Fix corrupted Content-Disposition header for long, non-ASCII attachment file names JakartaMailFlowableMailClient.createMultiPartContent() was manually RFC-2047-encoding attachment file names with MimeUtility.encodeText() before handing them to MimeBodyPart.setFileName(). When the resulting encoded-word string was also too long for one header line, Jakarta Mail's own RFC 2231 parameter continuation splitting kicked in on top of it, splitting mid-encoded-word and corrupting the header. Pass the raw file name to setFileName() instead and let Jakarta Mail's own RFC 2231 encoding/folding handle it in a single consistent pass. Co-Authored-By: Claude Sonnet 5 --- .../mail/JakartaMailFlowableMailClient.java | 9 ++- .../JakartaMailFlowableMailClientTest.java | 55 +++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 modules/flowable-mail/src/test/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClientTest.java diff --git a/modules/flowable-mail/src/main/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClient.java b/modules/flowable-mail/src/main/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClient.java index dc19341f3b7..ec820492e87 100644 --- a/modules/flowable-mail/src/main/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClient.java +++ b/modules/flowable-mail/src/main/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClient.java @@ -324,11 +324,10 @@ protected MimeMultipart createMultiPartContent(String text, String html, String for (DataSource attachment : attachments) { BodyPart bodyPart = new MimeBodyPart(); bodyPart.setDisposition(Part.ATTACHMENT); - try { - bodyPart.setFileName(MimeUtility.encodeText(attachment.getName(), charset, null)); - } catch (UnsupportedEncodingException e) { - throw new FlowableMailException("Could not encode attachment file name", e); - } + // Do not pre-encode the file name with MimeUtility.encodeText(): setFileName() sets a MIME parameter, + // and Jakarta Mail's own ParameterList already RFC 2231 encodes it, including continuation folding + // for long values. Doing both corrupts the header for names that are long and non-ASCII. + bodyPart.setFileName(attachment.getName()); bodyPart.setDataHandler(new DataHandler(attachment)); rootContainer.addBodyPart(bodyPart); } diff --git a/modules/flowable-mail/src/test/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClientTest.java b/modules/flowable-mail/src/test/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClientTest.java new file mode 100644 index 00000000000..163d52854b2 --- /dev/null +++ b/modules/flowable-mail/src/test/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClientTest.java @@ -0,0 +1,55 @@ +/* Licensed 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.flowable.mail.common.impl.jakarta.mail; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import jakarta.mail.BodyPart; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.util.ByteArrayDataSource; + +import org.junit.jupiter.api.Test; + +class JakartaMailFlowableMailClientTest { + + protected final JakartaMailFlowableMailClient client = new JakartaMailFlowableMailClient(null, null); + + @Test + void attachmentFileNameThatIsLongAndNonAsciiIsNotCorrupted() throws MessagingException { + String fileName = "Précis of Evidence VA23-5-1034 Lorge Chocolatier PN 1136882 Retail Henry Street Kenmare Co.pdf"; + + ByteArrayDataSource attachment = new ByteArrayDataSource("attachment content".getBytes(), "application/pdf"); + attachment.setName(fileName); + + MimeMultipart multipart = client.createMultiPartContent(null, null, "UTF-8", List.of(attachment)); + + BodyPart attachmentPart = multipart.getBodyPart(0); + assertThat(attachmentPart.getFileName()).isEqualTo(fileName); + } + + @Test + void attachmentFileNameThatIsShortAndAsciiIsUnaffected() throws MessagingException { + String fileName = "invoice.pdf"; + ByteArrayDataSource attachment = new ByteArrayDataSource("attachment content".getBytes(), "application/pdf"); + attachment.setName(fileName); + + MimeMultipart multipart = client.createMultiPartContent(null, null, "UTF-8", List.of(attachment)); + + BodyPart attachmentPart = multipart.getBodyPart(0); + assertThat(attachmentPart.getFileName()).isEqualTo(fileName); + } + +} From d637ba5163924897319cf74f958a473561bedfc9 Mon Sep 17 00:00:00 2001 From: Nathan Le Date: Thu, 24 Sep 2026 10:12:20 -0700 Subject: [PATCH 2/3] Encode attachment file names with the configured mail charset, not the JVM default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix removed the manual RFC-2047 pre-encoding that corrupted the Content-Disposition header, but MimeBodyPart.setFileName() ignores the charset argument passed to createMultiPartContent() entirely: its default code path RFC 2231-encodes with MimeUtility.getDefaultMIMECharset(), which resolves to the mail.mime.charset system property or, failing that, the JVM's file.encoding — neither of which is the charset Flowable computed for the rest of the message (subject, headers, text/html body). Build the Content-Disposition header directly via ParameterList/ ContentDisposition instead, passing the charset through explicitly so the attachment file name is encoded consistently with the rest of the message regardless of JVM/system defaults. Co-Authored-By: Claude Sonnet 5 --- .../mail/JakartaMailFlowableMailClient.java | 23 +++++++++++++++---- .../JakartaMailFlowableMailClientTest.java | 18 +++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/modules/flowable-mail/src/main/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClient.java b/modules/flowable-mail/src/main/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClient.java index ec820492e87..0dd9ba65613 100644 --- a/modules/flowable-mail/src/main/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClient.java +++ b/modules/flowable-mail/src/main/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClient.java @@ -15,6 +15,7 @@ import java.io.UnsupportedEncodingException; import java.net.IDN; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Collection; import java.util.Date; @@ -36,11 +37,13 @@ import jakarta.mail.Session; import jakarta.mail.Transport; import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.ContentDisposition; import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.MimeBodyPart; import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMultipart; import jakarta.mail.internet.MimeUtility; +import jakarta.mail.internet.ParameterList; import org.apache.commons.lang3.StringUtils; import org.flowable.common.engine.api.FlowableException; @@ -323,11 +326,7 @@ protected MimeMultipart createMultiPartContent(String text, String html, String if (attachmentsExists) { for (DataSource attachment : attachments) { BodyPart bodyPart = new MimeBodyPart(); - bodyPart.setDisposition(Part.ATTACHMENT); - // Do not pre-encode the file name with MimeUtility.encodeText(): setFileName() sets a MIME parameter, - // and Jakarta Mail's own ParameterList already RFC 2231 encodes it, including continuation folding - // for long values. Doing both corrupts the header for names that are long and non-ASCII. - bodyPart.setFileName(attachment.getName()); + setAttachmentFileName(bodyPart, attachment.getName(), charset); bodyPart.setDataHandler(new DataHandler(attachment)); rootContainer.addBodyPart(bodyPart); } @@ -336,6 +335,20 @@ protected MimeMultipart createMultiPartContent(String text, String html, String return rootContainer; } + protected void setAttachmentFileName(BodyPart bodyPart, String fileName, String charset) throws MessagingException { + // Build the Content-Disposition header ourselves instead of using BodyPart.setFileName(): that method + // ignores the charset argument entirely and RFC 2231 encodes with MimeUtility.getDefaultMIMECharset() + // (the JVM's file.encoding, unless mail.mime.charset is set), so it silently diverges from the charset + // used for the rest of the message. ParameterList.set(name, value, charset) does the same RFC 2231 + // encoding and continuation folding, but with the charset we actually pass it, and leaves ASCII names + // untouched. + String mimeCharset = charset != null ? charset : StandardCharsets.UTF_8.name(); + ParameterList parameters = new ParameterList(); + parameters.set("filename", fileName, mimeCharset); + ContentDisposition disposition = new ContentDisposition(Part.ATTACHMENT, parameters); + bodyPart.setHeader("Content-Disposition", disposition.toString()); + } + protected Session createSession() { if (serverConfiguration instanceof MailJndiServerConfiguration jndiServerConfiguration) { return createSession(jndiServerConfiguration); diff --git a/modules/flowable-mail/src/test/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClientTest.java b/modules/flowable-mail/src/test/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClientTest.java index 163d52854b2..6faa117483a 100644 --- a/modules/flowable-mail/src/test/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClientTest.java +++ b/modules/flowable-mail/src/test/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClientTest.java @@ -52,4 +52,22 @@ void attachmentFileNameThatIsShortAndAsciiIsUnaffected() throws MessagingExcepti assertThat(attachmentPart.getFileName()).isEqualTo(fileName); } + @Test + void attachmentFileNameIsEncodedWithTheGivenCharsetRatherThanThePlatformDefault() throws MessagingException { + String fileName = "café.pdf"; + ByteArrayDataSource attachment = new ByteArrayDataSource("attachment content".getBytes(), "application/pdf"); + attachment.setName(fileName); + + MimeMultipart multipart = client.createMultiPartContent(null, null, "ISO-8859-1", List.of(attachment)); + + BodyPart attachmentPart = multipart.getBodyPart(0); + String contentDisposition = attachmentPart.getHeader("Content-Disposition")[0]; + + // 'é' is a single byte (0xE9) in ISO-8859-1 but two bytes (0xC3 0xA9) in UTF-8: this string can only + // appear if the ISO-8859-1 charset passed to createMultiPartContent() was actually used to encode it, + // rather than UTF-8 or the JVM's platform default charset. + assertThat(contentDisposition).contains("ISO-8859-1''caf%E9.pdf"); + assertThat(attachmentPart.getFileName()).isEqualTo(fileName); + } + } From 051449fa97b6f1258b09f5fda8876cf093e6a09e Mon Sep 17 00:00:00 2001 From: Nathan Le Date: Thu, 24 Sep 2026 11:47:53 -0700 Subject: [PATCH 3/3] Add engine-level regression tests and fix null-charset fallback for attachment file names - Add two real integration tests to EmailServiceTaskTest (mail service task, real SMTP mock server, real MimeMessage on the wire) covering the long non-ASCII attachment file name corruption and the charset-fidelity fix, following the module's convention of testing through the engine rather than by calling internal methods directly. - When no charset is configured for the message, setAttachmentFileName() was falling back to a hardcoded UTF-8, while the text/html body parts (MimeBodyPart.setText() with a null charset) fall back to MimeUtility.getDefaultMIMECharset(). On a JVM whose default charset isn't UTF-8, this could reintroduce a charset mismatch between the attachment file name and the rest of the message. Use the same default the body already falls back to instead. Co-Authored-By: Claude Sonnet 5 --- .../test/bpmn/mail/EmailServiceTaskTest.java | 52 +++++++++++++++++++ .../mail/JakartaMailFlowableMailClient.java | 6 +-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/test/bpmn/mail/EmailServiceTaskTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/test/bpmn/mail/EmailServiceTaskTest.java index 2f772654336..a10d2b020f5 100644 --- a/modules/flowable-engine/src/test/java/org/flowable/engine/test/bpmn/mail/EmailServiceTaskTest.java +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/test/bpmn/mail/EmailServiceTaskTest.java @@ -502,6 +502,58 @@ public void testTextMailWithDataSourceAttachment() throws Exception { assertThat(attachmentFileName).isEqualTo(fileName); } + @Test + @Deployment(resources = "org/flowable/engine/test/bpmn/mail/EmailServiceTaskTest.testTextMailWithDataSourceAttachment.bpmn20.xml") + public void testTextMailWithDataSourceAttachmentLongNonAsciiFileName() throws Exception { + String fileName = "Précis of Evidence VA23-5-1034 Lorge Chocolatier PN 1136882 Retail Henry Street Kenmare Co.pdf"; + String fileContent = "This is the file content"; + HashMap vars = new HashMap<>(); + vars.put("attachmentsBean", new AttachmentsBean()); + vars.put("fileContent", fileContent); + vars.put("fileName", fileName); + runtimeService.startProcessInstanceByKey("textMailWithDataSourceAttachment", vars); + + List messages = wiser.getMessages(); + assertThat(messages).hasSize(1); + WiserMessage message = messages.get(0); + MimeMultipart mm = (MimeMultipart) message.getMimeMessage().getContent(); + assertThat(mm.getCount()).isEqualTo(2); + String attachmentFileName = mm.getBodyPart(1).getDataHandler().getName(); + assertThat(attachmentFileName).isEqualTo(fileName); + } + + @Test + @Deployment(resources = "org/flowable/engine/test/bpmn/mail/EmailServiceTaskTest.testTextMailWithDataSourceAttachment.bpmn20.xml") + public void testTextMailWithDataSourceAttachmentUsesConfiguredCharset() throws Exception { + Charset originalCharset = processEngineConfiguration.getMailServerDefaultCharset(); + + try { + processEngineConfiguration.setMailServerDefaultCharset(StandardCharsets.ISO_8859_1); + reinitilizeMailClients(); + + String fileName = "café.pdf"; + String fileContent = "This is the file content"; + HashMap vars = new HashMap<>(); + vars.put("attachmentsBean", new AttachmentsBean()); + vars.put("fileContent", fileContent); + vars.put("fileName", fileName); + runtimeService.startProcessInstanceByKey("textMailWithDataSourceAttachment", vars); + + List messages = wiser.getMessages(); + assertThat(messages).hasSize(1); + WiserMessage message = messages.get(0); + MimeMultipart mm = (MimeMultipart) message.getMimeMessage().getContent(); + String contentDisposition = mm.getBodyPart(1).getHeader("Content-Disposition")[0]; + + // 'é' is a single byte (0xE9) in ISO-8859-1 but two bytes (0xC3 0xA9) in UTF-8: this string can only + // appear if the configured ISO-8859-1 charset was actually used to encode the attachment file name. + assertThat(contentDisposition).contains("ISO-8859-1''caf%E9.pdf"); + assertThat(mm.getBodyPart(1).getDataHandler().getName()).isEqualTo(fileName); + } finally { + processEngineConfiguration.setMailServerDefaultCharset(originalCharset); + } + } + @Test @Deployment public void testTextMailWithNotExistingFileAttachment() throws Exception { diff --git a/modules/flowable-mail/src/main/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClient.java b/modules/flowable-mail/src/main/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClient.java index 0dd9ba65613..9b73277bef9 100644 --- a/modules/flowable-mail/src/main/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClient.java +++ b/modules/flowable-mail/src/main/java/org/flowable/mail/common/impl/jakarta/mail/JakartaMailFlowableMailClient.java @@ -15,7 +15,6 @@ import java.io.UnsupportedEncodingException; import java.net.IDN; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Collection; import java.util.Date; @@ -341,8 +340,9 @@ protected void setAttachmentFileName(BodyPart bodyPart, String fileName, String // (the JVM's file.encoding, unless mail.mime.charset is set), so it silently diverges from the charset // used for the rest of the message. ParameterList.set(name, value, charset) does the same RFC 2231 // encoding and continuation folding, but with the charset we actually pass it, and leaves ASCII names - // untouched. - String mimeCharset = charset != null ? charset : StandardCharsets.UTF_8.name(); + // untouched. When no charset is configured, fall back to the same default MimeBodyPart.setText() falls + // back to for the message body, so the file name and body stay consistent with each other. + String mimeCharset = charset != null ? charset : MimeUtility.mimeCharset(MimeUtility.getDefaultJavaCharset()); ParameterList parameters = new ParameterList(); parameters.set("filename", fileName, mimeCharset); ContentDisposition disposition = new ContentDisposition(Part.ATTACHMENT, parameters);