jakartaee / jakartaee/mail-api
MimeMultipart.parse() drops blank preamble lines, breaks S/MIME after saveChanges()
- Dominant language
- Java
- Stars
- 285
- Forks
- 109
- Avg merge
- 15h 19m
- Merged PRs (30d)
- 1
Description
**Describe the bug**
`MimeMultipart.parse()` discards blank lines between the header/content separator and the first boundary when parsing multipart body parts. This is a lossy operation: when the content is later serialized via `MimeMultipart.writeTo()`, those blank lines are not restored, resulting in byte-level content modification.
This breaks S/MIME signature verification, because the signature covers the exact bytes of the signed body part. After a `saveChanges()` + `writeTo()` cycle, the signed content loses 2 bytes (`\r\n`), the hash no longer matches the signature, and verification fails.
The root cause is in `MimeMultipart.parse()` ([MimeMultipart.java, line 651](https://github.com/jakartaee/mail-api/blob/master/api/src/main/java/jakarta/mail/internet/MimeMultipart.java#L651)):
```java
// save the preamble after skipping blank lines
if (line.length() > 0) {
if (preamblesb == null)
preamblesb = new StringBuilder(line.length() + 2);
preamblesb.append(line).append(System.lineSeparator());
}
```
The condition `if (line.length() > 0)` silently drops any empty lines that appear before the first boundary. These empty lines are valid per RFC 2046 (they fall in the preamble area) and are commonly produced by Microsoft Outlook / Exchange.
When `writeTo()` reconstructs the multipart, `preamble` is `null` (because nothing was saved), so the boundary is written immediately after the header/content separator — omitting the original blank line(s).
**To Reproduce**
1. Receive an S/MIME signed email from Microsoft Outlook / Exchange. The signed body part (`multipart/alternative`) contains an extra `\r\n` between the header/content separator and the first inner boundary. This is valid per RFC 2046:
```
Content-Type: multipart/alternative;
boundary="----=_NextPart_001_0024_01DC8EEA.EA4A1F90"
\r\n ← RFC 822 header/content separator
\r\n ← Extra blank line (valid preamble area per RFC 2046)
------=_NextPart_001_0024_01DC8EEA.EA4A1F90
Content-Type: text/plain; ...
```
2. Parse the email with `MimeMessage`:
```java
MimeMessage message = new MimeMessage(session, inputStream);
```
3. Call `saveChanges()` (e.g., after modifying headers):
```java
message.setHeader("X-Custom-Header", "value");
message.saveChanges();
```
4. Write the message back:
```java
ByteArrayOutputStream out = new ByteArrayOutputStream();
message.writeTo(out);
```
5. The signed body part content is now 2 bytes shorter. The extra `\r\n` before the first inner boundary has been removed:
```
Content-Type: multipart/alternative;
boundary="----=_NextPart_001_0024_01DC8EEA.EA4A1F90"
\r\n ← header/content separator
------=_NextPart_001_0024_01DC8EEA.EA4A1F90 ← blank line GONE
Content-Type: text/plain; ...
```
6. S/MIME signature verification now fails because the signed content bytes have changed.
**Expected behavior**
The `parse()` → `writeTo()` cycle should be byte-exact for the content of multipart body parts. Blank lines in the preamble area (before the first boundary) should be preserved, as they are part of the signed content in S/MIME messages.
After `saveChanges()` + `writeTo()`, the signed body part should retain the same bytes it had when originally parsed, and S/MIME signature verification should continue to succeed.
**Proposed fix**
In `MimeMultipart.parse()`, remove the `if (line.length() > 0)` guard so that blank lines are preserved as part of the preamble:
```java
// Current code (line ~651):
if (line.length() > 0) {
if (preamblesb == null)
preamblesb = new StringBuilder(line.length() + 2);
preamblesb.append(line).append(System.lineSeparator());
}
// Proposed fix:
if (preamblesb == null)
preamblesb = new StringBuilder(line.length() + 2);
preamblesb.append(line).append("\r\n");
```
Two changes:
1. **Remove `if (line.length() > 0)`** — preserve blank lines as preamble content instead of discarding them.
2. **Replace `System.lineSeparator()` with `"\r\n"`** — MIME requires CRLF line endings per RFC 2046. Using `System.lineSeparator()` produces `\n` on Linux, which would lose 1 byte per line when the preamble is written back via `writeTo()` using raw `los.write(pb)`.
**Desktop (please complete the following information):**
- OS: Linux (Ubuntu), also reproducible on any OS
- Java version: 17+
- Jakarta Mail API: 2.1.3 (also verified in 2.1.5)
- Angus Mail: 2.0.3
**Mail server:**
- Protocol being used: SMTP (sending side), IMAP (receiving side)
- Vendor/product: Microsoft Exchange / Outlook (Microsoft 365)
- The issue is not server-specific; it affects any S/MIME signed email that contains blank lines before the first boundary in a multipart body part.
**Additional context**
- The email structure with an extra blank line before the first boundary is valid per **RFC 2046 Section 5.1.1**, which defines the preamble area as "the area between the header and the first boundary". Any content there (including blank lines) is valid.
- Microsoft Outlook / Exchange commonly produces this structure in S/MIME signed emails.
- The `writeTo()` method in `MimeMultipart` already handles a non-null `preamble` correctly — it writes the bytes as-is and checks for trailing newline. The only issue is that `parse()` never stores blank-line-only preambles.
- The `System.lineSeparator()` issue is a secondary pre-existing bug that becomes relevant only with this fix. Currently it is inert because blank lines never reach the preamble storage path.
- This issue makes it impossible to verify S/MIME signatures after calling `saveChanges()`, which is required in many mail processing platforms before sending.
Contributor guide
Research direction
Start in api/src/main/java/jakarta/mail/internet/MimeMultipart.java around line 651 and trace how parse() stores the preamble before writeTo() serializes it. Reproduce the parse → saveChanges() → writeTo() flow with a multipart message containing a blank preamble line. Done means the preamble bytes, including CRLF line endings, are preserved and S/MIME signature verification continues to succeed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- api
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100