Skip to content

Commit 7616341

Browse files
authored
Merge pull request #102 from DirectoryTree/eml-attachments
Prevent merging `.eml` file attachments
2 parents fb187fb + 7bb8390 commit 7616341

File tree

2 files changed

+88
-8
lines changed

2 files changed

+88
-8
lines changed

src/HasParsedMessage.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use ZBateson\MailMimeParser\Header\Part\ContainerPart;
1414
use ZBateson\MailMimeParser\Header\Part\NameValuePart;
1515
use ZBateson\MailMimeParser\Message as MailMimeMessage;
16+
use ZBateson\MailMimeParser\Message\IMessagePart;
1617

1718
trait HasParsedMessage
1819
{
@@ -121,10 +122,7 @@ public function attachments(): array
121122
$attachments = [];
122123

123124
foreach ($this->parse()->getAllAttachmentParts() as $part) {
124-
// If the attachment's content type is message/rfc822, we're
125-
// working with a forwarded message. We will parse the
126-
// forwarded message and merge in its attachments.
127-
if (strtolower($part->getContentType()) === 'message/rfc822') {
125+
if ($this->isForwardedMessage($part)) {
128126
$message = new FileMessage($part->getContent());
129127

130128
$attachments = array_merge($attachments, $message->attachments());
@@ -157,6 +155,16 @@ public function attachmentCount(): int
157155
return $this->parse()->getAttachmentCount();
158156
}
159157

158+
/**
159+
* Determine if the attachment should be treated as an embedded forwarded message.
160+
*/
161+
protected function isForwardedMessage(IMessagePart $part): bool
162+
{
163+
return empty($part->getFilename())
164+
&& strtolower((string) $part->getContentType()) === 'message/rfc822'
165+
&& strtolower((string) $part->getContentDisposition()) !== 'attachment';
166+
}
167+
160168
/**
161169
* Get addresses from the given header.
162170
*

tests/Unit/FileMessageTest.php

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@
217217
expect($attachments[0]->filename())->toBe('inline_image.png');
218218
});
219219

220-
test('it can extract attachments from forwarded messages', function () {
220+
test('it merges attachments from an inline forwarded message', function () {
221221
// Create a forwarded message that contains an attachment
222222
$forwardedMessage = <<<'EOT'
223223
From: "Original Sender" <[email protected]>
@@ -243,7 +243,7 @@
243243
--ORIGINAL_BOUNDARY--
244244
EOT;
245245

246-
// Create the main message that forwards the above message
246+
// Create the main message that forwards the above message inline (no filename/disposition on message/rfc822)
247247
$contents = <<<EOT
248248
From: "Forwarder" <[email protected]>
249249
To: "Final Recipient" <[email protected]>
@@ -259,8 +259,7 @@
259259
Here is the forwarded message with its attachment.
260260
261261
--FORWARD_BOUNDARY
262-
Content-Type: message/rfc822; name="forwarded-message.eml"
263-
Content-Disposition: attachment; filename="forwarded-message.eml"
262+
Content-Type: message/rfc822
264263
265264
$forwardedMessage
266265
--FORWARD_BOUNDARY
@@ -290,6 +289,79 @@
290289
expect($attachments[1]->contentType())->toBe('application/zip');
291290
});
292291

292+
test('it does not merge attached .eml files', function () {
293+
// Forwarded message that itself contains an attachment
294+
$forwardedMessage = <<<'EOT'
295+
From: "Original Sender" <[email protected]>
296+
To: "Original Recipient" <[email protected]>
297+
Subject: Original Message with Attachment
298+
Date: Tue, 18 Feb 2025 10:00:00 -0500
299+
Message-ID: <[email protected]>
300+
MIME-Version: 1.0
301+
Content-Type: multipart/mixed; boundary="ORIGINAL_BOUNDARY"
302+
303+
--ORIGINAL_BOUNDARY
304+
Content-Type: text/plain; charset="UTF-8"
305+
306+
This is the original message with an attachment.
307+
308+
--ORIGINAL_BOUNDARY
309+
Content-Type: application/pdf; name="original-document.pdf"
310+
Content-Disposition: attachment; filename="original-document.pdf"
311+
Content-Transfer-Encoding: base64
312+
313+
JVBERi0xLjUKJeLjz9MKMyAwIG9iago8PC9MZW5ndGggNCAgIC9GaWx0ZXIvQXNjaWlIYXgg
314+
ICAgPj5zdHJlYW0Kc3R1ZmYKZW5kc3RyZWFtCmVuZG9iajAK
315+
--ORIGINAL_BOUNDARY--
316+
EOT;
317+
318+
// Top-level message that attaches the forwarded message as an .eml (should not merge)
319+
$contents = <<<EOT
320+
From: "Forwarder" <[email protected]>
321+
To: "Final Recipient" <[email protected]>
322+
Subject: Fwd: Original Message with Attachment (as .eml)
323+
Date: Wed, 19 Feb 2025 12:34:56 -0500
324+
Message-ID: <[email protected]>
325+
MIME-Version: 1.0
326+
Content-Type: multipart/mixed; boundary="FORWARD_BOUNDARY"
327+
328+
--FORWARD_BOUNDARY
329+
Content-Type: text/plain; charset="UTF-8"
330+
331+
Here is the forwarded message attached as an .eml file.
332+
333+
--FORWARD_BOUNDARY
334+
Content-Type: message/rfc822; name="forwarded-message.eml"
335+
Content-Disposition: attachment; filename="forwarded-message.eml"
336+
337+
$forwardedMessage
338+
--FORWARD_BOUNDARY
339+
Content-Type: application/zip; name="additional-file.zip"
340+
Content-Disposition: attachment; filename="additional-file.zip"
341+
Content-Transfer-Encoding: base64
342+
343+
UEsDBAoAAAAAAKxVVVMAAAAAAAAAAAAAAAAJAAAAdGVzdC50eHRQSwECFAAKAAAAAACs
344+
VVVTAAAAAAAAAAAAAAAACQAAAAAAAAAAAAAAAAAAAHRlc3QudHh0UEsFBgAAAAABAAEA
345+
NwAAAB8AAAAAAA==
346+
--FORWARD_BOUNDARY--
347+
EOT;
348+
349+
$message = new FileMessage($contents);
350+
351+
$attachments = $message->attachments();
352+
353+
// Expect exactly two top-level attachments: the attached .eml itself and the zip
354+
expect($attachments)->toHaveCount(2);
355+
356+
// .eml is preserved as-is (no merging of its inner PDF)
357+
expect($attachments[0]->contentType())->toBe('message/rfc822');
358+
expect($attachments[0]->filename())->toBe('forwarded-message.eml');
359+
360+
// The top-level zip attachment remains
361+
expect($attachments[1]->contentType())->toBe('application/zip');
362+
expect($attachments[1]->filename())->toBe('additional-file.zip');
363+
});
364+
293365
test('it can handle multiple levels of forwarded messages with attachments', function () {
294366
// Create the deepest nested message with an attachment
295367
$deepestMessage = <<<'EOT'

0 commit comments

Comments
 (0)