Skip to content

Commit e7d10b0

Browse files
committed
Add utility functions for retrieving message content and attachments
You can iterate over all of the message parts in the calling code and extract the plain text and HTML body, as well as the attachments. However, I thought that this would be something that people would do regularly; so much so such that adding utility functions for doing so might be a great productivity win for the package. The documentation's been updated to reflect these additions so that they're as visible as possible, and readily discoverable. Signed-off-by: Matthew Setter <matthew@matthewsetter.com>
1 parent 33f94d0 commit e7d10b0

5 files changed

Lines changed: 118 additions & 3 deletions

File tree

docs/book/message/attachments.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,5 +147,22 @@ use Laminas\Mime\Mime;
147147
$mimeMessage->setMime(new Mime($customBoundary));
148148
```
149149

150+
## Retrieving attachments
151+
152+
If you have created a multipart message with one or more attachments, whether programmatically
153+
or via the `Message::fromString();` method, you can readily retrieve them by calling the `getAttachments()` method.
154+
It will return an array of `\Laminas\Mime\Part` objects.
155+
156+
For example:
157+
158+
```php
159+
// Instantiate a Message object from a .eml file.
160+
$raw = file_get_contents(__DIR__ . '/mail_with_attachments.eml');
161+
$message = Message::fromString($raw);
162+
163+
// Retrieve the email's attachments.
164+
$attachments = $message->getAttachments();
165+
```
166+
150167
[mime-boundary]: https://www.oreilly.com/library/view/programming-internet-email/9780596802585/ch03s04.html
151168
[multipart-content-type]: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html

docs/book/message/intro.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,21 @@ EOF;
207207
$message = Message::fromString($rawEmail);
208208
```
209209

210+
### Retrieve a Message's plain text and HTML body
211+
212+
Commonly, though not always, an email will contain one or both of a plain text and/or HTMl body.
213+
To retrieve these directly, there are two methods available `getPlainTextBodyPart()` and
214+
`getHtmlBodyPart()`. For example:
215+
216+
```php
217+
// Instantiate a Message object from a .eml file.
218+
$raw = file_get_contents(__DIR__ . '/mail_with_attachments.eml');
219+
$message = Message::fromString($raw);
220+
221+
echo $message->getPlainTextBodyPart();
222+
echo $message->getHtmlBodyPart();
223+
```
224+
210225
## Configuration Options
211226

212227
The `Message` class has no configuration options, and is instead a value object.

src/Message.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,23 @@
1313
use Laminas\Mail\Header\ReplyTo;
1414
use Laminas\Mail\Header\Sender;
1515
use Laminas\Mail\Header\To;
16+
use Laminas\Mail\Iterator\AttachmentPartFilterIterator;
17+
use Laminas\Mail\Iterator\MessagePartFilterIterator;
18+
use Laminas\Mail\Iterator\PartsIterator;
1619
use Laminas\Mime;
20+
use Laminas\Mime\Part;
21+
use RecursiveIteratorIterator;
1722
use Traversable;
1823

1924
use function array_filter;
25+
use function array_pop;
2026
use function count;
2127
use function date;
2228
use function gettype;
2329
use function is_array;
2430
use function is_object;
2531
use function is_string;
32+
use function iterator_to_array;
2633
use function method_exists;
2734
use function sprintf;
2835
use function str_starts_with;
@@ -118,6 +125,46 @@ public function getHeaders()
118125
return $this->headers;
119126
}
120127

128+
public function getBodyPart(string $partType): Part
129+
{
130+
/** @var Part[] $iterator */
131+
$iterator = new RecursiveIteratorIterator(
132+
new MessagePartFilterIterator(
133+
new PartsIterator($this->getBody()->getParts()),
134+
$partType
135+
)
136+
);
137+
138+
$part = iterator_to_array($iterator);
139+
return array_pop($part);
140+
}
141+
142+
public function getPlainTextBodyPart(): Part
143+
{
144+
return $this->getBodyPart(\Laminas\Mime\Mime::TYPE_TEXT);
145+
}
146+
147+
public function getHtmlBodyPart(): Part
148+
{
149+
return $this->getBodyPart(\Laminas\Mime\Mime::TYPE_HTML);
150+
}
151+
152+
/**
153+
* @return Part[]
154+
*/
155+
public function getAttachments(): array
156+
{
157+
/** @var Part[] $iterator */
158+
$iterator = new RecursiveIteratorIterator(
159+
new AttachmentPartFilterIterator(
160+
new PartsIterator(
161+
$this->getBody()->getParts()
162+
),
163+
)
164+
);
165+
return iterator_to_array($iterator);
166+
}
167+
121168
/**
122169
* Set (overwrite) From addresses
123170
*

test/MessageTest.php

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
use Laminas\Mail\Message;
1313
use Laminas\Mime\Message as MimeMessage;
1414
use Laminas\Mime\Mime;
15+
use Laminas\Mime\Part;
1516
use Laminas\Mime\Part as MimePart;
1617
use PHPUnit\Framework\TestCase;
1718
use Smalot\PdfParser\Parser;
1819
use stdClass;
1920

21+
use function array_pop;
2022
use function count;
2123
use function date;
2224
use function fclose;
@@ -25,6 +27,7 @@
2527
use function fwrite;
2628
use function implode;
2729
use function sprintf;
30+
use function str_starts_with;
2831
use function substr;
2932
use function sys_get_temp_dir;
3033
use function trim;
@@ -888,12 +891,45 @@ public function testCanParseMultipartEmail(): void
888891
'<div>This is a test email with 1 attachment.</div>',
889892
trim($partOne->getParts()[1]->getRawContent())
890893
);
894+
}
895+
896+
public function testCanReturnPlainTextAndHTMLMessageBodyIfAvailable()
897+
{
898+
$raw = file_get_contents(__DIR__ . '/_files/mail_with_pdf_attachment.eml');
899+
$message = Message::fromString($raw);
900+
$this->assertInstanceOf(Message::class, $message);
901+
$this->assertTrue($message->getBody()->isMultiPart());
902+
$plainTextBody = $message->getPlainTextBodyPart();
903+
$this->assertSame("This is a test email with 1 attachment.", trim($plainTextBody->getRawContent()));
904+
$htmlBody = $message->getHtmlBodyPart();
905+
$this->assertSame("<div>This is a test email with 1 attachment.</div>", trim($htmlBody->getRawContent()));
906+
}
907+
908+
public function testReturnsEmptyAttachmentsListWhenEmailHasNoAttachments()
909+
{
910+
$raw = file_get_contents(__DIR__ . '/_files/laminas-mail-19.eml');
911+
$message = Message::fromString($raw);
912+
$this->assertInstanceOf(Message::class, $message);
913+
$this->assertTrue($message->getBody()->isMultiPart());
914+
$this->assertEmpty($message->getAttachments());
915+
}
916+
917+
public function testCanRetrieveMessageAttachmentsWhenAttachmentsAreAvailable()
918+
{
919+
$raw = file_get_contents(__DIR__ . '/_files/mail_with_pdf_attachment.eml');
920+
$message = Message::fromString($raw);
921+
$this->assertInstanceOf(Message::class, $message);
922+
$this->assertTrue($message->getBody()->isMultiPart());
891923

892-
$attachmentPart = $parts[1];
924+
$attachments = $message->getAttachments();
925+
$this->assertCount(1, $attachments);
926+
/** @var Part $attachment */
927+
$attachment = array_pop($attachments);
928+
$this->assertTrue(str_starts_with($attachment->getType(), "application/pdf"));
893929

894930
$tempFile = sprintf("%stemp.pdf", sys_get_temp_dir());
895931
$handle = fopen($tempFile, "w");
896-
fwrite($handle, $attachmentPart->getRawContent());
932+
fwrite($handle, $attachment->getRawContent());
897933
fclose($handle);
898934

899935
$parser = new Parser();

test/_files/mail_with_pdf_attachment.eml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Content-Transfer-Encoding: quoted-printable
3131
--001a11447dc881e40b0537fe6d58--
3232

3333
--001a11447dc881e40f0537fe6d5a
34-
Content-Disposition: inline;
34+
Content-Disposition: attachment;
3535
filename="test document.pdf"
3636
Content-Type: application/pdf;
3737
x-mac-hide-extension=yes;

0 commit comments

Comments
 (0)