Skip to content

Commit

Permalink
Add utility functions for retrieving message content and attachments
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
settermjd committed Nov 30, 2023
1 parent 33f94d0 commit e7d10b0
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 3 deletions.
17 changes: 17 additions & 0 deletions docs/book/message/attachments.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,22 @@ use Laminas\Mime\Mime;
$mimeMessage->setMime(new Mime($customBoundary));
```

## Retrieving attachments

If you have created a multipart message with one or more attachments, whether programmatically
or via the `Message::fromString();` method, you can readily retrieve them by calling the `getAttachments()` method.
It will return an array of `\Laminas\Mime\Part` objects.

For example:

```php
// Instantiate a Message object from a .eml file.
$raw = file_get_contents(__DIR__ . '/mail_with_attachments.eml');
$message = Message::fromString($raw);

// Retrieve the email's attachments.
$attachments = $message->getAttachments();
```

[mime-boundary]: https://www.oreilly.com/library/view/programming-internet-email/9780596802585/ch03s04.html
[multipart-content-type]: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
15 changes: 15 additions & 0 deletions docs/book/message/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,21 @@ EOF;
$message = Message::fromString($rawEmail);
```

### Retrieve a Message's plain text and HTML body

Commonly, though not always, an email will contain one or both of a plain text and/or HTMl body.
To retrieve these directly, there are two methods available `getPlainTextBodyPart()` and
`getHtmlBodyPart()`. For example:

```php
// Instantiate a Message object from a .eml file.
$raw = file_get_contents(__DIR__ . '/mail_with_attachments.eml');
$message = Message::fromString($raw);

echo $message->getPlainTextBodyPart();
echo $message->getHtmlBodyPart();
```

## Configuration Options

The `Message` class has no configuration options, and is instead a value object.
Expand Down
47 changes: 47 additions & 0 deletions src/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,23 @@
use Laminas\Mail\Header\ReplyTo;
use Laminas\Mail\Header\Sender;
use Laminas\Mail\Header\To;
use Laminas\Mail\Iterator\AttachmentPartFilterIterator;
use Laminas\Mail\Iterator\MessagePartFilterIterator;
use Laminas\Mail\Iterator\PartsIterator;
use Laminas\Mime;
use Laminas\Mime\Part;
use RecursiveIteratorIterator;
use Traversable;

use function array_filter;
use function array_pop;
use function count;
use function date;
use function gettype;
use function is_array;
use function is_object;
use function is_string;
use function iterator_to_array;
use function method_exists;
use function sprintf;
use function str_starts_with;
Expand Down Expand Up @@ -118,6 +125,46 @@ public function getHeaders()
return $this->headers;
}

public function getBodyPart(string $partType): Part
{
/** @var Part[] $iterator */
$iterator = new RecursiveIteratorIterator(
new MessagePartFilterIterator(
new PartsIterator($this->getBody()->getParts()),
$partType
)
);

$part = iterator_to_array($iterator);
return array_pop($part);
}

public function getPlainTextBodyPart(): Part
{
return $this->getBodyPart(\Laminas\Mime\Mime::TYPE_TEXT);
}

public function getHtmlBodyPart(): Part
{
return $this->getBodyPart(\Laminas\Mime\Mime::TYPE_HTML);
}

/**
* @return Part[]
*/
public function getAttachments(): array
{
/** @var Part[] $iterator */
$iterator = new RecursiveIteratorIterator(
new AttachmentPartFilterIterator(
new PartsIterator(
$this->getBody()->getParts()
),
)
);
return iterator_to_array($iterator);
}

/**
* Set (overwrite) From addresses
*
Expand Down
40 changes: 38 additions & 2 deletions test/MessageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
use Laminas\Mail\Message;
use Laminas\Mime\Message as MimeMessage;
use Laminas\Mime\Mime;
use Laminas\Mime\Part;
use Laminas\Mime\Part as MimePart;
use PHPUnit\Framework\TestCase;
use Smalot\PdfParser\Parser;
use stdClass;

use function array_pop;
use function count;
use function date;
use function fclose;
Expand All @@ -25,6 +27,7 @@
use function fwrite;
use function implode;
use function sprintf;
use function str_starts_with;
use function substr;
use function sys_get_temp_dir;
use function trim;
Expand Down Expand Up @@ -888,12 +891,45 @@ public function testCanParseMultipartEmail(): void
'<div>This is a test email with 1 attachment.</div>',
trim($partOne->getParts()[1]->getRawContent())
);
}

public function testCanReturnPlainTextAndHTMLMessageBodyIfAvailable()
{
$raw = file_get_contents(__DIR__ . '/_files/mail_with_pdf_attachment.eml');
$message = Message::fromString($raw);
$this->assertInstanceOf(Message::class, $message);
$this->assertTrue($message->getBody()->isMultiPart());
$plainTextBody = $message->getPlainTextBodyPart();
$this->assertSame("This is a test email with 1 attachment.", trim($plainTextBody->getRawContent()));
$htmlBody = $message->getHtmlBodyPart();
$this->assertSame("<div>This is a test email with 1 attachment.</div>", trim($htmlBody->getRawContent()));
}

public function testReturnsEmptyAttachmentsListWhenEmailHasNoAttachments()
{
$raw = file_get_contents(__DIR__ . '/_files/laminas-mail-19.eml');
$message = Message::fromString($raw);
$this->assertInstanceOf(Message::class, $message);
$this->assertTrue($message->getBody()->isMultiPart());
$this->assertEmpty($message->getAttachments());
}

public function testCanRetrieveMessageAttachmentsWhenAttachmentsAreAvailable()
{
$raw = file_get_contents(__DIR__ . '/_files/mail_with_pdf_attachment.eml');
$message = Message::fromString($raw);
$this->assertInstanceOf(Message::class, $message);
$this->assertTrue($message->getBody()->isMultiPart());

$attachmentPart = $parts[1];
$attachments = $message->getAttachments();
$this->assertCount(1, $attachments);
/** @var Part $attachment */
$attachment = array_pop($attachments);
$this->assertTrue(str_starts_with($attachment->getType(), "application/pdf"));

$tempFile = sprintf("%stemp.pdf", sys_get_temp_dir());
$handle = fopen($tempFile, "w");
fwrite($handle, $attachmentPart->getRawContent());
fwrite($handle, $attachment->getRawContent());
fclose($handle);

$parser = new Parser();
Expand Down
2 changes: 1 addition & 1 deletion test/_files/mail_with_pdf_attachment.eml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Content-Transfer-Encoding: quoted-printable
--001a11447dc881e40b0537fe6d58--

--001a11447dc881e40f0537fe6d5a
Content-Disposition: inline;
Content-Disposition: attachment;
filename="test document.pdf"
Content-Type: application/pdf;
x-mac-hide-extension=yes;
Expand Down

0 comments on commit e7d10b0

Please sign in to comment.