Skip to content

Commit

Permalink
Merge pull request #157 from kynx/extract-from-multipart-messages
Browse files Browse the repository at this point in the history
Added ability to extract text and html from multipart messages
  • Loading branch information
roelvanduijnhoven authored Nov 17, 2023
2 parents bbeb128 + 394177f commit b40a982
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 3 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ $message->setBody($body);

> For accessibility purposes, you should *always* provide both a text and HTML version of your mails.
### `multipart/alternative` emails with attachments

The correct way to compose an email message that contains text, html _and_ attachments is to create a
`multipart/alternative` part containing the text and html parts, followed by one or more parts for the attachments. See
the [Laminas Documentation](https://docs.laminas.dev/laminas-mail/message/attachments/#multipartalternative-emails-with-attachments)
for a full example.

### How to configure HttpClient with http_options and http_adapter

By default the adapter is Laminas\Http\Client\Adapter\Socket but you can override it with other adapter like this in your slm_mail.*.local.php
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0",
"laminas/laminas-mail": "^2.9",
"laminas/laminas-http": "^2.8",
"laminas/laminas-mime": "^2.7",
"laminas/laminas-mime": "^2.8",
"laminas/laminas-servicemanager": "^3.11"
},
"require-dev": {
Expand Down
38 changes: 36 additions & 2 deletions src/Service/AbstractMailService.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,21 @@
use Laminas\Mime\Message as MimeMessage;
use Laminas\Mime\Mime;

use function in_array;

/**
* Class AbstractMailService
*/
abstract class AbstractMailService implements MailServiceInterface
{
private const MULTIPART_TYPES = [
Mime::MULTIPART_ALTERNATIVE,
Mime::MULTIPART_MIXED,
Mime::MULTIPART_RELATED,
Mime::MULTIPART_RELATIVE,
Mime::MULTIPART_REPORT,
];

/**
* @var HttpClient
*/
Expand All @@ -74,11 +84,23 @@ protected function extractText(Message $message): ?string
return null;
}

foreach ($body->getParts() as $part) {
return $this->extractTextFromMimeMessage($body);
}

private function extractTextFromMimeMessage(MimeMessage $message): ?string
{
foreach ($message->getParts() as $part) {
if ($part->type === 'text/plain' && $part->disposition !== Mime::DISPOSITION_ATTACHMENT) {
return $part->getContent();
}
}
foreach ($message->getParts() as $part) {
if (in_array($part->type, self::MULTIPART_TYPES)) {
return $this->extractTextFromMimeMessage(
MimeMessage::createFromMessage($part->getContent(), $part->boundary)
);
}
}

return null;
}
Expand All @@ -98,11 +120,23 @@ protected function extractHtml(Message $message): ?string
return null;
}

foreach ($body->getParts() as $part) {
return $this->extractHtmlFromMimeMessage($body);
}

private function extractHtmlFromMimeMessage(MimeMessage $message): ?string
{
foreach ($message->getParts() as $part) {
if ($part->type === 'text/html' && $part->disposition !== Mime::DISPOSITION_ATTACHMENT) {
return $part->getContent();
}
}
foreach ($message->getParts() as $part) {
if (in_array($part->type, self::MULTIPART_TYPES)) {
return $this->extractHtmlFromMimeMessage(
MimeMessage::createFromMessage($part->getContent(), $part->boundary)
);
}
}

return null;
}
Expand Down
179 changes: 179 additions & 0 deletions tests/SlmMailTest/Service/AbstractMailServiceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<?php

declare(strict_types=1);

namespace SlmMailTest\Service;

use Laminas\Mail\Message;
use Laminas\Mime\Message as MimeMessage;
use Laminas\Mime\Mime;
use Laminas\Mime\Part;
use SlmMail\Service\AbstractMailService;
use PHPUnit\Framework\TestCase;

/**
* @covers \SlmMail\Service\AbstractMailService
*/
final class AbstractMailServiceTest extends TestCase
{
private AbstractMailService $service;

protected function setUp(): void
{
parent::setUp();

$this->service = new class () extends AbstractMailService {
public ?string $text = null;
public ?string $html = null;
public array $attachments = [];

public function send(Message $message)
{
$this->text = $this->extractText($message);
$this->html = $this->extractHtml($message);
$this->attachments = $this->extractAttachments($message);
}
};
}

public function testExtractTextFromStringBodyReturnsString(): void
{
$expected = 'Foo';
$message = new Message();
$message->setBody($expected);

$this->service->send($message);
self::assertSame($expected, $this->service->text);
}

public function testExtractTextFromEmptyBodyReturnsNull(): void
{
$message = new Message();

$this->service->send($message);
self::assertNull($this->service->text);
}

public function testExtractTextFromTwoPartMessageReturnsString(): void
{
$expected = 'Foo';
$message = new Message();
$body = new MimeMessage();
$body->addPart(new Part(''));
$body->addPart(
(new Part($expected))
->setType(Mime::TYPE_TEXT)
);
$message->setBody($body);

$this->service->send($message);
self::assertSame($expected, $this->service->text);
}

public function testExtractTextFromTextAttachmentReturnsNull(): void
{
$message = new Message();
$body = new MimeMessage();
$body->addPart(
(new Part('Foo'))
->setType(Mime::TYPE_TEXT)
->setDisposition(Mime::DISPOSITION_ATTACHMENT)
);
$message->setBody($body);

$this->service->send($message);
self::assertNull($this->service->text);
}

public function testExtractTextFromMultipartMessageReturnsString(): void
{
$expected = 'Foo';
$message = new Message();
$body = new MimeMessage();
$contentPart = new MimeMessage();
$contentPart->addPart(new Part());
$contentPart->addPart(
(new Part($expected))
->setType(Mime::TYPE_TEXT)
);
$body->addPart(
(new Part($contentPart->generateMessage()))
->setType(Mime::MULTIPART_ALTERNATIVE)
->setBoundary($contentPart->getMime()->boundary())
);
$message->setBody($body);

$this->service->send($message);
self::assertSame($expected, trim($this->service->text));
}

public function testExtractHtmlFromStringBodyReturnsNull(): void
{
$message = new Message();
$message->setBody('Foo');

$this->service->send($message);
self::assertNull($this->service->html);
}

public function testExtractHtmlFromEmptyBodyReturnsNull(): void
{
$message = new Message();

$this->service->send($message);
self::assertNull($this->service->html);
}

public function testExtractHtmlFromTwoPartMessageReturnsString(): void
{
$expected = 'Foo';
$message = new Message();
$body = new MimeMessage();
$body->addPart(new Part(''));
$body->addPart(
(new Part($expected))
->setType(Mime::TYPE_HTML)
);
$message->setBody($body);

$this->service->send($message);
self::assertSame($expected, $this->service->html);
}

public function testExtractHtmlFromHtmlAttachmentReturnsNull(): void
{
$message = new Message();
$body = new MimeMessage();
$body->addPart(
(new Part('Foo'))
->setType(Mime::TYPE_HTML)
->setDisposition(Mime::DISPOSITION_ATTACHMENT)
);
$message->setBody($body);

$this->service->send($message);
self::assertNull($this->service->html);
}

public function testExtractHtmlFromMultipartMessageReturnsString(): void
{
$expected = 'Foo';
$message = new Message();
$body = new MimeMessage();
$contentPart = new MimeMessage();
$contentPart->addPart(new Part());
$contentPart->addPart(
(new Part($expected))
->setType(Mime::TYPE_HTML)
);
$body->addPart(
(new Part($contentPart->generateMessage()))
->setType(Mime::MULTIPART_ALTERNATIVE)
->setBoundary($contentPart->getMime()->boundary())
);
$message->setBody($body);

$this->service->send($message);
self::assertSame($expected, trim($this->service->html));
}
}

0 comments on commit b40a982

Please sign in to comment.