Skip to content

Commit

Permalink
Merge pull request #45 from fcoedno/mime-decode
Browse files Browse the repository at this point in the history
Decode mime headers
  • Loading branch information
rpkamp committed Sep 9, 2020
2 parents 8f0fb9e + 979d71a commit 46a4b37
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 25 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@
"require": {
"php": "^7.2",
"ext-json": "*",
"ext-iconv": "*",
"php-http/client-implementation": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"php-http/message-factory": "^1.0",
"psr/http-message": "^1.0"
},
"require-dev": {
"jakub-onderka/php-parallel-lint": "^1.0",
"phpmd/phpmd": "^2.1.2",
"phpmd/phpmd": "~2.8.0",
"phpunit/phpunit": "^8.0",
"php-http/curl-client": "^2.0",
"swiftmailer/swiftmailer": "^6.2",
Expand Down
77 changes: 77 additions & 0 deletions src/Message/Headers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace rpkamp\Mailhog\Message;

use function iconv_mime_decode;
use function strtolower;

class Headers
{
/**
* @var array<string, string> $headers
*/
private $headers;

/**
* @param array<string, string> $headers
*/
public function __construct(array $headers)
{
$this->headers = $headers;
}

/**
* @param array<mixed, mixed> $mailhogResponse
*/
public static function fromMailhogResponse(array $mailhogResponse): self
{
return self::fromRawHeaders($mailhogResponse['Content']['Headers'] ?? []);
}

/**
* @param array<mixed, mixed> $mimePart
*/
public static function fromMimePart(array $mimePart): self
{
return self::fromRawHeaders($mimePart['Headers']);
}

/**
* @param array<string, array<string>> $rawHeaders
*/
private static function fromRawHeaders(array $rawHeaders): self
{
$headers = [];
foreach ($rawHeaders as $name => $header) {
if (!isset($header[0])) {
continue;
}

$decoded = iconv_mime_decode($header[0]);

$headers[strtolower($name)] = $decoded ? $decoded : $header[0];
}

return new Headers($headers);
}

public function get(string $name, string $default = ''): string
{
$name = strtolower($name);

if (isset($this->headers[$name])) {
return $this->headers[$name];
}

return $default;
}

public function has(string $name): bool
{
$name = strtolower($name);

return isset($this->headers[$name]);
}
}
27 changes: 11 additions & 16 deletions src/Message/MessageFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,28 @@ class MessageFactory
public static function fromMailhogResponse(array $mailhogResponse): Message
{
$mimeParts = MimePartCollection::fromMailhogResponse($mailhogResponse['MIME']['Parts'] ?? []);
$headers = $mailhogResponse['Content']['Headers'];
$headers = Headers::fromMailhogResponse($mailhogResponse);

return new Message(
$mailhogResponse['ID'],
Contact::fromString($headers['From'][0]),
ContactCollection::fromString($headers['To'][0] ?? ''),
ContactCollection::fromString($headers['Cc'][0] ?? ''),
ContactCollection::fromString($headers['Bcc'][0] ?? ''),
$headers['Subject'][0] ?? '',
Contact::fromString($headers->get('From')),
ContactCollection::fromString($headers->get('To', '')),
ContactCollection::fromString($headers->get('Cc', '')),
ContactCollection::fromString($headers->get('Bcc', '')),
$headers->get('Subject', ''),
!$mimeParts->isEmpty()
? $mimeParts->getBody()
: static::getBodyFrom($mailhogResponse['Content']),
: static::decodeBody($headers, $mailhogResponse['Content']['Body']),
!$mimeParts->isEmpty() ? $mimeParts->getAttachments() : []
);
}

/**
* @param mixed[] $content
*/
private static function getBodyFrom(array $content): string
private static function decodeBody(Headers $headers, string $body): string
{
if (isset($content['Headers']['Content-Transfer-Encoding'][0]) &&
$content['Headers']['Content-Transfer-Encoding'][0] === 'quoted-printable'
) {
return quoted_printable_decode($content['Body']);
if ($headers->get('Content-Transfer-Encoding') === 'quoted-printable') {
return quoted_printable_decode($body);
}

return $content['Body'];
return $body;
}
}
21 changes: 13 additions & 8 deletions src/Message/Mime/MimePart.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace rpkamp\Mailhog\Message\Mime;

use rpkamp\Mailhog\Message\Headers;
use function base64_decode;
use function explode;
use function preg_match;
Expand Down Expand Up @@ -55,25 +56,29 @@ private function __construct(
*/
public static function fromMailhogResponse(array $mimePart): MimePart
{
$headers = Headers::fromMimePart($mimePart);

$filename = null;
if (isset($mimePart['Headers']['Content-Disposition'][0]) &&
stripos($mimePart['Headers']['Content-Disposition'][0], 'attachment') === 0
if ($headers->has('Content-Disposition') &&
stripos($headers->get('Content-Disposition'), 'attachment') === 0
) {
$matches = [];
preg_match('~filename=(?P<filename>.*?)(;|$)~i', $mimePart['Headers']['Content-Disposition'][0], $matches);
preg_match('~filename=(?P<filename>.*?)(;|$)~i', $headers->get('Content-Disposition'), $matches);
$filename = $matches['filename'];
}

$isAttachment = false;
if (isset($mimePart['Headers']['Content-Disposition'][0])) {
$isAttachment = stripos($mimePart['Headers']['Content-Disposition'][0], 'attachment') === 0;
if ($headers->has('Content-Disposition')) {
$isAttachment = stripos($headers->get('Content-Disposition'), 'attachment') === 0;
}

return new self(
isset($mimePart['Headers']['Content-Type'][0])
? explode(';', $mimePart['Headers']['Content-Type'][0])[0]
$headers->has('Content-Type')
? explode(';', $headers->get('Content-Type'))[0]
: 'application/octet-stream',
$mimePart['Headers']['Content-Transfer-Encoding'][0] ?? null,
$headers->has('Content-Transfer-Encoding')
? $headers->get('Content-Transfer-Encoding')
: null,
$isAttachment,
$filename,
$mimePart['Body']
Expand Down
78 changes: 78 additions & 0 deletions tests/unit/Message/Fixtures/sample_mailhog_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"total":1,
"count":1,
"start":0,
"items":[
{
"ID":"1SepfeC9XRP7xKMDmPfczx8k3ktwjh605ZVatm4L9ew=@mailhog.example",
"From":{
"Relays":null,
"Mailbox":"no-reply",
"Domain":"myself.example",
"Params":""
},
"To":[
{
"Relays":null,
"Mailbox":"jose",
"Domain":"myself.example",
"Params":""
},
{
"Relays":null,
"Mailbox":"leticia",
"Domain":"myself.example",
"Params":""
}
],
"Content":{
"Headers":{
"Content-Transfer-Encoding":[
"quoted-printable"
],
"Content-Type":[
"text/html; charset=utf-8"
],
"Date":[
"Sun, 06 Sep 2020 15:24:56 -0300"
],
"From":[
"=?utf-8?Q?Jos=C3=A9?= de tal \u003cno-reply@myself.example\u003e"
],
"MIME-Version":[
"1.0"
],
"Message-ID":[
"\u003c3f91997768d3c98ab3e19cebb5a731f8@swift.generated\u003e"
],
"Received":[
"from [127.0.0.1] by mailhog.example (MailHog)\r\n id 1SepfeC9XRP7xKMDmPfczx8k3ktwjh605ZVatm4L9ew=@mailhog.example; Sun, 06 Sep 2020 18:24:56 +0000"
],
"Return-Path":[
"\u003cno-reply@myself.example\u003e"
],
"Subject":[
"Mailhog =?utf-8?Q?=C3=A9?= muito bom mesmo: =?utf-8?Q?=F0=9F=98=81?="
],
"To":[
"=?utf-8?Q?Jos=C3=A9?= \u003cjose@myself.example\u003e, =?utf-8?Q?Let=C3=ADcia_maranh=C3=A3o?= \u003cleticia@myself.example\u003e"
]
},
"Body":"foobar",
"Size":474,
"MIME":null
},
"Created":"2020-09-06T18:24:56.58754722Z",
"MIME":null,
"Raw":{
"From":"no-reply@myself.example",
"To":[
"jose@myself.example",
"leticia@myself.example"
],
"Data":"Message-ID: \u003c3f91997768d3c98ab3e19cebb5a731f8@swift.generated\u003e\r\nDate: Sun, 06 Sep 2020 15:24:56 -0300\r\nSubject: Mailhog =?utf-8?Q?=C3=A9?= muito bom mesmo:\r\n =?utf-8?Q?=F0=9F=98=81?=\r\nFrom: =?utf-8?Q?Jos=C3=A9?= de tal \u003cno-reply@myself.example\u003e\r\nTo: =?utf-8?Q?Jos=C3=A9?= \u003cjose@myself.example\u003e,\r\n =?utf-8?Q?Let=C3=ADcia_maranh=C3=A3o?= \u003cleticia@myself.example\u003e\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=utf-8\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nfoobar",
"Helo":"[127.0.0.1]"
}
}
]
}
95 changes: 95 additions & 0 deletions tests/unit/Message/HeadersTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace rpkamp\Mailhog\Tests\unit\Message;

use PHPUnit\Framework\TestCase;
use rpkamp\Mailhog\Message\Headers;

class HeadersTest extends TestCase
{
/**
* @test
*/
public function it_should_parse_headers(): void
{
$messageData = $this->getMessageData();
$headers = Headers::fromMailHogResponse($messageData);

$this->assertEquals(
"Mailhog é muito bom mesmo: 😁",
$headers->get("Subject")
);

$this->assertEquals(
"José <jose@myself.example>, Letícia maranhão <leticia@myself.example>",
$headers->get("To")
);

$this->assertEquals(
"José de tal <no-reply@myself.example>",
$headers->get("From")
);

$this->assertEquals(
"quoted-printable",
$headers->get("Content-Transfer-Encoding")
);

$this->assertEquals(
"text/html; charset=utf-8",
$headers->get("Content-Type")
);
}

/**
* @test
*/
public function it_should_ignore_case_for_the_header_name(): void
{
$messageData = $this->getMessageData();
$headers = Headers::fromMailHogResponse($messageData);

$this->assertEquals(
"José de tal <no-reply@myself.example>",
$headers->get("from")
);
}

/**
* @test
*/
public function it_should_return_the_default_value_when_the_header_does_not_exist(): void
{
$headers = new Headers([]);

$this->assertEquals(
'default value',
$headers->get('foobar', 'default value')
);
}

/**
* @test
*/
public function it_should_check_if_a_header_exists(): void
{
$headers = new Headers([]);
$this->assertFalse($headers->has('foobar'));
}

/**
* @return array<mixed, mixed>
*/
private function getMessageData(): array
{
$contents = file_get_contents(__DIR__ . '/Fixtures/sample_mailhog_response.json');
if (!$contents) {
return [];
}

$allMessagesData = json_decode($contents, true);
return $allMessagesData['items'][0];
}
}

0 comments on commit 46a4b37

Please sign in to comment.