-
-
Notifications
You must be signed in to change notification settings - Fork 189
Description
Description
When a multipart/alternative (or any multipart container) has only one sub-part (e.g. only text/html without a text/plain counterpart), the body is lost and misclassified as an attachment. getHTMLBody()
returns "", getTextBody() returns "".
This is common with transactional/automated emails (government portals, notification systems, etc.) that only send HTML without a text/plain alternative.
Root cause
In Structure.php, method parsePart(), line 118:
if (($boundary = $headers->getBoundary()) !== null) {
$parts = $this->detectParts($boundary, $body, $part_number);
if(count($parts) > 1) { // <-- BUG: should be > 0
return $parts;
}
}
return [new Part($body, $this->header->getConfig(), $headers, $part_number)];
When detectParts() finds exactly 1 sub-part inside the multipart container, count($parts) > 1 evaluates to false. The method falls through and wraps the entire raw multipart body as a single Part with
subtype=alternative.
isAttachment() then returns true for this part because "alternative" is not in the ["plain", "html"] whitelist (Part.php line 295), so the HTML content is never added to $this->bodies['html'] — it becomes a
ghost attachment instead.
Steps to reproduce
- Receive an email with this MIME structure:
Content-Type: multipart/mixed; boundary="outer"
└── Content-Type: multipart/alternative; boundary="inner"
└── Content-Type: text/html; charset="utf-8" ← only 1 sub-part, no text/plain - Fetch with php-imap
- $message->getHTMLBody() returns "" (empty string)
- $message->getAttachments() contains the HTML body as a fake attachment
Suggested fix
Change > 1 to > 0 in Structure.php:118:
if(count($parts) > 0) {
return $parts;
}
If detectParts() returns 0 parts, the existing fallthrough behavior is preserved. If it returns 1 or more, they are correctly returned as parsed sub-parts.
Environment
- php-imap version: 6.2.0
- PHP version: 8.4.18
- IMAP server: Microsoft 365 (Outlook)