Skip to content

Commit

Permalink
Refactor instantiation of mime messages
Browse files Browse the repository at this point in the history
From my understanding of the various specs, primarily RFC 1341, the
existing functionality for setting message headers was wrong. Given
that, I've made this change, which should bring it into line with the
spec, corrects issues with the existing tests, and sets one existing
test as skipped (for the time being).

Firstly, it removes the functionality that sets headers on the main
message from message sub-parts. Secondly, equally as importantly, it
sets the required headers, along with their defaults, based on the
message's composition. I won't get into detail here, as it's documented
in the code and in the updates to the documentation.

Finally, the change also cleans up some of the existing functionality as
well, setting return types, as they're supported in the package's
allowed PHP versions.
  • Loading branch information
settermjd committed Nov 23, 2023
1 parent f2419f4 commit 2a9594f
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 32 deletions.
4 changes: 2 additions & 2 deletions src/Headers.php
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,8 @@ public function clearHeaders()
* Get all headers of a certain name/type
*
* @param string $name
* @return false|ArrayIterator|HeaderInterface Returns false if there is no headers with $name in this
* contain, an ArrayIterator if the header is a MultipleHeadersInterface instance and finally returns
* @return false|ArrayIterator|HeaderInterface Returns false if there are no headers with $name,
* an ArrayIterator if the header is a MultipleHeadersInterface instance, and finally returns
* HeaderInterface for the rest of cases.
*/
public function get($name)
Expand Down
123 changes: 99 additions & 24 deletions src/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
use ArrayIterator;
use Laminas\Mail\Header\Bcc;
use Laminas\Mail\Header\Cc;
use Laminas\Mail\Header\ContentTransferEncoding;
use Laminas\Mail\Header\ContentType;
use Laminas\Mail\Header\From;
use Laminas\Mail\Header\HeaderInterface;
use Laminas\Mail\Header\MimeVersion;
use Laminas\Mail\Header\ReplyTo;
use Laminas\Mail\Header\Sender;
use Laminas\Mail\Header\To;
use Laminas\Mime;
use Traversable;

use function array_shift;
use function array_filter;
use function count;
use function date;
use function gettype;
Expand All @@ -23,6 +25,9 @@
use function is_string;
use function method_exists;
use function sprintf;
use function str_starts_with;

use const ARRAY_FILTER_USE_BOTH;

class Message
{
Expand Down Expand Up @@ -372,9 +377,8 @@ public function getSubject()
*
* @param null|string|\Laminas\Mime\Message|object $body
* @throws Exception\InvalidArgumentException
* @return Message
*/
public function setBody($body)
public function setBody($body): Message
{
if (! is_string($body) && $body !== null) {
if (! is_object($body)) {
Expand All @@ -396,34 +400,105 @@ public function setBody($body)
}
}
}
$this->body = $body;

if (! $this->body instanceof Mime\Message) {
return $this;
/**
* Set the required mime message headers.
*/
if ($body instanceof Mime\Message) {
/**
* Add the mime-version header if the body is mime-compliant,
* and the mime-version header is not already set.
*
* @see https://www.w3.org/Protocols/rfc1341/3_MIME-Version.html
*/
if (! $this->getHeaders()->has('mime-version')) {
$this->headers->addHeader(new MimeVersion());
}

/**
* Add a multipart (mixed) content-type header, if the body
* is multipart, and a multipart (mixed) content-type header is not
* already set.
*
* @see https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
* */
if ($body->isMultiPart()) {
if (! $this->hasMultipartContentType()) {
$this->headers->addHeader(
(new ContentType())
->setType(Mime\Mime::MULTIPART_MIXED)
->addParameter('boundary', $body->getMime()->boundary())
);
}
}

switch (count($body->getParts())) {
/**
* Set the default headers (content-type and content-transfer-encoding) to their default values.
*
* @see https://www.w3.org/Protocols/rfc1341/7_1_Text.html
* @see https://www.w3.org/Protocols/rfc1341/5_Content-Transfer-Encoding.html
*/
case 0:
$this->headers->addHeader(
(new ContentType())
->setType(Mime\Mime::TYPE_TEXT)
->addParameter('charset', 'us-ascii')
);
$this->headers->addHeader(
(new ContentTransferEncoding())
->setTransferEncoding(Mime\Mime::ENCODING_7BIT)
);
break;

/**
* Set the default headers from the sole message part available.
*/
case 1:
$part = $body->getParts()[0];
$this->headers->addHeader(
(new ContentType())
->setType($part->getType())
->addParameter('charset', $part->getCharset())
);
$this->headers->addHeader(
(new ContentTransferEncoding())
->setTransferEncoding($part->getEncoding())
);
break;
}
}

// Get headers, and set Mime-Version header
$headers = $this->getHeaders();
$this->getHeaderByName('mime-version', MimeVersion::class);
$this->body = $body;

return $this;
}

public function hasMultipartContentType(): bool
{
if (! $this->getHeaders()->has('content-type')) {
return false;
}

// Multipart content headers
if ($this->body->isMultiPart()) {
$mime = $this->body->getMime();
$contentTypes = $this->getHeaders()->get('content-type');

/** @var ContentType $header */
$header = $this->getHeaderByName('content-type', ContentType::class);
$header->setType('multipart/mixed');
$header->addParameter('boundary', $mime->boundary());
return $this;
if ($contentTypes instanceof HeaderInterface) {
return str_starts_with($contentTypes->getFieldValue(), 'multipart');
}

// MIME single part headers
$parts = $this->body->getParts();
if (! empty($parts)) {
$part = array_shift($parts);
$headers->addHeaders($part->getHeadersArray("\r\n"));
if ($contentTypes instanceof ArrayIterator) {
$headers = array_filter(
$contentTypes->getArrayCopy(),
/** @var HeaderInterface $contentType */
function ($contentType) {
return str_starts_with($contentType->getFieldValue(), 'multipart');
},
ARRAY_FILTER_USE_BOTH
);
return count($headers) !== 0;
}
return $this;

return false;
}

/**
Expand Down Expand Up @@ -457,7 +532,7 @@ public function getBodyText()
*
* @param string $headerName
* @param string $headerClass
* @return Header\HeaderInterface|ArrayIterator header instance or collection of headers
* @return HeaderInterface|ArrayIterator header instance or collection of headers
*/
protected function getHeaderByName($headerName, $headerClass)
{
Expand Down
24 changes: 18 additions & 6 deletions test/MessageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,20 @@ public function testMaySetBodyFromMimeMessage(): void
$body = new MimeMessage();
$this->message->setBody($body);
$this->assertSame($body, $this->message->getBody());
$headers = $this->message->getHeaders();
$this->assertTrue($headers->has('content-type'));
$contentTypeHeader = $headers->get('content-type');
$this->assertSame(
sprintf(
'%s;%scharset="us-ascii"',
Mime::TYPE_TEXT,
Headers::FOLDING
),
$contentTypeHeader->getFieldValue()
);
$this->assertTrue($headers->has('content-transfer-encoding'));
$contentTypeHeader = $headers->get('content-transfer-encoding');
$this->assertSame(Mime::ENCODING_7BIT, $contentTypeHeader->getFieldValue());
}

public function testMaySetNullBody(): void
Expand Down Expand Up @@ -569,10 +583,6 @@ public function testSettingBodyFromSinglePartMimeMessageSetsAppropriateHeaders()
$this->assertTrue($headers->has('mime-version'));
$header = $headers->get('mime-version');
$this->assertEquals('1.0', $header->getFieldValue());

$this->assertTrue($headers->has('content-type'));
$header = $headers->get('content-type');
$this->assertEquals('text/html', $header->getFieldValue());
}

public function testSettingUtf8MailBodyFromSinglePartMimeUtf8MessageSetsAppropriateHeaders(): void
Expand Down Expand Up @@ -799,6 +809,8 @@ public function testDetectsCRLFInjectionViaSubject(): void

public function testHeaderUnfoldingWorksAsExpectedForMultipartMessages(): void
{
$this->markTestSkipped("This likely isn't required anymore, as header unfolding is incorrect functionality");

$text = new MimePart('Test content');
$text->type = Mime::TYPE_TEXT;
$text->encoding = Mime::ENCODING_QUOTEDPRINTABLE;
Expand Down Expand Up @@ -840,10 +852,9 @@ public function testCanParseMultipartReport(): void
$raw = file_get_contents(__DIR__ . '/_files/laminas-mail-19.eml');
$message = Message::fromString($raw);
$this->assertInstanceOf(Message::class, $message);
$this->assertIsString($message->getBody());
$this->assertInstanceOf(MimeMessage::class, $message->getBody());

$headers = $message->getHeaders();
$this->assertCount(8, $headers);
$this->assertTrue($headers->has('Date'));
$this->assertTrue($headers->has('From'));
$this->assertTrue($headers->has('Message-Id'));
Expand All @@ -854,6 +865,7 @@ public function testCanParseMultipartReport(): void
$this->assertTrue($headers->has('Auto-Submitted'));

$contentType = $headers->get('Content-Type');
$this->assertInstanceOf(Header\HeaderInterface::class, $contentType);
$this->assertEquals('multipart/report', $contentType->getType());
}

Expand Down

0 comments on commit 2a9594f

Please sign in to comment.