From 6a82139521a40989496e62da32c23371ea0c2f3a Mon Sep 17 00:00:00 2001 From: Arwin Date: Thu, 16 Feb 2023 11:44:59 +0100 Subject: [PATCH 1/3] Method to get all html parts and complete html body --- .gitignore | 2 + composer.json | 16 +- phpstan-baseline.neon | 4 +- src/Message/AbstractMessage.php | 62 ++++++ src/Message/BasicMessageInterface.php | 16 +- tests/MessageTest.php | 29 +++ .../multiple_html_parts_and_attachments.eml | 203 ++++++++++++++++++ 7 files changed, 322 insertions(+), 10 deletions(-) create mode 100644 tests/fixtures/multiple_html_parts_and_attachments.eml diff --git a/.gitignore b/.gitignore index 0d825c39..84781e1b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ composer.lock phpunit.xml wait-for-it coverage/ + +.DS_Store diff --git a/composer.json b/composer.json index 724f5398..3bd26f5a 100644 --- a/composer.json +++ b/composer.json @@ -25,15 +25,17 @@ "php": "~8.1.0 || ~8.2.0", "ext-iconv": "*", "ext-imap": "*", - "ext-mbstring": "*" + "ext-mbstring": "*", + "ext-dom": "*", + "ext-libxml": "*" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.13.2", - "laminas/laminas-mail": "^2.21.1", - "phpstan/phpstan": "^1.9.8", - "phpstan/phpstan-phpunit": "^1.3.3", - "phpstan/phpstan-strict-rules": "^1.4.4", - "phpunit/phpunit": "^9.5.27" + "friendsofphp/php-cs-fixer": "^3.14.4", + "laminas/laminas-mail": "^2.22.0", + "phpstan/phpstan": "^1.9.17", + "phpstan/phpstan-phpunit": "^1.3.4", + "phpstan/phpstan-strict-rules": "^1.4.5", + "phpunit/phpunit": "^9.6.3" }, "autoload": { "psr-4": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e3360ff6..dc566524 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -22,12 +22,12 @@ parameters: - message: "#^Cannot call method getDecodedContent\\(\\) on mixed\\.$#" - count: 2 + count: 3 path: src/Message/AbstractMessage.php - message: "#^Cannot call method getSubtype\\(\\) on mixed\\.$#" - count: 2 + count: 3 path: src/Message/AbstractMessage.php - diff --git a/src/Message/AbstractMessage.php b/src/Message/AbstractMessage.php index 0f022cc4..4fe29c93 100644 --- a/src/Message/AbstractMessage.php +++ b/src/Message/AbstractMessage.php @@ -222,6 +222,68 @@ final public function getBodyHtml(): ?string return null; } + /** + * Get body HTML parts. + * + * @return string[] + */ + final public function getBodyHtmlParts(): array + { + $iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST); + $htmlParts = []; + foreach ($iterator as $part) { + if (self::SUBTYPE_HTML === $part->getSubtype()) { + $htmlParts[] = $part->getDecodedContent(); + } + } + if (\count($htmlParts) > 0) { + return $htmlParts; + } + + // If message has no parts and is HTML, return content of message itself. + if (self::SUBTYPE_HTML === $this->getSubtype()) { + return [$this->getDecodedContent()]; + } + + return []; + } + + /** + * Get all body HTML parts merged into 1 html. + */ + final public function getCompleteBodyHtml(): ?string + { + $htmlParts = $this->getBodyHtmlParts(); + + if (1 === \count($htmlParts)) { + return $htmlParts[0]; + } + if (0 === \count($htmlParts)) { + return null; + } + \libxml_use_internal_errors(true); // Suppress parse errors, get errors with libxml_get_errors(); + + $newDom = new \DOMDocument(); + + $newBody = ''; + $newDom->loadHTML(\mb_convert_encoding(\implode('', $htmlParts), 'HTML-ENTITIES', 'UTF-8')); + + $bodyTags = $newDom->getElementsByTagName('body'); + + foreach ($bodyTags as $body) { + foreach ($body->childNodes as $node) { + $newBody .= $newDom->saveHTML($node); + } + } + + $newDom = new \DOMDocument(); + $newDom->loadHTML(\mb_convert_encoding($newBody, 'HTML-ENTITIES', 'UTF-8')); + + $completeHtml = $newDom->saveHTML(); + + return false === $completeHtml ? null : $completeHtml; + } + /** * Get body text. */ diff --git a/src/Message/BasicMessageInterface.php b/src/Message/BasicMessageInterface.php index 83dbd17f..ace15f0c 100644 --- a/src/Message/BasicMessageInterface.php +++ b/src/Message/BasicMessageInterface.php @@ -109,12 +109,26 @@ public function getInReplyTo(): array; public function getReferences(): array; /** - * Get body HTML. + * Get first body HTML part. * * @return null|string Null if message has no HTML message part */ public function getBodyHtml(): ?string; + /** + * Get all body HTML parts as array. + * + * @return string[] + */ + public function getBodyHtmlParts(): array; + + /** + * Get all body HTML parts merged into 1 html. + * + * @return null|string Null if message has no HTML message part + */ + public function getCompleteBodyHtml(): ?string; + /** * Get body text. */ diff --git a/tests/MessageTest.php b/tests/MessageTest.php index ed32f153..5e349eb4 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -990,6 +990,35 @@ public function testNestesEmbeddedWithAttachment(): void } } + public function testMultipleHtmlParts(): void + { + $this->mailbox->addMessage($this->getFixture('multiple_html_parts_and_attachments')); + + $message = $this->mailbox->getMessage(1); + + // Test attachments + $expectedFileNames = [ + 'attachment1.pdf', + 'attachment2.pdf', + ]; + $attachments = $message->getAttachments(); + static::assertCount(2, $attachments); + foreach ($attachments as $attachment) { + static::assertContains($attachment->getFilename(), $expectedFileNames); + } + + // Test html parts + static::assertCount(3, $message->getBodyHtmlParts()); + + // Test html parts + $completeBody = $message->getCompleteBodyHtml(); + $completeBody = null === $completeBody ? '' : $completeBody; + + static::assertStringContainsString('first', $completeBody); + static::assertStringContainsString('second', $completeBody); + static::assertStringContainsString('last', $completeBody); + } + public function testImapMimeHeaderDecodeReturnsFalse(): void { $this->mailbox->addMessage($this->getFixture('imap_mime_header_decode_returns_false')); diff --git a/tests/fixtures/multiple_html_parts_and_attachments.eml b/tests/fixtures/multiple_html_parts_and_attachments.eml new file mode 100644 index 00000000..efcac55a --- /dev/null +++ b/tests/fixtures/multiple_html_parts_and_attachments.eml @@ -0,0 +1,203 @@ +Return-Path: +Delivered-To: to@there.com +Return-path: +Envelope-to: to@there.com +Delivery-date: Thu, 16 Feb 2023 09:19:23 +0000 +From: FromName +Content-Type: multipart/alternative; + boundary="Apple-Mail=_5382A451-FE2F-4504-9E3F-8FEB0B716E43" +Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3731.400.51.1.1\)) +Subject: multiple_html_parts_and_attachments +Date: Thu, 16 Feb 2023 10:19:02 +0100 +To: to@there.com + + + +--Apple-Mail=_5382A451-FE2F-4504-9E3F-8FEB0B716E43 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=utf-8 + +This is the first html part + +=EF=BF=BC + +This is the second html part + +=EF=BF=BC + +This is the last html part +https://www.there.com + + +--Apple-Mail=_5382A451-FE2F-4504-9E3F-8FEB0B716E43 +Content-Type: multipart/mixed; + boundary="Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE" + + +--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +This is the first html part

+--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Disposition: inline; + filename=attachment1.pdf +Content-Type: application/pdf; + x-unix-mode=0666; + name="attachment1.pdf" +Content-Transfer-Encoding: base64 + +JVBERi0xLjMKJcTl8uXrp/Og0MTGCjMgMCBvYmoKPDwgL0ZpbHRlciAvRmxhdGVEZWNvZGUgL0xl +bmd0aCA5MyA+PgpzdHJlYW0KeAEdjDsOgCAQBXtP8U7AzwWW3sZOKmtDKExEhej9JWbamamIqFAd +G6ww3jowacEcGC1jxQm55Jby/bzbgbZ3W2v6CzPCkBJu9AxSQRBRGFKBnIvGdPVz/ABGZhatCmVu +ZHN0cmVhbQplbmRvYmoKMSAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDIgMCBSIC9SZXNv +dXJjZXMgNCAwIFIgL0NvbnRlbnRzIDMgMCBSIC9NZWRpYUJveCBbMCAwIDU5NS4yNzU2IDg0MS44 +ODk4XQo+PgplbmRvYmoKNCAwIG9iago8PCAvUHJvY1NldCBbIC9QREYgL0ltYWdlQiAvSW1hZ2VD +IC9JbWFnZUkgXSAvWE9iamVjdCA8PCAvSW0xIDUgMCBSID4+ID4+CmVuZG9iago1IDAgb2JqCjw8 +IC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggMTE0IC9IZWlnaHQgMjMgL0lu +dGVycG9sYXRlIHRydWUKL0NvbG9yU3BhY2UgNiAwIFIgL0ludGVudCAvUGVyY2VwdHVhbCAvQml0 +c1BlckNvbXBvbmVudCA4IC9MZW5ndGggMTUzNCAvRmlsdGVyCi9GbGF0ZURlY29kZSA+PgpzdHJl +YW0KeAHtmAlTllUUxz9CZTVN+75rmkJmZqMpVgoCoogikArhgmyhKYoKCqK4gQQoCgIBKiiEGCIg +u4CoKU1Mi/uu7fUB7CdnunPnPmLvy4Bj08s888w592z3/J9zz7kvN286/voWgb9u9Hc8PUagu2/T +Y4cOQxBwoNoXZXBvonrj0uCfLg3ui3zvjk9bUD16/MP7xh3j2Vo0z9jVhTNvd3476rerA2XdYA1l +29nHvZoGzyq3Xb+PNG1M58p556CETV9W+6pt2ILqgpR4QXVsRL4yFCIwIRnRkWMf3ZY1lG1n7xFU +jey6239i9iJASMharBT+FVXq8Hmf2icmNT47pe7+8Uc7O0cqWwgjrsHqmnbR/wlUvz7psmJLzISF +O4DFXlSLK/0xmZe0Tip25bYlCp+AuNQXptUgHRW6i3WDFbWiioDgxA39Aw4MDS6dv27tnko/ZQ6x +r2bqB5H5T01u4MNNWrJN1bygWn7IZ+qyDEKMmFesV8KUmK3sp6Zpol9s2qt+B31XpFc1Tjrz4/CZ +8SkSKDot9o/rA1QgehSiIbP20VWCEze2HR2nRLgir6Y2N++lma9Mrxoxtzguc8mfXba3TUcZQmSV +zAZPgdReVAmHSd1hD0JDDJq5X3n2XJz1tHc9i06B+xanxhksahm7Q5DyAPvI+bsfnNAGXbh/pnjI +3DOXLfVzO/J+6K7nfGoR8T79w7tIQfUB13b0H3ZvHfBxhThZnxclhkgf6hI96nlYNsDK6/6VqKGM +QwjAEeWGFvcnJzewwtcZPmdvP9f2xyY2872UK2wf8WhByg4Jh+aa7M+QWtMRE+t7c2EYVvp3v3MH +OHPqHTapkITAvL7FXXk2jrzBukQUAE7rP7UBjJhTNphfPe9MS8E5JQdLn6HhI92Q9yksmYrmxbND +qZyc0k9gOWsSV6ToXz731vWLQ1hHyhdpaR+PQmX9ZFiOADS2lB9sckGEjNSSqukA6xRUprui2q9d +cGKFs4Oy28IckRrpyKL1bS+q63KjiLJqW7S4Wr5lGSwHWXk24hpsfvksVZmYkBHmHDpoRNAccOWq +45vRiJLzI1gBN8oY0ET6+7U3qKKXfKuFFSmTV9jVWbeGBadeWJBEgZYC29g6AdHAGV+JSN5jwgpZ +PH5iLCyanAi+nYjoG9StCmSkozvRaXtRdQ4qYwPTlmdEJcfzSDfg0P16ZZC4NeIaLDrNR1zpgaPD +drJVaUGCKucFz3qX1vdJshxJfQVzJqasIH1Rk6YUhOMqbVeo0n/Gu45Nwu4oDUYEwtSzevDMYkWd +Nwq4op0qQ4jX/A6qQNZ0dE1F24WqNFI2YH2YQeLTiKuz1AwAYksTYNzMWLU5NnMprKAauWk1tPUC +LG5J1rivGqjqUkFVdwWqTEBc0U+IAoz0IuM51OyJgjUQqNKHb5udLFrfdqEakpTElpJyFjBe1SM9 +QZDBvw6jwdIwMWeHp7oGEFKGLytiu/GLSOg5a9erTX7/3XvhGxNpoaxYk+0ZqsUHbl1gmDsqCgSB +aPW/dB03a6A+rdWfL78JIPQcfl/oWzrL/HJtZwTLuqCq5pHO5pYFkZHeORelrlSoMvKgGdnqd1n4 +hjWsgHYvosrtmt3SKhlqkgW/hflAdGlpy7agqrLTcdBp22s1ryyQHLlD6uZCj4/KRZS6Mwx2afoK +aK/o7Uwig2Uc0EjJiIvK9r2zgVf6KvO39rAHyh6LsrFl4G4pCvGP/Zz7AMdWCtuabM9qlSjysbg1 +cQqyS4K5XBGUGYGIxxpIr1UjOzGxvm1HVVJmUludpO+ez8ZcwgsR8R2ZsLByrg2WwmOsIOWhuZVW ++TL4oOXqQrXI+BMFrm1c5iUc84JLux76Zd9qaZUsGlJJKrN4rtLnlkVrFZb/0oSuXysheHPFjUlf +rqat4QoTfkeoQEY6yr9BUGB45nerWr/zfVWp3ZngoKkjhqbOcikCqxMdLsoDNNdgxZ4/Pay60etk +xxg01WKvE0TkLrq/dsq508Psda6nY6Ntr6BqY6z/j5oD1b741g5U7yaq3aHtWLcXgb8Bi7W2lwpl +bmRzdHJlYW0KZW5kb2JqCjcgMCBvYmoKPDwgL04gMyAvQWx0ZXJuYXRlIC9EZXZpY2VSR0IgL0xl +bmd0aCAzNDMgL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngBdZA7SwNBFIXPxsiCpkgh +Vim2sRBWiZsgdpKoiJBiiYqPbrKJiZDHsFkRG7HzD4iVYCdprWKhIP4CQVCxshC1FwI+wnomUTaF +3uHOfHPm3N3LBUIRIWU5DKBS9dzsfNpYXVs39BeEEIWOJEzh1GXKtjO04PdUHETrFpq63Yypb8VO +P7Kv+eP9+7Q5rYvGYuD7kwbyhbrDly+m5UjXA7Q42d72pOI98pDLpsgHiotdbijOdfm841nKztBz +TY46JZEnP5LNXI9e7OFKeUv9V4XqPlKoLqteh5kxzGIOGS4DNixOYQJT1PBPTbJTM4MaJHbgYhNF +lOCxOkVFoowCeQFVOBiHSbYQZybUrFnLCGYYaLUIMPnMx6tAE0/A2e7PSAxVCYzQEz0CLt6lcEVX +4661wvWNhNW5a4NNoP/Q999WAH0UaN/5/mfT99snQN8DcNn6BsE0ZMMKZW5kc3RyZWFtCmVuZG9i +ago2IDAgb2JqClsgL0lDQ0Jhc2VkIDcgMCBSIF0KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1Bh +Z2VzIC9NZWRpYUJveCBbMCAwIDU5NS4yNzU2IDg0MS44ODk4XSAvQ291bnQgMSAvS2lkcyBbIDEg +MCBSIF0KPj4KZW5kb2JqCjggMCBvYmoKPDwgL1R5cGUgL0NhdGFsb2cgL1BhZ2VzIDIgMCBSID4+ +CmVuZG9iago5IDAgb2JqCjw8IC9UaXRsZSAo/v9cMDAwU1wwMDBjXDAwMGhcMDAwZVwwMDByXDAw +MG1cMDAwrVwwMDBhXDAwMGZcMDAwYlwwMDBlXDAwMGVcMDAwbFwwMDBkXDAwMGlcMDAwblwwMDBn +XDAwMCBcMDAwMlwwMDAwXDAwMDJcMDAwM1wwMDAtXDAwMDBcMDAwMlwwMDAtXDAwMDFcMDAwNlww +MDAgXDAwMG9cMDAwbVwwMDAgXDAwMDFcMDAwMFwwMDAuXDAwMDFcMDAwMVwwMDAuXDAwMDBcMDAw +M1wwMDAuXDAwMHBcMDAwblwwMDBnKQovUHJvZHVjZXIgKG1hY09TIFZlcnNpZSAxMy4yIFwoYnVp +bGQgMjJENDlcKSBRdWFydHogUERGQ29udGV4dCkgL0NyZWF0b3IgKFZvb3J2ZXJ0b25pbmcpCi9D +cmVhdGlvbkRhdGUgKEQ6MjAyMzAyMTYwOTExNDdaMDAnMDAnKSAvTW9kRGF0ZSAoRDoyMDIzMDIx +NjA5MTE0N1owMCcwMCcpCj4+CmVuZG9iagp4cmVmCjAgMTAKMDAwMDAwMDAwMCA2NTUzNSBmIAow +MDAwMDAwMTg2IDAwMDAwIG4gCjAwMDAwMDI2MDIgMDAwMDAgbiAKMDAwMDAwMDAyMiAwMDAwMCBu +IAowMDAwMDAwMzAwIDAwMDAwIG4gCjAwMDAwMDAzODkgMDAwMDAgbiAKMDAwMDAwMjU2NyAwMDAw +MCBuIAowMDAwMDAyMTI1IDAwMDAwIG4gCjAwMDAwMDI2OTUgMDAwMDAgbiAKMDAwMDAwMjc0NCAw +MDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDEwIC9Sb290IDggMCBSIC9JbmZvIDkgMCBSIC9JRCBb +IDxlNzk0OWE2YzQ2MWM1MDYxNTk0ODU5YjkxZjczYzAzMz4KPGU3OTQ5YTZjNDYxYzUwNjE1OTQ4 +NTliOTFmNzNjMDMzPiBdID4+CnN0YXJ0eHJlZgozMTYxCiUlRU9GCg== +--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +

This is the second html part

+--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Disposition: inline; + filename=attachment2.pdf +Content-Type: application/pdf; + x-unix-mode=0666; + name="attachment2.pdf" +Content-Transfer-Encoding: base64 + +JVBERi0xLjMKJcTl8uXrp/Og0MTGCjMgMCBvYmoKPDwgL0ZpbHRlciAvRmxhdGVEZWNvZGUgL0xl +bmd0aCA5MyA+PgpzdHJlYW0KeAEdjDsOgCAQBXtP8U4ALB9Zehs7qawNoTARFaL3l5hpZ6YiokJ1 +XHBCezeCLQnmwGgZK07IJbeU7+fdDrS926TpL4yCNl6Q8QyrnAjWhiEVyLkQpquf4wdGBBarCmVu +ZHN0cmVhbQplbmRvYmoKMSAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDIgMCBSIC9SZXNv +dXJjZXMgNCAwIFIgL0NvbnRlbnRzIDMgMCBSIC9NZWRpYUJveCBbMCAwIDU5NS4yNzU2IDg0MS44 +ODk4XQo+PgplbmRvYmoKNCAwIG9iago8PCAvUHJvY1NldCBbIC9QREYgL0ltYWdlQiAvSW1hZ2VD +IC9JbWFnZUkgXSAvWE9iamVjdCA8PCAvSW0xIDUgMCBSID4+ID4+CmVuZG9iago1IDAgb2JqCjw8 +IC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggMTIxIC9IZWlnaHQgMzAgL0lu +dGVycG9sYXRlIHRydWUKL0NvbG9yU3BhY2UgNiAwIFIgL0ludGVudCAvUGVyY2VwdHVhbCAvQml0 +c1BlckNvbXBvbmVudCA4IC9MZW5ndGggMTczNSAvRmlsdGVyCi9GbGF0ZURlY29kZSA+PgpzdHJl +YW0KeAHtl4lT1kUYx/+Eymq677s0TTHzajSPPBFE8UA0T1RuxRMVUfEAEVECFQVREQ9UxiNFVFAU +FJAxGrDM+9bu+gPsI8+0s+27v3qFmnLm987OO8+9u9999nn2d++e+3MRcBFwEXARcBFwEXBE4Le7 +Td3RYAQcYbUpGjyL6wgCNkQdZS5ijUHAEVabojETNd737vWW311v2fg4/1UEG6KOMn2RVdU9Hul1 +mrFme6guh7568aO62s4/3WoucoM1jL1nnws43nL0Xu/t/yXLv93O+XMd0rZEjluUMjRu1ayMuZXV +PWQljrDaFPrip6xIEKi7R+fqcugxC1NRVZzuKXKDNYy9Z/8nUP/1dsjAt4IOsf0n+51kQDzue2rd +zvFs04aoo0zBQsa+Nrj4+QGlrwwqebR3VV1dJ6WCMBZjsLrlA9EPBdQ9J28C3qUbYoDo1lWflXmR +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 +99snQN8DcNn6BsE0ZMMKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqClsgL0lDQ0Jhc2VkIDcgMCBS +IF0KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9NZWRpYUJveCBbMCAwIDU5NS4yNzU2 +IDg0MS44ODk4XSAvQ291bnQgMSAvS2lkcyBbIDEgMCBSIF0KPj4KZW5kb2JqCjggMCBvYmoKPDwg +L1R5cGUgL0NhdGFsb2cgL1BhZ2VzIDIgMCBSID4+CmVuZG9iago5IDAgb2JqCjw8IC9UaXRsZSAo +/v9cMDAwU1wwMDBjXDAwMGhcMDAwZVwwMDByXDAwMG1cMDAwrVwwMDBhXDAwMGZcMDAwYlwwMDBl +XDAwMGVcMDAwbFwwMDBkXDAwMGlcMDAwblwwMDBnXDAwMCBcMDAwMlwwMDAwXDAwMDJcMDAwM1ww +MDAtXDAwMDBcMDAwMlwwMDAtXDAwMDFcMDAwNlwwMDAgXDAwMG9cMDAwbVwwMDAgXDAwMDFcMDAw +MFwwMDAuXDAwMDFcMDAwMVwwMDAuXDAwMDFcMDAwMFwwMDAuXDAwMHBcMDAwblwwMDBnKQovUHJv +ZHVjZXIgKG1hY09TIFZlcnNpZSAxMy4yIFwoYnVpbGQgMjJENDlcKSBRdWFydHogUERGQ29udGV4 +dCkgL0NyZWF0b3IgKFZvb3J2ZXJ0b25pbmcpCi9DcmVhdGlvbkRhdGUgKEQ6MjAyMzAyMTYwOTEx +MzhaMDAnMDAnKSAvTW9kRGF0ZSAoRDoyMDIzMDIxNjA5MTEzOFowMCcwMCcpCj4+CmVuZG9iagp4 +cmVmCjAgMTAKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMTg2IDAwMDAwIG4gCjAwMDAwMDI4 +MDMgMDAwMDAgbiAKMDAwMDAwMDAyMiAwMDAwMCBuIAowMDAwMDAwMzAwIDAwMDAwIG4gCjAwMDAw +MDAzODkgMDAwMDAgbiAKMDAwMDAwMjc2OCAwMDAwMCBuIAowMDAwMDAyMzI2IDAwMDAwIG4gCjAw +MDAwMDI4OTYgMDAwMDAgbiAKMDAwMDAwMjk0NSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDEw +IC9Sb290IDggMCBSIC9JbmZvIDkgMCBSIC9JRCBbIDxjOWI4YzBlZGQ5Mjk1Y2U3ZmQ2YzFkOWJj +ZTJhZTRiOD4KPGM5YjhjMGVkZDkyOTVjZTdmZDZjMWQ5YmNlMmFlNGI4PiBdID4+CnN0YXJ0eHJl +ZgozMzYyCiUlRU9GCg== +--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +

This is the last html part
https://www.there.com



+
+--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE-- + +--Apple-Mail=_5382A451-FE2F-4504-9E3F-8FEB0B716E43-- From 4579c0d99992741292b5bbfbae5688bc5c38bc91 Mon Sep 17 00:00:00 2001 From: Arwin Date: Thu, 16 Feb 2023 12:04:24 +0100 Subject: [PATCH 2/3] Simplify getBodyHtml, fix php 8.2 and composer normalize --- composer.json | 6 +++--- phpstan-baseline.neon | 4 ++-- src/Message/AbstractMessage.php | 20 +++++--------------- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/composer.json b/composer.json index 3bd26f5a..67643423 100644 --- a/composer.json +++ b/composer.json @@ -23,11 +23,11 @@ ], "require": { "php": "~8.1.0 || ~8.2.0", + "ext-dom": "*", "ext-iconv": "*", "ext-imap": "*", - "ext-mbstring": "*", - "ext-dom": "*", - "ext-libxml": "*" + "ext-libxml": "*", + "ext-mbstring": "*" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.14.4", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index dc566524..e3360ff6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -22,12 +22,12 @@ parameters: - message: "#^Cannot call method getDecodedContent\\(\\) on mixed\\.$#" - count: 3 + count: 2 path: src/Message/AbstractMessage.php - message: "#^Cannot call method getSubtype\\(\\) on mixed\\.$#" - count: 3 + count: 2 path: src/Message/AbstractMessage.php - diff --git a/src/Message/AbstractMessage.php b/src/Message/AbstractMessage.php index 4fe29c93..4145b2d1 100644 --- a/src/Message/AbstractMessage.php +++ b/src/Message/AbstractMessage.php @@ -203,23 +203,13 @@ final public function getReferences(): array } /** - * Get body HTML. + * Get first body HTML part. */ final public function getBodyHtml(): ?string { - $iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST); - foreach ($iterator as $part) { - if (self::SUBTYPE_HTML === $part->getSubtype()) { - return $part->getDecodedContent(); - } - } - - // If message has no parts and is HTML, return content of message itself. - if (self::SUBTYPE_HTML === $this->getSubtype()) { - return $this->getDecodedContent(); - } + $htmlParts = $this->getBodyHtmlParts(); - return null; + return $htmlParts[0] ?? null; } /** @@ -266,7 +256,7 @@ final public function getCompleteBodyHtml(): ?string $newDom = new \DOMDocument(); $newBody = ''; - $newDom->loadHTML(\mb_convert_encoding(\implode('', $htmlParts), 'HTML-ENTITIES', 'UTF-8')); + $newDom->loadHTML(\implode('', $htmlParts)); $bodyTags = $newDom->getElementsByTagName('body'); @@ -277,7 +267,7 @@ final public function getCompleteBodyHtml(): ?string } $newDom = new \DOMDocument(); - $newDom->loadHTML(\mb_convert_encoding($newBody, 'HTML-ENTITIES', 'UTF-8')); + $newDom->loadHTML($newBody); $completeHtml = $newDom->saveHTML(); From 10dc04f1f795735bd25bb34ab302248e1777894d Mon Sep 17 00:00:00 2001 From: Arwin Date: Thu, 16 Feb 2023 13:47:59 +0100 Subject: [PATCH 3/3] Add tests for body html (#557) --- tests/MessageTest.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 5e349eb4..5b7ca728 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -1019,6 +1019,28 @@ public function testMultipleHtmlParts(): void static::assertStringContainsString('last', $completeBody); } + public function testBodyHtmlEmpty(): void + { + $this->mailbox->addMessage($this->getFixture('plain_only')); + + $message = $this->mailbox->getMessage(1); + + static::assertCount(0, $message->getBodyHtmlParts()); + + static::assertNull($message->getCompleteBodyHtml()); + } + + public function testBodyHtmlOnePart(): void + { + $this->mailbox->addMessage($this->getFixture('html_only')); + + $message = $this->mailbox->getMessage(1); + + static::assertCount(1, $message->getBodyHtmlParts()); + + static::assertNotNull($message->getCompleteBodyHtml()); + } + public function testImapMimeHeaderDecodeReturnsFalse(): void { $this->mailbox->addMessage($this->getFixture('imap_mime_header_decode_returns_false'));