Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ability to extract text and html from multipart messages #157

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Author

@kynx kynx Nov 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've raised a PR against laminas/laminas-mail to fix what I think are issues with their documentation. If that's merged this section may not be needed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kynx It seems they are merging your work :). So .. what do you think makes most sense here? Drop this PR, or merge it neverthless?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is still required so the text and html can be extracted from the multipart/alternative part. But I'll just link to the laminas docs for a (now correct) example of how to create the email in the first place.


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));
}
}