From 5cfd7aeaf7366641f4299ee13056e5acdf3ef3d7 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 18 Jan 2023 19:21:03 +0100 Subject: [PATCH 001/203] Additional test for issue #275 added --- tests/issues/Issue275Test.php | 12 +++- tests/messages/issue-275-2.eml | 123 +++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 tests/messages/issue-275-2.eml diff --git a/tests/issues/Issue275Test.php b/tests/issues/Issue275Test.php index 5d004254..3cb5c5de 100644 --- a/tests/issues/Issue275Test.php +++ b/tests/issues/Issue275Test.php @@ -17,7 +17,7 @@ class Issue275Test extends TestCase { - public function testIssue() { + public function testIssueEmail1() { $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-275.eml"]); $message = Message::fromFile($filename); @@ -25,4 +25,14 @@ public function testIssue() { self::assertSame("testing123 this is a body", $message->getTextBody()); } + public function testIssueEmail2() { + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-275-2.eml"]); + $message = Message::fromFile($filename); + + $body = "Test\r\n\r\nMed venlig hilsen\r\nMartin Larsen\r\nFeline Holidays A/S\r\nTlf 78 77 04 12"; + + self::assertSame("Test 1017", (string)$message->subject); + self::assertSame($body, $message->getTextBody()); + } + } \ No newline at end of file diff --git a/tests/messages/issue-275-2.eml b/tests/messages/issue-275-2.eml new file mode 100644 index 00000000..d80e66ed --- /dev/null +++ b/tests/messages/issue-275-2.eml @@ -0,0 +1,123 @@ +Received: from AS4PR02MB8234.eurprd02.prod.outlook.com (2603:10a6:20b:4f1::17) + by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS; Wed, 18 Jan 2023 + 09:17:10 +0000 +Received: from AS1PR02MB7917.eurprd02.prod.outlook.com (2603:10a6:20b:4a5::5) + by AS4PR02MB8234.eurprd02.prod.outlook.com (2603:10a6:20b:4f1::17) with + Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.5986.19; Wed, 18 Jan + 2023 09:17:09 +0000 +Received: from AS1PR02MB7917.eurprd02.prod.outlook.com + ([fe80::4871:bdde:a499:c0d9]) by AS1PR02MB7917.eurprd02.prod.outlook.com + ([fe80::4871:bdde:a499:c0d9%9]) with mapi id 15.20.5986.023; Wed, 18 Jan 2023 + 09:17:09 +0000 +From: =?iso-8859-1?Q?Martin_B=FClow_Larsen?= +To: Cofman Drift +Subject: Test 1017 +Thread-Topic: Test 1017 +Thread-Index: AdkrHaKsy+bUiL/eTIS5W3AP+zzLjQ== +Date: Wed, 18 Jan 2023 09:17:09 +0000 +Message-ID: + +Accept-Language: da-DK, en-US +Content-Language: en-US +X-MS-Exchange-Organization-AuthAs: Internal +X-MS-Exchange-Organization-AuthMechanism: 04 +X-MS-Exchange-Organization-AuthSource: AS1PR02MB7917.eurprd02.prod.outlook.com +X-MS-Has-Attach: +X-MS-Exchange-Organization-Network-Message-Id: + 98cea1c9-a497-454a-6606-08daf934c6c4 +X-MS-Exchange-Organization-SCL: -1 +X-MS-TNEF-Correlator: +X-MS-Exchange-Organization-RecordReviewCfmType: 0 +x-ms-publictraffictype: Email +X-Microsoft-Antispam-Mailbox-Delivery: + ucf:0;jmr:0;auth:0;dest:I;ENG:(910001)(944506478)(944626604)(920097)(425001)(930097); +X-Microsoft-Antispam-Message-Info: + BzqL6hvPyQW0lSkWGop6vQlsIZK48umY74vuKlNgF0pb/H659W+0fuTB+6guqGM0oof00mlzu3gn1pu1R5pUOE2Fb58OqnBEFkB30vVrG6TNsG/6KBtecXkP3FptqO/WRmsxCQx7bK7J2VyS2SbOibqX8mDZhkTrwP1+IA0R9eD0/NvoMqX9GssewUDxSAbaaKdADCuU1dG7qpF8M9tfLDJz+dUL5qZoO+oGINGLLdo2y6Z/F+h3UWv7BXiS4BJKc+jwAng26BUMKmg2VVRdMvc+LbZTovUr9hyEq1orS9fOg1iIV6sPcyIVl3NIEy5bHMYh1d6sUCqvTO1UPSdf7lIvKxSszyKptIPNgioOvFpF9tTHDyKU5p1IiLm5FxW/+kGdPq6ZoTIZVriJoyjx6gWKpPY3vHN6bHUK9mA+LspIYAeuDFFvwkZx2b2Rtw3S99S7zz3eBqv3xlGqJixx/apl4Af7ZaxKFpMj9XZXAQVIv9BA0tIA+1nLByt4dPW4Xzoj3KcBbX5K/HkuR/30Lrq7gRQQDyNYgf5S/MO2MLJqcvnVFPXgVubK6XFu5quDibsZjPjxOUfBTJkJ/n4gB8Z8/TOM0oKD76hszXXoaWd9leUeQ1x88oy+QuXPRxzuLzVec3GiPNMYA42QvvTiWmrrhdceRzhV0J7pJBbi10ik+hXqSeNkldgktd5cWPss5F74yxAaEaPJO9I7MOUpE0XzlRfljPptykrIQt8SARMllykzJNrDt8VAl37ArEZbWxFLm3RuypOI0zgCZMRLf5JeElpCv1ay4wilz4vsYGr4fs3KUQzI1YY43uaDxNMz8k7dH/UkC9Dfg1oyHlNs+w== +Content-Type: multipart/alternative; + boundary="_000_AS1PR02MB791721260DE0273A15AFEC3EB5C79AS1PR02MB7917eurp_" +MIME-Version: 1.0 + +--_000_AS1PR02MB791721260DE0273A15AFEC3EB5C79AS1PR02MB7917eurp_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Test + +Med venlig hilsen +Martin Larsen +Feline Holidays A/S +Tlf 78 77 04 12 + + + +--_000_AS1PR02MB791721260DE0273A15AFEC3EB5C79AS1PR02MB7917eurp_ +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + + + +
+

Test

+

 

+

= +Med venlig hilsen

+

Martin Larsen

+

Feline Holidays A/S

+

Tlf 78 77 04 12

+

 

+

 

+
+ + + +--_000_AS1PR02MB791721260DE0273A15AFEC3EB5C79AS1PR02MB7917eurp_-- From bbc52543239dd681bb7e9ff5637c9b80e85b593e Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 18 Jan 2023 19:36:54 +0100 Subject: [PATCH 002/203] Release information added --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f0c2a94..03d5f8dd 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Breaking changes +- NaN + + +## [5.0.0] - 2023-01-18 +### Fixed - The message uid and message number will only be fetched if accessed and wasn't previously set #326 #285 (thanks @szymekjanaczek) - Fix undefined attachment name when headers use "filename*=" format #301 (thanks @JulienChavee) - Fixed `ImapProtocol::logout` always throws 'not connected' Exception after upgraded to 4.1.2 #351 From 3facf95fe88134bc27db672ba6a7dbf1f8b35d05 Mon Sep 17 00:00:00 2001 From: Axel Guckelsberger Date: Fri, 20 Jan 2023 16:01:32 +0100 Subject: [PATCH 003/203] more unique ID generation to prevent multiple attachments with same ID (#363) --- src/Attachment.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index ff280b1b..c6afe30f 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -202,7 +202,7 @@ protected function fetch(): void { if (($id = $this->part->id) !== null) { $this->id = str_replace(['<', '>'], '', $id); }else{ - $this->id = hash("sha256", (string)microtime(true)); + $this->id = hash("sha256", uniqid((string) rand(10000, 99999), true)); } $this->size = $this->part->bytes; @@ -339,4 +339,4 @@ public function mask(string $mask = null): mixed { throw new MaskNotFoundException("Unknown mask provided: ".$mask); } -} \ No newline at end of file +} From 3addefa1dda8d1a45b0b189a4ab124dff09cbd11 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 20 Jan 2023 16:02:47 +0100 Subject: [PATCH 004/203] Changelog updated --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03d5f8dd..ef170a34 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- More unique ID generation to prevent multiple attachments with same ID #363 (thanks @Guite) ### Added - NaN From 46ef2a641ddd906b9eddaea8c796c4c9ab3abaa4 Mon Sep 17 00:00:00 2001 From: Adrian Kuriata Date: Mon, 13 Feb 2023 19:09:31 +0100 Subject: [PATCH 005/203] Fix for saving all attachments from them messages. (#372) There was a problem with fetch all attachments. Some attachments are not added to the collection. Checking attachment ID before add it to collection solved the problem in my case. --- src/Message.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Message.php b/src/Message.php index c3eabb0a..568f2a1e 100755 --- a/src/Message.php +++ b/src/Message.php @@ -701,7 +701,7 @@ protected function fetchAttachment(Part $part): void { $oAttachment = new Attachment($this, $part); if ($oAttachment->getName() !== null && $oAttachment->getSize() > 0) { - if ($oAttachment->getId() !== null) { + if ($oAttachment->getId() !== null && $this->attachments->offsetExists($oAttachment->getId())) { $this->attachments->put($oAttachment->getId(), $oAttachment); } else { $this->attachments->push($oAttachment); From 4a6228fbcbe236f7d4feb1015a742d046bfc5ac0 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 1 Mar 2023 02:08:47 +0100 Subject: [PATCH 006/203] Partial fix for #362 (allow search response to be empty) --- CHANGELOG.md | 2 ++ src/Connection/Protocols/ImapProtocol.php | 3 +-- src/Connection/Protocols/LegacyProtocol.php | 1 + src/Connection/Protocols/Response.php | 13 ++++++++++++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef170a34..01f3af17 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - More unique ID generation to prevent multiple attachments with same ID #363 (thanks @Guite) +- Not all attachments are pushed to the collection #372 (thanks @AdrianKuriata) +- Partial fix for #362 (allow search response to be empty) ### Added - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index e8cd71d8..cc9cf46d 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -1196,12 +1196,11 @@ public function done(): bool { */ public function search(array $params, int|string $uid = IMAP::ST_UID): Response { $command = $this->buildUIDCommand("SEARCH", $uid); - $response = $this->requestAndResponse($command, $params); + $response = $this->requestAndResponse($command, $params)->setCanBeEmpty(true); foreach ($response->data() as $ids) { if ($ids[0] === 'SEARCH') { array_shift($ids); - return $response->setResult($ids); } } diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index e8323372..de2ddb60 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -697,6 +697,7 @@ public function done() { */ public function search(array $params, int|string $uid = IMAP::ST_UID): Response { return $this->response("imap_search")->wrap(function($response)use($params, $uid){ + $response->setCanBeEmpty(true); $result = \imap_search($this->stream, $params[0], $uid ? IMAP::ST_UID : IMAP::NIL); return $result ?: []; }); diff --git a/src/Connection/Protocols/Response.php b/src/Connection/Protocols/Response.php index 6f9a2753..068f082f 100644 --- a/src/Connection/Protocols/Response.php +++ b/src/Connection/Protocols/Response.php @@ -64,6 +64,8 @@ class Response { */ protected bool $debug = false; + protected bool $can_be_empty = false; + /** * Create a new Response instance */ @@ -328,7 +330,7 @@ public function successful(): bool { return false; } } - return $this->boolean() && !$this->getErrors(); + return ($this->boolean() || $this->canBeEmpty()) && !$this->getErrors(); } @@ -369,4 +371,13 @@ public function failed(): bool { public function Noun(): int { return $this->noun; } + + public function setCanBeEmpty(bool $can_be_empty): Response { + $this->can_be_empty = $can_be_empty; + return $this; + } + + public function canBeEmpty(): bool { + return $this->can_be_empty; + } } \ No newline at end of file From 9f56ebf0dd4808cd5893f88cd45fd3d2bd0b207c Mon Sep 17 00:00:00 2001 From: Santiago H <52068520+shuergab@users.noreply.github.com> Date: Wed, 1 Mar 2023 02:13:45 +0100 Subject: [PATCH 007/203] Fixed issue #354 (#366) Fixed the issue #354 by casting the key to string in the switch. Tested on over 100000 emails and over 100 accounts without any problem. --- src/Header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index 77e3de11..d4a0117f 100644 --- a/src/Header.php +++ b/src/Header.php @@ -265,7 +265,7 @@ public function rfc822_parse_headers($raw_headers): object { foreach ($headers as $key => $values) { if (isset($imap_headers[$key])) continue; $value = null; - switch ($key) { + switch ((string)$key) { case 'from': case 'to': case 'cc': From e108c27f3862356e687d63565f2e23618b170b36 Mon Sep 17 00:00:00 2001 From: Gioele Luchetti Date: Wed, 1 Mar 2023 02:18:19 +0100 Subject: [PATCH 008/203] Fix use of ST_MSGN as sequence method (#356) --- src/Connection/Protocols/ImapProtocol.php | 12 ++++++------ src/Connection/Protocols/LegacyProtocol.php | 2 +- src/Folder.php | 2 +- src/Message.php | 14 +++++++------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index cc9cf46d..737f83ef 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -644,7 +644,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in $data = []; // find array key of UID value; try the last elements, or search for it - if ($uid) { + if ($uid === IMAP::ST_UID) { $count = count($tokens[2]); if ($tokens[2][$count - 2] == 'UID') { $uidKey = $count - 1; @@ -661,7 +661,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in } // ignore other messages - if ($to === null && !is_array($from) && ($uid ? $tokens[2][$uidKey] != $from : $tokens[0] != $from)) { + if ($to === null && !is_array($from) && ($uid === IMAP::ST_UID ? $tokens[2][$uidKey] != $from : $tokens[0] != $from)) { continue; } @@ -669,7 +669,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in if (count($items) == 1) { if ($tokens[2][0] == $items[0]) { $data = $tokens[2][1]; - } elseif ($uid && $tokens[2][2] == $items[0]) { + } elseif ($uid === IMAP::ST_UID && $tokens[2][2] == $items[0]) { $data = $tokens[2][3]; } else { $expectedResponse = 0; @@ -696,12 +696,12 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in } // if we want only one message we can ignore everything else and just return - if ($to === null && !is_array($from) && ($uid ? $tokens[2][$uidKey] == $from : $tokens[0] == $from)) { + if ($to === null && !is_array($from) && ($uid === IMAP::ST_UID ? $tokens[2][$uidKey] == $from : $tokens[0] == $from)) { // we still need to read all lines while (!$this->readLine($response, $tokens, $tag)) return $response->setResult($data); } - if ($uid) { + if ($uid === IMAP::ST_UID) { $result[$tokens[2][$uidKey]] = $data; } else { $result[$tokens[0]] = $data; @@ -1226,7 +1226,7 @@ public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Resp $response = $this->getUid(); $ids = []; foreach ($response->data() as $msgn => $v) { - $id = $uid ? $v : $msgn; + $id = $uid === IMAP::ST_UID ? $v : $msgn; if (($to >= $id && $from <= $id) || ($to === "*" && $from <= $id)) { $ids[] = $id; } diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index de2ddb60..b515d255 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -253,7 +253,7 @@ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid $uids = is_array($uids) ? $uids : [$uids]; foreach ($uids as $id) { $response->addCommand("imap_fetchbody"); - $result[$id] = \imap_fetchbody($this->stream, $id, "", $uid ? IMAP::ST_UID : IMAP::NIL); + $result[$id] = \imap_fetchbody($this->stream, $id, "", $uid === IMAP::ST_UID ? IMAP::ST_UID : IMAP::NIL); } return $result; diff --git a/src/Folder.php b/src/Folder.php index af15476d..99b1e7bf 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -295,7 +295,7 @@ public function move(string $new_name, bool $expunge = true): array { public function overview(string $sequence = null): array { $this->client->openFolder($this->path); $sequence = $sequence === null ? "1:*" : $sequence; - $uid = ClientManager::get('options.sequence', IMAP::ST_MSGN) == IMAP::ST_UID; + $uid = ClientManager::get('options.sequence', IMAP::ST_MSGN); $response = $this->client->getConnection()->overview($sequence, $uid); return $response->validatedData(); } diff --git a/src/Message.php b/src/Message.php index 568f2a1e..af2a23da 100755 --- a/src/Message.php +++ b/src/Message.php @@ -495,7 +495,7 @@ public function getHTMLBody(): string { */ private function parseHeader(): void { $sequence_id = $this->getSequenceId(); - $headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID)->validatedData(); + $headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence)->validatedData(); if (!isset($headers[$sequence_id])) { throw new MessageHeaderFetchingException("no headers found", 0); } @@ -548,7 +548,7 @@ private function parseFlags(): void { $sequence_id = $this->getSequenceId(); try { - $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence === IMAP::ST_UID)->validatedData(); + $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageFlagException("flag could not be fetched", 0, $e); } @@ -578,7 +578,7 @@ public function parseBody(): Message { $sequence_id = $this->getSequenceId(); try { - $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence === IMAP::ST_UID)->validatedData(); + $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageContentFetchingException("failed to fetch content", 0); } @@ -1008,7 +1008,7 @@ public function copy(string $folder_path, bool $expunge = false): ?Message { $folder = $this->client->getFolderByPath($folder_path); $this->client->openFolder($this->folder_path); - if ($this->client->getConnection()->copyMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID)->validatedData()) { + if ($this->client->getConnection()->copyMessage($folder->path, $this->getSequenceId(), null, $this->sequence)->validatedData()) { return $this->fetchNewMail($folder, $next_uid, "copied", $expunge); } } @@ -1047,7 +1047,7 @@ public function move(string $folder_path, bool $expunge = false): ?Message { $folder = $this->client->getFolderByPath($folder_path); $this->client->openFolder($this->folder_path); - if ($this->client->getConnection()->moveMessage($folder->path, $this->getSequenceId(), null, $this->sequence === IMAP::ST_UID)->validatedData()) { + if ($this->client->getConnection()->moveMessage($folder->path, $this->getSequenceId(), null, $this->sequence)->validatedData()) { return $this->fetchNewMail($folder, $next_uid, "moved", $expunge); } } @@ -1172,7 +1172,7 @@ public function setFlag(array|string $flag): bool { $flag = "\\" . trim(is_array($flag) ? implode(" \\", $flag) : $flag); $sequence_id = $this->getSequenceId(); try { - $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "+", true, $this->sequence === IMAP::ST_UID)->validatedData(); + $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "+", true, $this->sequence)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageFlagException("flag could not be set", 0, $e); } @@ -1204,7 +1204,7 @@ public function unsetFlag(array|string $flag): bool { $flag = "\\" . trim(is_array($flag) ? implode(" \\", $flag) : $flag); $sequence_id = $this->getSequenceId(); try { - $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "-", true, $this->sequence === IMAP::ST_UID)->validatedData(); + $status = $this->client->getConnection()->store([$flag], $sequence_id, $sequence_id, "-", true, $this->sequence)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageFlagException("flag could not be removed", 0, $e); } From 56c2c5121e094489ac3b8e3fa555cc0d039a794b Mon Sep 17 00:00:00 2001 From: Daniel Castilla Date: Wed, 1 Mar 2023 02:21:48 +0100 Subject: [PATCH 009/203] Prevent infinite loop in ImapProtocol (#316) * Prevent infinite loop in ImapProtocol * Fix PHP TypeError --- src/Connection/Protocols/ImapProtocol.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 737f83ef..fc23c56f 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -112,7 +112,7 @@ protected function enableStartTls() { */ public function nextLine(Response $response): string { $line = ""; - while (($next_char = fread($this->stream, 1)) !== false && $next_char !== "\n") { + while (($next_char = fread($this->stream, 1)) !== false && !in_array($next_char, ["","\n"])) { $line .= $next_char; } if ($line === "" && $next_char === false) { @@ -147,7 +147,7 @@ protected function nextTaggedLine(Response $response, ?string &$tag): string { $line = $this->nextLine($response); list($tag, $line) = explode(' ', $line, 2); - return $line; + return $line ?? ''; } /** From 27446461b4340c459106e5b0950ba41d3d89b8b6 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 1 Mar 2023 02:24:19 +0100 Subject: [PATCH 010/203] Changelog updated --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01f3af17..a5c2543f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - More unique ID generation to prevent multiple attachments with same ID #363 (thanks @Guite) - Not all attachments are pushed to the collection #372 (thanks @AdrianKuriata) - Partial fix for #362 (allow search response to be empty) +- Unsafe usage of switch case. #354 #366 (thanks @shuergab) +- Fix use of ST_MSGN as sequence method #356 (thanks @gioid) +- Prevent infinite loop in ImapProtocol #316 (thanks @thin-k-design) ### Added - NaN From 4fdf19bab8271940b85d31419ffcfd4359dd385d Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 1 Mar 2023 02:26:21 +0100 Subject: [PATCH 011/203] Release information added --- CHANGELOG.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5c2543f..b5035414 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- More unique ID generation to prevent multiple attachments with same ID #363 (thanks @Guite) -- Not all attachments are pushed to the collection #372 (thanks @AdrianKuriata) -- Partial fix for #362 (allow search response to be empty) -- Unsafe usage of switch case. #354 #366 (thanks @shuergab) -- Fix use of ST_MSGN as sequence method #356 (thanks @gioid) -- Prevent infinite loop in ImapProtocol #316 (thanks @thin-k-design) +- NaN ### Added - NaN @@ -20,6 +15,16 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN +## [5.0.1] - 2023-03-01 +### Fixed +- More unique ID generation to prevent multiple attachments with same ID #363 (thanks @Guite) +- Not all attachments are pushed to the collection #372 (thanks @AdrianKuriata) +- Partial fix for #362 (allow search response to be empty) +- Unsafe usage of switch case. #354 #366 (thanks @shuergab) +- Fix use of ST_MSGN as sequence method #356 (thanks @gioid) +- Prevent infinite loop in ImapProtocol #316 (thanks @thin-k-design) + + ## [5.0.0] - 2023-01-18 ### Fixed - The message uid and message number will only be fetched if accessed and wasn't previously set #326 #285 (thanks @szymekjanaczek) From 72f0868b4a1decafb168cf3ffd452bf5047eadab Mon Sep 17 00:00:00 2001 From: didi1357 Date: Wed, 1 Mar 2023 23:10:21 +0100 Subject: [PATCH 012/203] Add sizes() methods to protocol interface and implmentations (#379) * Add retrieval of message size using FETCH RFC822.SIZE * Replace misleading while with if * Only fetch size if explicitly requested --- CHANGELOG.md | 2 +- examples/size_test.php | 83 +++++++++++++++++++ src/Connection/Protocols/ImapProtocol.php | 15 +++- src/Connection/Protocols/LegacyProtocol.php | 28 +++++++ .../Protocols/ProtocolInterface.php | 11 +++ .../MessageSizeFetchingException.php | 24 ++++++ src/Message.php | 30 ++++++- 7 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 examples/size_test.php create mode 100644 src/Exceptions/MessageSizeFetchingException.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b5035414..62ea0d92 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Added -- NaN +- sizes() methods to protocol interface and implmentations (to have FETCH NNN RFC822.SIZE attribute available) ### Breaking changes - NaN diff --git a/examples/size_test.php b/examples/size_test.php new file mode 100644 index 00000000..44f832c5 --- /dev/null +++ b/examples/size_test.php @@ -0,0 +1,83 @@ + 'toFill', + 'port' => 993, + 'encryption' => 'ssl', + 'validate_cert' => true, + 'username' => 'toFill', + 'password' => 'toFill', + 'protocol' => 'imap' +); +set_include_path('/home/didi1357/git/php-imap/src'); + + +ini_set('display_errors', 1); +ini_set('display_startup_errors', 1); +error_reporting(E_ALL); + +require_once 'IMAP.php'; +require_once 'Traits/HasEvents.php'; +require_once 'Exceptions/MaskNotFoundException.php'; +require_once 'Client.php'; +require_once 'ClientManager.php'; +require_once 'Support/Masks/Mask.php'; +require_once 'Support/Masks/MessageMask.php'; +require_once 'Support/Masks/AttachmentMask.php'; +require_once 'Connection/Protocols/Response.php'; +require_once 'Connection/Protocols/ProtocolInterface.php'; +require_once 'Connection/Protocols/Protocol.php'; +require_once 'Connection/Protocols/ImapProtocol.php'; +require_once '../tests/vendor/autoload.php'; +require_once 'Support/PaginatedCollection.php'; +require_once 'Support/FolderCollection.php'; +require_once 'Folder.php'; +require_once 'Exceptions/ResponseException.php'; +require_once 'Query/Query.php'; +require_once 'Query/WhereQuery.php'; +require_once 'Support/MessageCollection.php'; +require_once 'Support/FlagCollection.php'; +require_once 'Support/AttachmentCollection.php'; +require_once 'Part.php'; +require_once 'Structure.php'; +require_once 'Attribute.php'; +require_once 'Address.php'; +require_once 'EncodingAliases.php'; +require_once 'Header.php'; +require_once 'Message.php'; + +use Webklex\PHPIMAP\ClientManager; +use Webklex\PHPIMAP\Support\Masks\MessageMask; +use Webklex\PHPIMAP\Support\Masks\AttachmentMask; + +$CONFIG['masks'] = array( + 'message' => MessageMask::class, + 'attachment' => AttachmentMask::class +); +echo "
";
+$cm = new ClientManager($options = []);
+$client = $cm->make($CONFIG)->connect();
+//$client->getConnection()->enableDebug(); // uncomment this for debug output!
+$folder = $client->getFolderByPath('INBOX');
+//$message = $folder->messages()->getMessageByMsgn(1); // did not work as msgn implementation is currently broken!
+$message = $folder->messages()->getMessageByUid(2);
+var_dump($message->getSize());
+
+
+?>
+
diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index fc23c56f..07ea3f6d 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -698,7 +698,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in
             // if we want only one message we can ignore everything else and just return
             if ($to === null && !is_array($from) && ($uid === IMAP::ST_UID ? $tokens[2][$uidKey] == $from : $tokens[0] == $from)) {
                 // we still need to read all lines
-                while (!$this->readLine($response, $tokens, $tag))
+                if (!$this->readLine($response, $tokens, $tag))
                     return $response->setResult($data);
             }
             if ($uid === IMAP::ST_UID) {
@@ -756,6 +756,19 @@ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response
         return $this->fetch(["FLAGS"], $uids, null, $uid);
     }
 
+    /**
+     * Fetch message sizes
+     * @param int|array $uids
+     * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
+     * message numbers instead.
+     *
+     * @return Response
+     * @throws RuntimeException
+     */
+    public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response {
+        return $this->fetch(["RFC822.SIZE"], $uids, null, $uid);
+    }
+
     /**
      * Get uid for a given id
      * @param int|null $id message number
diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php
index b515d255..65c5342d 100644
--- a/src/Connection/Protocols/LegacyProtocol.php
+++ b/src/Connection/Protocols/LegacyProtocol.php
@@ -314,6 +314,34 @@ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response
             return $result;
         });
     }
+    
+    /**
+     * Fetch message sizes
+     * @param int|array $uids
+     * @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers.
+     *
+     * @return Response
+     */
+    public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response {
+        return $this->response()->wrap(function($response)use($uids, $uid){
+            /** @var Response $response */
+            $result = [];
+            $uids = is_array($uids) ? $uids : [$uids];
+            $uid_text = implode("','",$uids);
+            $response->addCommand("imap_fetch_overview");
+            $raw_overview = false;
+            if ($uid == IMAP::ST_UID)
+              $raw_overview = imap_fetch_overview($this->stream, $uid_text, FT_UID);
+            else
+              $raw_overview = imap_fetch_overview($this->stream, $uid_text);
+            if ($raw_overview !== false) {
+              foreach ($raw_overview as $overview_element) {
+                $result[$overview_element[$uid == IMAP::ST_UID ? 'uid': 'msgno']] = $overview_element['size'];
+              }
+            }
+            return $result;
+        });
+    }
 
     /**
      * Get uid for a given id
diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php
index 0093e513..c02d7800 100644
--- a/src/Connection/Protocols/ProtocolInterface.php
+++ b/src/Connection/Protocols/ProtocolInterface.php
@@ -151,6 +151,17 @@ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid
      * @throws RuntimeException
      */
     public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response;
+    
+    /**
+     * Fetch message sizes
+     * @param int|array $uids
+     * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use
+     * message numbers instead.
+     *
+     * @return Response
+     * @throws RuntimeException
+     */
+    public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response;
 
     /**
      * Get uid for a given id
diff --git a/src/Exceptions/MessageSizeFetchingException.php b/src/Exceptions/MessageSizeFetchingException.php
new file mode 100644
index 00000000..f14fce51
--- /dev/null
+++ b/src/Exceptions/MessageSizeFetchingException.php
@@ -0,0 +1,24 @@
+attributes[$name] = $this->client->getConnection()->getMessageNumber($this->uid)->validate()->integer();
                 return $this->attributes[$name];
+            case "size":
+                if (!isset($this->attributes[$name])) {
+                    $this->fetchSize();
+                }
+                return $this->attributes[$name];
         }
 
         return $this->header->get($name);
@@ -592,6 +597,20 @@ public function parseBody(): Message {
 
         return $body;
     }
+    
+    /**
+     * Fetches the size for this message.
+     * 
+     * @throws MessageSizeFetchingException
+     */
+    private function fetchSize(): void {
+        $sequence_id = $this->getSequenceId();
+        $sizes = $this->client->getConnection()->sizes([$sequence_id], $this->sequence)->validatedData();
+         if (!isset($sizes[$sequence_id])) {
+            throw new MessageSizeFetchingException("sizes did not set an array entry for the supplied sequence_id", 0);
+        }
+        $this->attributes["size"] = $sizes[$sequence_id];
+    }
 
     /**
      * Handle auto "Seen" flag handling
@@ -1301,6 +1320,15 @@ public function getHeader(): ?Header {
         return $this->header;
     }
 
+    /**
+     * Get the message size in bytes
+     *
+     * @return int Size of the message in bytes
+     */
+    public function getSize(): int {
+        return $this->get("size");
+    }
+
     /**
      * Get the current client
      *
@@ -1565,7 +1593,7 @@ public function getSequence(): int {
     }
 
     /**
-     * Set the sequence type
+     * Get the current sequence id (either a UID or a message number!)
      *
      * @return int
      */

From 759c65b5d0ad02491d1bacc83507d5e18624d439 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 1 Mar 2023 23:46:27 +0100
Subject: [PATCH 013/203] Changelog updated

---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62ea0d92..3d81348a 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
 - NaN
 
 ### Added
-- sizes() methods to protocol interface and implmentations (to have FETCH NNN RFC822.SIZE attribute available)
+- `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357)
 
 ### Breaking changes
 - NaN

From 377214ec064c358505335c02a623932d06085faa Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 1 Mar 2023 23:46:54 +0100
Subject: [PATCH 014/203] Code formatted

---
 src/Connection/Protocols/LegacyProtocol.php |  4 ++--
 src/Message.php                             | 25 ++++++++++++---------
 2 files changed, 16 insertions(+), 13 deletions(-)

diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php
index 65c5342d..6eedddef 100644
--- a/src/Connection/Protocols/LegacyProtocol.php
+++ b/src/Connection/Protocols/LegacyProtocol.php
@@ -331,9 +331,9 @@ public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response
             $response->addCommand("imap_fetch_overview");
             $raw_overview = false;
             if ($uid == IMAP::ST_UID)
-              $raw_overview = imap_fetch_overview($this->stream, $uid_text, FT_UID);
+              $raw_overview = \imap_fetch_overview($this->stream, $uid_text, IMAP::FT_UID);
             else
-              $raw_overview = imap_fetch_overview($this->stream, $uid_text);
+              $raw_overview = \imap_fetch_overview($this->stream, $uid_text);
             if ($raw_overview !== false) {
               foreach ($raw_overview as $overview_element) {
                 $result[$overview_element[$uid == IMAP::ST_UID ? 'uid': 'msgno']] = $overview_element['size'];
diff --git a/src/Message.php b/src/Message.php
index 33c66161..97f34a5d 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -27,6 +27,7 @@
 use Webklex\PHPIMAP\Exceptions\MessageFlagException;
 use Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException;
 use Webklex\PHPIMAP\Exceptions\MessageNotFoundException;
+use Webklex\PHPIMAP\Exceptions\MessageSizeFetchingException;
 use Webklex\PHPIMAP\Exceptions\MethodNotFoundException;
 use Webklex\PHPIMAP\Exceptions\ResponseException;
 use Webklex\PHPIMAP\Exceptions\RuntimeException;
@@ -45,6 +46,7 @@
  * @property integer msglist
  * @property integer uid
  * @property integer msgn
+ * @property integer size
  * @property Attribute subject
  * @property Attribute message_id
  * @property Attribute message_no
@@ -62,6 +64,7 @@
  * @method integer setMsglist($msglist)
  * @method integer getUid()
  * @method integer getMsgn()
+ * @method integer getSize()
  * @method Attribute getPriority()
  * @method Attribute getSubject()
  * @method Attribute getMessageId()
@@ -358,6 +361,7 @@ public function boot(): void {
      * @throws ImapServerErrorException
      * @throws MessageNotFoundException
      * @throws MethodNotFoundException
+     * @throws MessageSizeFetchingException
      * @throws RuntimeException
      * @throws ResponseException
      */
@@ -400,6 +404,7 @@ public function __set($name, $value) {
      * @throws ImapBadRequestException
      * @throws ImapServerErrorException
      * @throws MessageNotFoundException
+     * @throws MessageSizeFetchingException
      * @throws RuntimeException
      * @throws ResponseException
      */
@@ -419,6 +424,7 @@ public function __get($name) {
      * @throws MessageNotFoundException
      * @throws RuntimeException
      * @throws ResponseException
+     * @throws MessageSizeFetchingException
      */
     public function get($name): mixed {
         if (isset($this->attributes[$name]) && $this->attributes[$name] !== null) {
@@ -597,11 +603,17 @@ public function parseBody(): Message {
 
         return $body;
     }
-    
+
     /**
      * Fetches the size for this message.
-     * 
+     *
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
      * @throws MessageSizeFetchingException
+     * @throws ResponseException
+     * @throws RuntimeException
      */
     private function fetchSize(): void {
         $sequence_id = $this->getSequenceId();
@@ -1320,15 +1332,6 @@ public function getHeader(): ?Header {
         return $this->header;
     }
 
-    /**
-     * Get the message size in bytes
-     *
-     * @return int Size of the message in bytes
-     */
-    public function getSize(): int {
-        return $this->get("size");
-    }
-
     /**
      * Get the current client
      *

From cccd7c6d3beb737cd56dd2a0b3e0fdd64f180f1c Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 1 Mar 2023 23:48:29 +0100
Subject: [PATCH 015/203] Live mailbox environment variables added

---
 phpunit.xml.dist | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 6caa954a..6ec96954 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -20,6 +20,12 @@
     
   
   
-    
+    
+    
+    
+    
+    
+    
+    
   
 

From ec97dc5f244ab674e451c0601ccd27d680e052eb Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 1 Mar 2023 23:50:15 +0100
Subject: [PATCH 016/203] Example unit test for #379 added

---
 examples/size_test.php        | 83 -----------------------------------
 tests/issues/Issue379Test.php | 55 +++++++++++++++++++++++
 2 files changed, 55 insertions(+), 83 deletions(-)
 delete mode 100644 examples/size_test.php
 create mode 100644 tests/issues/Issue379Test.php

diff --git a/examples/size_test.php b/examples/size_test.php
deleted file mode 100644
index 44f832c5..00000000
--- a/examples/size_test.php
+++ /dev/null
@@ -1,83 +0,0 @@
- 'toFill',
-  'port'          => 993,
-  'encryption'    => 'ssl',
-  'validate_cert' => true,
-  'username'      => 'toFill',
-  'password'      => 'toFill',
-  'protocol'      => 'imap'
-);
-set_include_path('/home/didi1357/git/php-imap/src');
-
-
-ini_set('display_errors', 1);
-ini_set('display_startup_errors', 1);
-error_reporting(E_ALL);
-
-require_once 'IMAP.php';
-require_once 'Traits/HasEvents.php';
-require_once 'Exceptions/MaskNotFoundException.php';
-require_once 'Client.php';
-require_once 'ClientManager.php';
-require_once 'Support/Masks/Mask.php';
-require_once 'Support/Masks/MessageMask.php';
-require_once 'Support/Masks/AttachmentMask.php';
-require_once 'Connection/Protocols/Response.php';
-require_once 'Connection/Protocols/ProtocolInterface.php';
-require_once 'Connection/Protocols/Protocol.php';
-require_once 'Connection/Protocols/ImapProtocol.php';
-require_once '../tests/vendor/autoload.php';
-require_once 'Support/PaginatedCollection.php';
-require_once 'Support/FolderCollection.php';
-require_once 'Folder.php';
-require_once 'Exceptions/ResponseException.php';
-require_once 'Query/Query.php';
-require_once 'Query/WhereQuery.php';
-require_once 'Support/MessageCollection.php';
-require_once 'Support/FlagCollection.php';
-require_once 'Support/AttachmentCollection.php';
-require_once 'Part.php';
-require_once 'Structure.php';
-require_once 'Attribute.php';
-require_once 'Address.php';
-require_once 'EncodingAliases.php';
-require_once 'Header.php';
-require_once 'Message.php';
-
-use Webklex\PHPIMAP\ClientManager;
-use Webklex\PHPIMAP\Support\Masks\MessageMask;
-use Webklex\PHPIMAP\Support\Masks\AttachmentMask;
-
-$CONFIG['masks'] = array(
-  'message' => MessageMask::class,
-  'attachment' => AttachmentMask::class
-);
-echo "
";
-$cm = new ClientManager($options = []);
-$client = $cm->make($CONFIG)->connect();
-//$client->getConnection()->enableDebug(); // uncomment this for debug output!
-$folder = $client->getFolderByPath('INBOX');
-//$message = $folder->messages()->getMessageByMsgn(1); // did not work as msgn implementation is currently broken!
-$message = $folder->messages()->getMessageByUid(2);
-var_dump($message->getSize());
-
-
-?>
-
diff --git a/tests/issues/Issue379Test.php b/tests/issues/Issue379Test.php
new file mode 100644
index 00000000..c2394891
--- /dev/null
+++ b/tests/issues/Issue379Test.php
@@ -0,0 +1,55 @@
+markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
+        }
+        $cm = new ClientManager([
+            'accounts' => [
+                'default' => [
+                    'host'           => $_ENV["LIVE_MAILBOX_HOST"] ?? "localhost",
+                    'port'           => $_ENV["LIVE_MAILBOX_PORT"] ?? 143,
+                    'protocol'       => 'imap', //might also use imap, [pop3 or nntp (untested)]
+                    'encryption'     => $_ENV["LIVE_MAILBOX_ENCRYPTION"] ?? false, // Supported: false, 'ssl', 'tls'
+                    'validate_cert'  => $_ENV["LIVE_MAILBOX_VALIDATE_CERT"] ?? false,
+                    'username'       => $_ENV["LIVE_MAILBOX_USERNAME"] ?? "root@example.com",
+                    'password'       => $_ENV["LIVE_MAILBOX_PASSWORD"] ?? "foobar",
+                ],
+            ],
+        ]);
+
+        /** @var Client $client */
+        $client = $cm->account('default');
+        $this->assertNotNull($client);
+
+        //Connect to the IMAP Server
+        $client->connect();
+
+        $folder = $client->getFolderByPath('INBOX');
+        $this->assertNotNull($folder);
+
+        $message = $folder->messages()->getMessageByUid(2);
+        $this->assertNotNull($message);
+
+        $this->assertEquals(127, $message->getSize());
+    }
+
+}
\ No newline at end of file

From f1e3f83ead955c4900109ccfec817b5065fc1c5e Mon Sep 17 00:00:00 2001
From: webklex 
Date: Mon, 6 Mar 2023 19:54:49 +0100
Subject: [PATCH 017/203] Live mailbox testing support added

---
 .github/docker/Dockerfile       |  23 ++++++
 .github/docker/dovecot_setup.sh |  63 +++++++++++++++++
 .github/workflows/tests.yaml    |  12 ++++
 README.md                       |  55 +++++++++++++++
 phpunit.xml.dist                |   8 ++-
 tests/LiveMailboxTest.php       |  49 +++++++++++++
 tests/LiveMailboxTestCase.php   | 120 ++++++++++++++++++++++++++++++++
 tests/issues/Issue379Test.php   |  31 +++------
 tests/messages/plain.eml        |   9 +++
 9 files changed, 345 insertions(+), 25 deletions(-)
 create mode 100644 .github/docker/Dockerfile
 create mode 100644 .github/docker/dovecot_setup.sh
 create mode 100644 tests/LiveMailboxTest.php
 create mode 100644 tests/LiveMailboxTestCase.php
 create mode 100644 tests/messages/plain.eml

diff --git a/.github/docker/Dockerfile b/.github/docker/Dockerfile
new file mode 100644
index 00000000..ff0f11e6
--- /dev/null
+++ b/.github/docker/Dockerfile
@@ -0,0 +1,23 @@
+FROM ubuntu:latest
+LABEL maintainer="Webklex "
+
+RUN apt-get update
+RUN apt-get upgrade -y
+RUN apt-get install -y sudo dovecot-imapd
+
+ENV LIVE_MAILBOX=true
+ENV LIVE_MAILBOX_HOST=mail.example.local
+ENV LIVE_MAILBOX_PORT=993
+ENV LIVE_MAILBOX_ENCRYPTION=ssl
+ENV LIVE_MAILBOX_VALIDATE_CERT=true
+ENV LIVE_MAILBOX_USERNAME=root@example.local
+ENV LIVE_MAILBOX_PASSWORD=foobar
+ENV LIVE_MAILBOX_QUOTA_SUPPORT=true
+
+EXPOSE 993
+
+ADD dovecot_setup.sh /root/dovecot_setup.sh
+RUN chmod +x /root/dovecot_setup.sh
+
+CMD ["/bin/bash", "-c", "/root/dovecot_setup.sh && tail -f /dev/null"]
+
diff --git a/.github/docker/dovecot_setup.sh b/.github/docker/dovecot_setup.sh
new file mode 100644
index 00000000..25a51928
--- /dev/null
+++ b/.github/docker/dovecot_setup.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+set -ex
+
+sudo apt-get -q update
+sudo apt-get -q -y install dovecot-imapd
+
+{
+    echo "127.0.0.1 $LIVE_MAILBOX_HOST"
+} | sudo tee -a /etc/hosts
+
+SSL_CERT="/etc/ssl/certs/dovecot.crt"
+SSL_KEY="/etc/ssl/private/dovecot.key"
+
+sudo openssl req -new -x509 -days 3 -nodes \
+    -out "$SSL_CERT" \
+    -keyout "$SSL_KEY" \
+    -subj "/C=EU/ST=Europe/L=Home/O=Webklex/OU=Webklex DEV/CN=""$LIVE_MAILBOX_HOST"
+
+sudo chown root:dovecot "$SSL_CERT" "$SSL_KEY"
+sudo chmod 0440 "$SSL_CERT"
+sudo chmod 0400 "$SSL_KEY"
+
+DOVECOT_CONF="/etc/dovecot/local.conf"
+MAIL_CONF="/etc/dovecot/conf.d/10-mail.conf"
+IMAP_CONF="/etc/dovecot/conf.d/20-imap.conf"
+QUOTA_CONF="/etc/dovecot/conf.d/90-quota.conf"
+sudo touch "$DOVECOT_CONF" "$MAIL_CONF" "$IMAP_CONF" "$QUOTA_CONF"
+sudo chown root:dovecot "$DOVECOT_CONF" "$MAIL_CONF" "$IMAP_CONF" "$QUOTA_CONF"
+sudo chmod 0640 "$DOVECOT_CONF" "$MAIL_CONF" "$IMAP_CONF" "$QUOTA_CONF"
+
+{
+    echo "ssl = required"
+    echo "disable_plaintext_auth = yes"
+    echo "ssl_cert = <""$SSL_CERT"
+    echo "ssl_key = <""$SSL_KEY"
+    echo "ssl_protocols = !SSLv2 !SSLv3"
+    echo "ssl_cipher_list = AES128+EECDH:AES128+EDH"
+} | sudo tee -a "$DOVECOT_CONF"
+
+{
+    echo "mail_plugins = \$mail_plugins quota"
+} | sudo tee -a "$MAIL_CONF"
+
+{
+    echo "protocol imap {"
+    echo "  mail_plugins = \$mail_plugins imap_quota"
+    echo "}"
+} | sudo tee -a "$IMAP_CONF"
+
+{
+    echo "plugin {"
+    echo "  quota = maildir:User quota"
+    echo "  quota_rule = *:storage=1G"
+    echo "}"
+} | sudo tee -a "$QUOTA_CONF"
+
+sudo useradd --create-home --shell /bin/false "$LIVE_MAILBOX_USERNAME"
+echo "$LIVE_MAILBOX_USERNAME"":""$LIVE_MAILBOX_PASSWORD" | sudo chpasswd
+
+sudo service dovecot restart
+
+sudo doveadm auth test -x service=imap "$LIVE_MAILBOX_USERNAME" "$LIVE_MAILBOX_PASSWORD"
\ No newline at end of file
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index d4146065..b441efd3 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -20,6 +20,16 @@ jobs:
 
     name: PHP ${{ matrix.php }}
 
+    env:
+      LIVE_MAILBOX: true
+      LIVE_MAILBOX_DEBUG: true
+      LIVE_MAILBOX_HOST: mail.example.local
+      LIVE_MAILBOX_PORT: 993
+      LIVE_MAILBOX_USERNAME: root@example.local
+      LIVE_MAILBOX_ENCRYPTION: ssl
+      LIVE_MAILBOX_PASSWORD: foobar
+      LIVE_MAILBOX_QUOTA_SUPPORT: true
+
     steps:
       - name: Checkout code
         uses: actions/checkout@v3
@@ -34,5 +44,7 @@ jobs:
       - name: Install Composer dependencies
         run: composer install --prefer-dist --no-interaction --no-progress
 
+      - run: "sh .github/docker/dovecot_setup.sh"
+
       - name: Execute tests
         run: vendor/bin/phpunit
\ No newline at end of file
diff --git a/README.md b/README.md
index 60bfa12a..912b46ce 100755
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ Discord: [discord.gg/rd4cN9h6][link-discord]
 - [Documentations](#documentations)
 - [Compatibility](#compatibility)
 - [Basic usage example](#basic-usage-example)
+- [Testing](#testing)
 - [Known issues](#known-issues)
 - [Support](#support)
 - [Features & pull requests](#features--pull-requests)
@@ -97,6 +98,60 @@ foreach($folders as $folder){
 ```
 
 
+## Testing
+To run the tests, please execute the following command:
+```bash
+composer test
+```
+
+### Quick-Test / Static Test
+To disable all test which require a live mailbox, please copy the `phpunit.xml.dist` to `phpunit.xml` and adjust the configuration:
+```xml
+
+    
+
+```
+
+### Full-Test / Live Mailbox Test
+To run all tests, you need to provide a valid imap configuration.
+
+To provide a valid imap configuration, please copy the `phpunit.xml.dist` to `phpunit.xml` and adjust the configuration:
+```xml
+
+    
+    
+    
+    
+    
+    
+    
+    
+    
+
+```
+
+The test account should **not** contain any important data, as it will be deleted during the test.
+Furthermore, the test account should be able to create new folders, move messages and should **not** be used by any other
+application during the test.
+
+It's recommended to use a dedicated test account for this purpose. You can use the provided `Dockerfile` to create an imap server used for testing purposes.
+
+Build the docker image:
+```bash
+cd .github/docker
+
+docker build -t php-imap-server .
+```
+Run the docker image:
+```bash
+docker run --name imap-server -p 993:993 -it --rm -d php-imap-server
+```
+Stop the docker image:
+```bash
+docker stop imap-server
+```
+
+
 ### Known issues
 | Error                                                                      | Solution                                                                                |
 |:---------------------------------------------------------------------------|:----------------------------------------------------------------------------------------|
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 6ec96954..1f9bd48c 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -21,11 +21,13 @@
   
   
     
-    
-    
+    
+    
+    
     
+    
     
-    
+    
     
   
 
diff --git a/tests/LiveMailboxTest.php b/tests/LiveMailboxTest.php
new file mode 100644
index 00000000..ae484640
--- /dev/null
+++ b/tests/LiveMailboxTest.php
@@ -0,0 +1,49 @@
+getClient();
+        $client->connect();
+
+        self::assertTrue($client->isConnected());
+    }
+
+}
\ No newline at end of file
diff --git a/tests/LiveMailboxTestCase.php b/tests/LiveMailboxTestCase.php
new file mode 100644
index 00000000..e56da046
--- /dev/null
+++ b/tests/LiveMailboxTestCase.php
@@ -0,0 +1,120 @@
+-@#[]_ß_б_π_€_✔_你_يد_Z_';
+
+    /**
+     * Client manager
+     * @var ClientManager $manager
+     */
+    protected static ClientManager $manager;
+
+    /**
+     * Get the client manager
+     *
+     * @return ClientManager
+     */
+    final protected function getManager(): ClientManager {
+        if (!isset(self::$manager)) {
+            self::$manager = new ClientManager([
+                'options' => [
+                    "debug" => $_ENV["LIVE_MAILBOX_DEBUG"] ?? false,
+                ],
+                'accounts' => [
+                    'default' => [
+                        'host'          => getenv("LIVE_MAILBOX_HOST"),
+                        'port'          => getenv("LIVE_MAILBOX_PORT"),
+                        'encryption'    => getenv("LIVE_MAILBOX_ENCRYPTION"),
+                        'validate_cert' => getenv("LIVE_MAILBOX_VALIDATE_CERT"),
+                        'username'      => getenv("LIVE_MAILBOX_USERNAME"),
+                        'password'      => getenv("LIVE_MAILBOX_PASSWORD"),
+                        'protocol'      => 'imap', //might also use imap, [pop3 or nntp (untested)]
+                    ],
+                ],
+            ]);
+        }
+        return self::$manager;
+    }
+
+    /**
+     * Get the client
+     *
+     * @return Client
+     * @throws MaskNotFoundException
+     */
+    final protected function getClient(): Client {
+        return $this->getManager()->account('default');
+    }
+
+    final protected function getSpecialChars(): string {
+        return self::SPECIAL_CHARS;
+    }
+
+    /**
+     * Append a message to a folder
+     * @param Folder $folder
+     * @param string $message
+     *
+     * @return Message
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    final protected function appendMessage(Folder $folder, string $message): Message {
+        $response = $folder->appendMessage($message);
+
+        if (!isset($response[0])) {
+            $this->fail("No message ID returned");
+        }
+        $test = explode(' ', $response[0]);
+        if (!isset($test[3])) {
+            $this->fail("No message ID returned");
+        }
+
+        $id = substr($test[3], 0, -1);
+        return $folder->messages()->getMessageByUid(intval($id));
+    }
+}
\ No newline at end of file
diff --git a/tests/issues/Issue379Test.php b/tests/issues/Issue379Test.php
index c2394891..9c942960 100644
--- a/tests/issues/Issue379Test.php
+++ b/tests/issues/Issue379Test.php
@@ -12,32 +12,16 @@
 
 namespace Tests\issues;
 
-use PHPUnit\Framework\TestCase;
-use Webklex\PHPIMAP\Client;
-use Webklex\PHPIMAP\ClientManager;
+use Tests\LiveMailboxTestCase;
 
-class Issue379Test extends TestCase {
+class Issue379Test extends LiveMailboxTestCase {
 
     public function testIssue() {
         if (!$_ENV["LIVE_MAILBOX"] ?? false) {
             $this->markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
         }
-        $cm = new ClientManager([
-            'accounts' => [
-                'default' => [
-                    'host'           => $_ENV["LIVE_MAILBOX_HOST"] ?? "localhost",
-                    'port'           => $_ENV["LIVE_MAILBOX_PORT"] ?? 143,
-                    'protocol'       => 'imap', //might also use imap, [pop3 or nntp (untested)]
-                    'encryption'     => $_ENV["LIVE_MAILBOX_ENCRYPTION"] ?? false, // Supported: false, 'ssl', 'tls'
-                    'validate_cert'  => $_ENV["LIVE_MAILBOX_VALIDATE_CERT"] ?? false,
-                    'username'       => $_ENV["LIVE_MAILBOX_USERNAME"] ?? "root@example.com",
-                    'password'       => $_ENV["LIVE_MAILBOX_PASSWORD"] ?? "foobar",
-                ],
-            ],
-        ]);
-
-        /** @var Client $client */
-        $client = $cm->account('default');
+
+        $client = $this->getClient();
         $this->assertNotNull($client);
 
         //Connect to the IMAP Server
@@ -46,10 +30,13 @@ public function testIssue() {
         $folder = $client->getFolderByPath('INBOX');
         $this->assertNotNull($folder);
 
-        $message = $folder->messages()->getMessageByUid(2);
+        $content = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "plain.eml"]));
+        $message = $this->appendMessage($folder, $content);
         $this->assertNotNull($message);
 
-        $this->assertEquals(127, $message->getSize());
+        $this->assertEquals(214, $message->getSize());
+
+        $this->assertEquals(true, $message->delete(true));
     }
 
 }
\ No newline at end of file
diff --git a/tests/messages/plain.eml b/tests/messages/plain.eml
new file mode 100644
index 00000000..4d3d22b1
--- /dev/null
+++ b/tests/messages/plain.eml
@@ -0,0 +1,9 @@
+From: from@someone.com
+To: to@someone-else.com
+Subject: Example
+Date: Mon, 21 Jan 2023 19:36:45 +0200
+Content-Type: text/plain;
+	charset=\"us-ascii\"
+Content-Transfer-Encoding: quoted-printable
+
+Hi there!
\ No newline at end of file

From 94e5f83a133ee7ee57176971763ab33b7e78a9f7 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Mon, 6 Mar 2023 22:15:18 +0100
Subject: [PATCH 018/203] interactive docker flag removed

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 912b46ce..2fb9c13a 100755
--- a/README.md
+++ b/README.md
@@ -144,7 +144,7 @@ docker build -t php-imap-server .
 ```
 Run the docker image:
 ```bash
-docker run --name imap-server -p 993:993 -it --rm -d php-imap-server
+docker run --name imap-server -p 993:993 --rm -d php-imap-server
 ```
 Stop the docker image:
 ```bash

From 9491e4668a517a538114fe8784085053e8466034 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Mon, 6 Mar 2023 22:16:23 +0100
Subject: [PATCH 019/203] Extended UTF-7 support added (RFC2060) #383

---
 CHANGELOG.md                              |   2 +-
 src/Client.php                            |   1 +
 src/Connection/Protocols/ImapProtocol.php |  12 +++
 src/EncodingAliases.php                   | 101 ++++++++++++++++++-
 src/Folder.php                            |   9 +-
 src/Header.php                            |  34 +------
 tests/LiveMailboxTestCase.php             | 116 ++++++++++++++++++++--
 tests/issues/Issue379Test.php             |  52 +++++++---
 tests/issues/Issue383Test.php             |  67 +++++++++++++
 9 files changed, 334 insertions(+), 60 deletions(-)
 create mode 100644 tests/issues/Issue383Test.php

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d81348a..553b8781 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
 
 ## [UNRELEASED]
 ### Fixed
-- NaN
+- Extended UTF-7 support added (RFC2060) #383
 
 ### Added
 - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357)
diff --git a/src/Client.php b/src/Client.php
index f5fa4909..893284f8 100755
--- a/src/Client.php
+++ b/src/Client.php
@@ -546,6 +546,7 @@ public function getFolderByName($folder_name): ?Folder {
      * @throws ResponseException
      */
     public function getFolderByPath($folder_path): ?Folder {
+        $folder_path = EncodingAliases::convert($folder_path, "", "UTF7-IMAP");
         return $this->getFolders(false)->where("path", $folder_path)->first();
     }
 
diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index 07ea3f6d..db0bb216 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -13,6 +13,7 @@
 namespace Webklex\PHPIMAP\Connection\Protocols;
 
 use Exception;
+use Webklex\PHPIMAP\EncodingAliases;
 use Webklex\PHPIMAP\Exceptions\AuthFailedException;
 use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
 use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
@@ -588,6 +589,7 @@ public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'I
     public function selectFolder(string $folder = 'INBOX'): Response {
         $this->uid_cache = [];
 
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->examineOrSelect('SELECT', $folder);
     }
 
@@ -599,6 +601,7 @@ public function selectFolder(string $folder = 'INBOX'): Response {
      * @throws RuntimeException
      */
     public function examineFolder(string $folder = 'INBOX'): Response {
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->examineOrSelect('EXAMINE', $folder);
     }
 
@@ -911,6 +914,7 @@ public function store(
      * @throws RuntimeException
      */
     public function appendMessage(string $folder, string $message, array $flags = null, string $date = null): Response {
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         $tokens = [];
         $tokens[] = $this->escapeString($folder);
         if ($flags !== null) {
@@ -990,6 +994,7 @@ public function moveMessage(string $folder, $from, int $to = null, int|string $u
         $set = $this->buildSet($from, $to);
         $command = $this->buildUIDCommand("MOVE", $uid);
 
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true);
     }
 
@@ -1009,6 +1014,7 @@ public function moveMessage(string $folder, $from, int $to = null, int|string $u
      */
     public function moveManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response {
         $command = $this->buildUIDCommand("MOVE", $uid);
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
 
         $set = implode(',', $messages);
         $tokens = [$set, $this->escapeString($folder)];
@@ -1051,6 +1057,7 @@ public function ID($ids = null): Response {
      * @throws RuntimeException
      */
     public function createFolder(string $folder): Response {
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('CREATE', [$this->escapeString($folder)], true);
     }
 
@@ -1067,6 +1074,8 @@ public function createFolder(string $folder): Response {
      * @throws RuntimeException
      */
     public function renameFolder(string $old, string $new): Response {
+        $old = EncodingAliases::convert($old, "", "UTF7-IMAP");
+        $new = EncodingAliases::convert($new, "", "UTF7-IMAP");
         return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true);
     }
 
@@ -1081,6 +1090,7 @@ public function renameFolder(string $old, string $new): Response {
      * @throws RuntimeException
      */
     public function deleteFolder(string $folder): Response {
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true);
     }
 
@@ -1095,6 +1105,7 @@ public function deleteFolder(string $folder): Response {
      * @throws RuntimeException
      */
     public function subscribeFolder(string $folder): Response {
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true);
     }
 
@@ -1109,6 +1120,7 @@ public function subscribeFolder(string $folder): Response {
      * @throws RuntimeException
      */
     public function unsubscribeFolder(string $folder): Response {
+        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true);
     }
 
diff --git a/src/EncodingAliases.php b/src/EncodingAliases.php
index 274a8010..32d32e2f 100644
--- a/src/EncodingAliases.php
+++ b/src/EncodingAliases.php
@@ -107,6 +107,8 @@ class EncodingAliases {
         "ibm864"                   => "IBM864",
         "utf-8"                    => "UTF-8",
         "utf-7"                    => "UTF-7",
+        "utf-7-imap"               => "UTF7-IMAP",
+        "utf7-imap"                => "UTF7-IMAP",
         "shift_jis"                => "Shift_JIS",
         "big5"                     => "Big5",
         "euc-jp"                   => "EUC-JP",
@@ -478,5 +480,102 @@ public static function get(?string $encoding, string $fallback = null): string {
         }
         return $fallback ?: $encoding;
     }
-    
+
+
+    /**
+     * Convert the encoding of a string
+     * @param $str
+     * @param string $from
+     * @param string $to
+     *
+     * @return mixed
+     */
+    public static function convert($str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed {
+        $from = self::get($from, self::detectEncoding($str));
+        $to = self::get($to, self::detectEncoding($str));
+
+        if ($from === $to) {
+            return $str;
+        }
+
+        // We don't need to do convertEncoding() if charset is ASCII (us-ascii):
+        //     ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded
+        //     https://stackoverflow.com/a/11303410
+        //
+        // us-ascii is the same as ASCII:
+        //     ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA)
+        //     prefers the updated name US-ASCII, which clarifies that this system was developed in the US and
+        //     based on the typographical symbols predominantly in use there.
+        //     https://en.wikipedia.org/wiki/ASCII
+        //
+        // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken.
+        if (strtolower($from) == 'us-ascii' && $to == 'UTF-8') {
+            return $str;
+        }
+
+        try {
+            if (function_exists('iconv') && !self::isUtf7($from) && !self::isUtf7($to)) {
+                return iconv($from, $to, $str);
+            }
+            if (!$from) {
+                return mb_convert_encoding($str, $to);
+            }
+            return mb_convert_encoding($str, $to, $from);
+        } catch (\Exception $e) {
+            if (str_contains($from, '-')) {
+                $from = str_replace('-', '', $from);
+                return self::convert($str, $from, $to);
+            }
+            return $str;
+        }
+    }
+
+    /**
+     * Attempts to detect the encoding of a string
+     * @param string $string
+     *
+     * @return string
+     */
+    public static function detectEncoding(string $string): string {
+        $encoding = mb_detect_encoding($string, array_filter(self::getEncodings(), function($value){
+            return !in_array($value, [
+                'ISO-8859-6-I', 'ISO-8859-6-E', 'ISO-8859-8-I', 'ISO-8859-8-E',
+                'ISO-8859-11', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16', 'ISO-IR-111',"ISO-2022-CN",
+                "windows-1250", "windows-1253", "windows-1255", "windows-1256", "windows-1257", "windows-1258",
+                "IBM852", "IBM855", "IBM857", "IBM866", "IBM864", "IBM862", "KOI8-R", "KOI8-U",
+                "TIS-620", "ISO-8859-1", "ISO-8859-2", "ISO-8859-3", "ISO-8859-4",
+                "VISCII", "T.61-8bit", "Big5-HKSCS", "windows-874", "macintosh", "ISO-8859-12", "ISO-8859-7",
+                "IMAP-UTF-7"
+            ]);
+        }), true);
+        if ($encoding === false) {
+            $encoding = 'UTF-8';
+        }
+        return $encoding;
+    }
+
+    /**
+     * Returns all available encodings
+     *
+     * @return array
+     */
+    public static function getEncodings(): array {
+        $encodings = [];
+        foreach (self::$aliases as $encoding) {
+            if (!in_array($encoding, $encodings)) {
+                $encodings[] = $encoding;
+            }
+        }
+        return $encodings;
+    }
+
+    /**
+     * Returns true if the encoding is UTF-7 like
+     * @param string $encoding
+     *
+     * @return bool
+     */
+    protected static function isUtf7(string $encoding): bool {
+        return str_contains(str_replace("-", "", strtolower($encoding)), "utf7");
+    }
 }
diff --git a/src/Folder.php b/src/Folder.php
index 99b1e7bf..047fe7e8 100755
--- a/src/Folder.php
+++ b/src/Folder.php
@@ -223,7 +223,12 @@ public function setChildren(FolderCollection $children): Folder {
      * @return string|array|bool|string[]|null
      */
     protected function decodeName($name): string|array|bool|null {
-        return mb_convert_encoding($name, "UTF-8", "UTF7-IMAP");
+        $parts = [];
+        foreach (explode($this->delimiter, $name) as $item) {
+            $parts[] = EncodingAliases::convert($item, "", "UTF-8");
+        }
+
+        return implode($this->delimiter, $parts);
     }
 
     /**
@@ -236,7 +241,7 @@ protected function decodeName($name): string|array|bool|null {
     protected function getSimpleName($delimiter, $full_name): string|bool {
         $arr = explode($delimiter, $full_name);
 
-        return end($arr);
+        return EncodingAliases::convert(end($arr), "", "UTF-8");
     }
 
     /**
diff --git a/src/Header.php b/src/Header.php
index d4a0117f..3e437aed 100644
--- a/src/Header.php
+++ b/src/Header.php
@@ -346,7 +346,6 @@ private function notDecoded($encoded, $decoded): bool {
      * @return mixed|string
      */
     public function convertEncoding($str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed {
-
         $from = EncodingAliases::get($from, $this->fallback_encoding);
         $to = EncodingAliases::get($to, $this->fallback_encoding);
 
@@ -354,38 +353,7 @@ public function convertEncoding($str, string $from = "ISO-8859-2", string $to =
             return $str;
         }
 
-        // We don't need to do convertEncoding() if charset is ASCII (us-ascii):
-        //     ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded
-        //     https://stackoverflow.com/a/11303410
-        //
-        // us-ascii is the same as ASCII:
-        //     ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA)
-        //     prefers the updated name US-ASCII, which clarifies that this system was developed in the US and
-        //     based on the typographical symbols predominantly in use there.
-        //     https://en.wikipedia.org/wiki/ASCII
-        //
-        // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken.
-        if (strtolower($from) == 'us-ascii' && $to == 'UTF-8') {
-            return $str;
-        }
-
-        try {
-            if (function_exists('iconv') && $from != 'UTF-7' && $to != 'UTF-7') {
-                return iconv($from, $to, $str);
-            } else {
-                if (!$from) {
-                    return mb_convert_encoding($str, $to);
-                }
-                return mb_convert_encoding($str, $to, $from);
-            }
-        } catch (\Exception $e) {
-            if (str_contains($from, '-')) {
-                $from = str_replace('-', '', $from);
-                return $this->convertEncoding($str, $from, $to);
-            } else {
-                return $str;
-            }
-        }
+        return EncodingAliases::convert($str, $from, $to);
     }
 
     /**
diff --git a/tests/LiveMailboxTestCase.php b/tests/LiveMailboxTestCase.php
index e56da046..3bb9c9ae 100644
--- a/tests/LiveMailboxTestCase.php
+++ b/tests/LiveMailboxTestCase.php
@@ -18,6 +18,7 @@
 use Webklex\PHPIMAP\Exceptions\AuthFailedException;
 use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
 use Webklex\PHPIMAP\Exceptions\EventNotFoundException;
+use Webklex\PHPIMAP\Exceptions\FolderFetchingException;
 use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
 use Webklex\PHPIMAP\Exceptions\ImapServerErrorException;
 use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
@@ -36,6 +37,10 @@
  * @package Tests
  */
 abstract class LiveMailboxTestCase extends TestCase {
+
+    /**
+     * Special chars
+     */
     const SPECIAL_CHARS = 'A_\\|!"£$%&()=?àèìòùÀÈÌÒÙ<>-@#[]_ß_б_π_€_✔_你_يد_Z_';
 
     /**
@@ -81,10 +86,42 @@ final protected function getClient(): Client {
         return $this->getManager()->account('default');
     }
 
+    /**
+     * Get special chars
+     *
+     * @return string
+     */
     final protected function getSpecialChars(): string {
         return self::SPECIAL_CHARS;
     }
 
+    /**
+     * Get a folder
+     * @param string $folder_path
+     *
+     * @return Folder
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     * @throws FolderFetchingException
+     */
+    final protected function getFolder(string $folder_path = "INDEX"): Folder {
+        $client = $this->getClient();
+        $this->assertNotNull($client);
+
+        //Connect to the IMAP Server
+        $client->connect();
+
+        $folder = $client->getFolderByPath($folder_path);
+        $this->assertNotNull($folder);
+
+        return $folder;
+    }
+
     /**
      * Append a message to a folder
      * @param Folder $folder
@@ -104,17 +141,80 @@ final protected function getSpecialChars(): string {
      * @throws RuntimeException
      */
     final protected function appendMessage(Folder $folder, string $message): Message {
-        $response = $folder->appendMessage($message);
+        $status = $folder->examine();
+        if (!isset($status['uidnext'])) {
+            $this->fail("No UIDNEXT returned");
+        }
 
-        if (!isset($response[0])) {
-            $this->fail("No message ID returned");
+        $response = $folder->appendMessage($message);
+        $valid_response = false;
+        foreach ($response as $line) {
+            if (str_starts_with($line, 'OK')) {
+                $valid_response = true;
+                break;
+            }
         }
-        $test = explode(' ', $response[0]);
-        if (!isset($test[3])) {
-            $this->fail("No message ID returned");
+        if (!$valid_response) {
+            $this->fail("Failed to append message: ".implode("\n", $response));
         }
 
-        $id = substr($test[3], 0, -1);
-        return $folder->messages()->getMessageByUid(intval($id));
+        $message = $folder->messages()->getMessageByUid($status['uidnext']);
+        $this->assertNotNull($message);
+
+        return $message;
+    }
+
+    /**
+     * Append a message template to a folder
+     * @param Folder $folder
+     * @param string $template
+     *
+     * @return Message
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    final protected function appendMessageTemplate(Folder $folder, string $template): Message {
+        $content = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", $template]));
+        return $this->appendMessage($folder, $content);
+    }
+
+    /**
+     * Delete a folder if it is given
+     * @param Folder|null $folder
+     *
+     * @return bool
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    final protected function deleteFolder(Folder $folder = null): bool {
+        $response = $folder?->delete(false);
+        if (is_array($response)) {
+            $valid_response = false;
+            foreach ($response as $line) {
+                if (str_starts_with($line, 'OK')) {
+                    $valid_response = true;
+                    break;
+                }
+            }
+            if (!$valid_response) {
+                $this->fail("Failed to delete mailbox: ".implode("\n", $response));
+            }
+            return $valid_response;
+        }
+        return false;
     }
 }
\ No newline at end of file
diff --git a/tests/issues/Issue379Test.php b/tests/issues/Issue379Test.php
index 9c942960..55efcdee 100644
--- a/tests/issues/Issue379Test.php
+++ b/tests/issues/Issue379Test.php
@@ -13,29 +13,51 @@
 namespace Tests\issues;
 
 use Tests\LiveMailboxTestCase;
+use Webklex\PHPIMAP\Exceptions\AuthFailedException;
+use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
+use Webklex\PHPIMAP\Exceptions\EventNotFoundException;
+use Webklex\PHPIMAP\Exceptions\FolderFetchingException;
+use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
+use Webklex\PHPIMAP\Exceptions\ImapServerErrorException;
+use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
+use Webklex\PHPIMAP\Exceptions\MaskNotFoundException;
+use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException;
+use Webklex\PHPIMAP\Exceptions\MessageFlagException;
+use Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException;
+use Webklex\PHPIMAP\Exceptions\MessageNotFoundException;
+use Webklex\PHPIMAP\Exceptions\ResponseException;
+use Webklex\PHPIMAP\Exceptions\RuntimeException;
 
 class Issue379Test extends LiveMailboxTestCase {
 
-    public function testIssue() {
-        if (!$_ENV["LIVE_MAILBOX"] ?? false) {
+    /**
+     * Test issue #379 - Message::getSize() added
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     * @throws MaskNotFoundException
+     */
+    public function testIssue(): void {
+        if (!getenv("LIVE_MAILBOX") ?? false) {
             $this->markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
         }
+        $folder = $this->getFolder('INBOX');
 
-        $client = $this->getClient();
-        $this->assertNotNull($client);
-
-        //Connect to the IMAP Server
-        $client->connect();
-
-        $folder = $client->getFolderByPath('INBOX');
-        $this->assertNotNull($folder);
-
-        $content = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "plain.eml"]));
-        $message = $this->appendMessage($folder, $content);
-        $this->assertNotNull($message);
-
+        $message = $this->appendMessageTemplate($folder, "plain.eml");
         $this->assertEquals(214, $message->getSize());
 
+        // Clean up
         $this->assertEquals(true, $message->delete(true));
     }
 
diff --git a/tests/issues/Issue383Test.php b/tests/issues/Issue383Test.php
new file mode 100644
index 00000000..2f514b6e
--- /dev/null
+++ b/tests/issues/Issue383Test.php
@@ -0,0 +1,67 @@
+markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
+        }
+        $client = $this->getClient();
+        $client->connect();
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $folder_path = implode($delimiter, ['INBOX', 'Entwürfe+']);
+
+        $folder = $client->getFolder($folder_path);
+        $this->deleteFolder($folder);
+
+        $folder = $client->createFolder($folder_path, false);
+        $this->assertNotNull($folder);
+        $folder = $this->getFolder($folder_path);
+        $this->assertNotNull($folder);
+
+        $this->assertEquals('Entwürfe+', $folder->name);
+        $this->assertEquals($folder_path, $folder->full_name);
+
+        // Clean up
+        if ($this->deleteFolder($folder) === false) {
+            $this->fail("Could not delete folder: " . $folder->path);
+        }
+    }
+}
\ No newline at end of file

From 2645b30098e7bc09aaf117a1b35fada2adbfed3b Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 02:47:38 +0100
Subject: [PATCH 020/203] Optional utf-7-imap support #383

---
 src/Client.php                            | 35 ++++++++++++-----------
 src/Connection/Protocols/ImapProtocol.php | 13 ---------
 src/Folder.php                            |  5 ++--
 tests/issues/Issue383Test.php             |  5 ++++
 4 files changed, 26 insertions(+), 32 deletions(-)

diff --git a/src/Client.php b/src/Client.php
index 893284f8..c10fa8a2 100755
--- a/src/Client.php
+++ b/src/Client.php
@@ -505,11 +505,11 @@ public function disconnect(): Client {
      * @throws RuntimeException
      * @throws ResponseException
      */
-    public function getFolder(string $folder_name, ?string $delimiter = null): ?Folder {
+    public function getFolder(string $folder_name, ?string $delimiter = null, bool $utf7 = false): ?Folder {
         // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names)
         $delimiter = is_null($delimiter) ? ClientManager::get('options.delimiter', "/") : $delimiter;
         if (str_contains($folder_name, (string)$delimiter)) {
-            return $this->getFolderByPath($folder_name);
+            return $this->getFolderByPath($folder_name, $utf7);
         }
 
         return $this->getFolderByName($folder_name);
@@ -535,18 +535,18 @@ public function getFolderByName($folder_name): ?Folder {
     /**
      * Get a folder instance by a folder path
      * @param $folder_path
-     *
+     * @param bool $utf7
      * @return Folder|null
-     * @throws FolderFetchingException
-     * @throws ConnectionFailedException
      * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
      * @throws ImapBadRequestException
      * @throws ImapServerErrorException
-     * @throws RuntimeException
      * @throws ResponseException
+     * @throws RuntimeException
      */
-    public function getFolderByPath($folder_path): ?Folder {
-        $folder_path = EncodingAliases::convert($folder_path, "", "UTF7-IMAP");
+    public function getFolderByPath($folder_path, bool $utf7 = false): ?Folder {
+        if (!$utf7) $folder_path = EncodingAliases::convert($folder_path, "utf-8", "utf7-imap");
         return $this->getFolders(false)->where("path", $folder_path)->first();
     }
 
@@ -663,24 +663,27 @@ public function openFolder(string $folder_path, bool $force_select = false): arr
      * Create a new Folder
      * @param string $folder_path
      * @param boolean $expunge
-     *
+     * @param bool $utf7
      * @return Folder
-     * @throws ConnectionFailedException
      * @throws AuthFailedException
-     * @throws ImapBadRequestException
-     * @throws ImapServerErrorException
-     * @throws RuntimeException
+     * @throws ConnectionFailedException
      * @throws EventNotFoundException
      * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
      * @throws ResponseException
+     * @throws RuntimeException
      */
-    public function createFolder(string $folder_path, bool $expunge = true): Folder {
+    public function createFolder(string $folder_path, bool $expunge = true, bool $utf7 = false): Folder {
         $this->checkConnection();
+
+        if (!$utf7) $folder_path = EncodingAliases::convert($folder_path, "utf-8", "UTF7-IMAP");
+
         $status = $this->connection->createFolder($folder_path)->validatedData();
 
         if($expunge) $this->expunge();
 
-        $folder = $this->getFolderByPath($folder_path);
+        $folder = $this->getFolderByPath($folder_path, true);
         if($status && $folder) {
             $event = $this->getEvent("folder", "new");
             $event::dispatch($folder);
@@ -708,7 +711,7 @@ public function deleteFolder(string $folder_path, bool $expunge = true): array {
         $this->checkConnection();
 
         $folder = $this->getFolderByPath($folder_path);
-        $status = $this->getConnection()->deleteFolder($folder_path)->validatedData();
+        $status = $this->getConnection()->deleteFolder($folder->path)->validatedData();
         if ($expunge) $this->expunge();
 
         $event = $this->getEvent("folder", "deleted");
diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index db0bb216..aee0e16b 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -13,7 +13,6 @@
 namespace Webklex\PHPIMAP\Connection\Protocols;
 
 use Exception;
-use Webklex\PHPIMAP\EncodingAliases;
 use Webklex\PHPIMAP\Exceptions\AuthFailedException;
 use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
 use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
@@ -589,7 +588,6 @@ public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'I
     public function selectFolder(string $folder = 'INBOX'): Response {
         $this->uid_cache = [];
 
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->examineOrSelect('SELECT', $folder);
     }
 
@@ -601,7 +599,6 @@ public function selectFolder(string $folder = 'INBOX'): Response {
      * @throws RuntimeException
      */
     public function examineFolder(string $folder = 'INBOX'): Response {
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->examineOrSelect('EXAMINE', $folder);
     }
 
@@ -914,7 +911,6 @@ public function store(
      * @throws RuntimeException
      */
     public function appendMessage(string $folder, string $message, array $flags = null, string $date = null): Response {
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         $tokens = [];
         $tokens[] = $this->escapeString($folder);
         if ($flags !== null) {
@@ -994,7 +990,6 @@ public function moveMessage(string $folder, $from, int $to = null, int|string $u
         $set = $this->buildSet($from, $to);
         $command = $this->buildUIDCommand("MOVE", $uid);
 
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true);
     }
 
@@ -1014,8 +1009,6 @@ public function moveMessage(string $folder, $from, int $to = null, int|string $u
      */
     public function moveManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response {
         $command = $this->buildUIDCommand("MOVE", $uid);
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
-
         $set = implode(',', $messages);
         $tokens = [$set, $this->escapeString($folder)];
 
@@ -1057,7 +1050,6 @@ public function ID($ids = null): Response {
      * @throws RuntimeException
      */
     public function createFolder(string $folder): Response {
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('CREATE', [$this->escapeString($folder)], true);
     }
 
@@ -1074,8 +1066,6 @@ public function createFolder(string $folder): Response {
      * @throws RuntimeException
      */
     public function renameFolder(string $old, string $new): Response {
-        $old = EncodingAliases::convert($old, "", "UTF7-IMAP");
-        $new = EncodingAliases::convert($new, "", "UTF7-IMAP");
         return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true);
     }
 
@@ -1090,7 +1080,6 @@ public function renameFolder(string $old, string $new): Response {
      * @throws RuntimeException
      */
     public function deleteFolder(string $folder): Response {
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true);
     }
 
@@ -1105,7 +1094,6 @@ public function deleteFolder(string $folder): Response {
      * @throws RuntimeException
      */
     public function subscribeFolder(string $folder): Response {
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true);
     }
 
@@ -1120,7 +1108,6 @@ public function subscribeFolder(string $folder): Response {
      * @throws RuntimeException
      */
     public function unsubscribeFolder(string $folder): Response {
-        $folder = EncodingAliases::convert($folder, "", "UTF7-IMAP");
         return $this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true);
     }
 
diff --git a/src/Folder.php b/src/Folder.php
index 047fe7e8..9256492e 100755
--- a/src/Folder.php
+++ b/src/Folder.php
@@ -225,7 +225,7 @@ public function setChildren(FolderCollection $children): Folder {
     protected function decodeName($name): string|array|bool|null {
         $parts = [];
         foreach (explode($this->delimiter, $name) as $item) {
-            $parts[] = EncodingAliases::convert($item, "", "UTF-8");
+            $parts[] = EncodingAliases::convert($item, "UTF7-IMAP", "UTF-8");
         }
 
         return implode($this->delimiter, $parts);
@@ -240,8 +240,7 @@ protected function decodeName($name): string|array|bool|null {
      */
     protected function getSimpleName($delimiter, $full_name): string|bool {
         $arr = explode($delimiter, $full_name);
-
-        return EncodingAliases::convert(end($arr), "", "UTF-8");
+        return end($arr);
     }
 
     /**
diff --git a/tests/issues/Issue383Test.php b/tests/issues/Issue383Test.php
index 2f514b6e..db82c1dc 100644
--- a/tests/issues/Issue383Test.php
+++ b/tests/issues/Issue383Test.php
@@ -42,6 +42,7 @@ public function testIssue(): void {
         if (!getenv("LIVE_MAILBOX") ?? false) {
             $this->markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
         }
+
         $client = $this->getClient();
         $client->connect();
 
@@ -53,12 +54,16 @@ public function testIssue(): void {
 
         $folder = $client->createFolder($folder_path, false);
         $this->assertNotNull($folder);
+
         $folder = $this->getFolder($folder_path);
         $this->assertNotNull($folder);
 
         $this->assertEquals('Entwürfe+', $folder->name);
         $this->assertEquals($folder_path, $folder->full_name);
 
+        $folder_path = implode($delimiter, ['INBOX', 'Entw&APw-rfe+']);
+        $this->assertEquals($folder_path, $folder->path);
+
         // Clean up
         if ($this->deleteFolder($folder) === false) {
             $this->fail("Could not delete folder: " . $folder->path);

From 38ac40ae23f5c6f5beeb70b16e65ae607e6c60ee Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 19:56:02 +0100
Subject: [PATCH 021/203] IMAP Quota root command fixed

---
 src/Connection/Protocols/ImapProtocol.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index aee0e16b..8715b6a8 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -1163,7 +1163,7 @@ public function getQuota($username): Response {
      * @throws RuntimeException
      */
     public function getQuotaRoot(string $quota_root = 'INBOX'): Response {
-        $command = "QUOTA";
+        $command = "GETQUOTAROOT";
         $params = [$quota_root];
 
         return $this->requestAndResponse($command, $params);

From 421ccf5ecc8c4ba857424881d3e22789f7b5fe5d Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 19:57:48 +0100
Subject: [PATCH 022/203] Prevent line-breaks in folder path caused by special
 chars

---
 src/Connection/Protocols/ImapProtocol.php | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index 8715b6a8..f295293a 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -189,7 +189,7 @@ protected function decodeLine(Response $response, string $line): array {
                 $token = substr($token, 1);
             }
             if ($token[0] == '"') {
-                if (preg_match('%^\(*"((.|\\\\|\")*?)" *%', $line, $matches)) {
+                if (preg_match('%^\(*\"((.|\\\|\")*?)\"( |$)%', $line, $matches)) {
                     $tokens[] = $matches[1];
                     $line = substr($line, strlen($matches[0]));
                     continue;
@@ -844,6 +844,7 @@ public function folders(string $reference = '', string $folder = '*'): Response
                 if (count($item) != 4 || $item[0] != 'LIST') {
                     continue;
                 }
+                $item[3] = str_replace("\\\\", "\\", str_replace("\\\"", "\"", $item[3]));
                 $result[$item[3]] = ['delimiter' => $item[2], 'flags' => $item[1]];
             }
         }

From 4219664d50e6397136da6d164acf58d65aabd4b7 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 19:59:05 +0100
Subject: [PATCH 023/203] Partial fix for #362 (allow overview response to be
 empty)

---
 src/Connection/Protocols/ImapProtocol.php | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index f295293a..f4f8eb62 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -1244,12 +1244,14 @@ public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Resp
                 $ids[] = $id;
             }
         }
-        $headers = $this->headers($ids, "RFC822", $uid);
-        $response->stack($headers);
-        foreach ($headers->data() as $id => $raw_header) {
-            $result[$id] = (new Header($raw_header, false))->getAttributes();
+        if (!empty($ids)) {
+            $headers = $this->headers($ids, "RFC822", $uid);
+            $response->stack($headers);
+            foreach ($headers->data() as $id => $raw_header) {
+                $result[$id] = (new Header($raw_header, false))->getAttributes();
+            }
         }
-        return $response->setResult($result);
+        return $response->setResult($result)->setCanBeEmpty(true);
     }
 
     /**

From 34f7757b1a70bb2a48b9b326d48fc35542f9db78 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 20:00:05 +0100
Subject: [PATCH 024/203] live test location moved

---
 tests/LiveMailboxTest.php                | 49 ------------------------
 tests/issues/Issue379Test.php            |  5 +--
 tests/issues/Issue383Test.php            | 11 ++----
 tests/{ => live}/LiveMailboxTestCase.php | 16 ++++----
 4 files changed, 13 insertions(+), 68 deletions(-)
 delete mode 100644 tests/LiveMailboxTest.php
 rename tests/{ => live}/LiveMailboxTestCase.php (93%)

diff --git a/tests/LiveMailboxTest.php b/tests/LiveMailboxTest.php
deleted file mode 100644
index ae484640..00000000
--- a/tests/LiveMailboxTest.php
+++ /dev/null
@@ -1,49 +0,0 @@
-getClient();
-        $client->connect();
-
-        self::assertTrue($client->isConnected());
-    }
-
-}
\ No newline at end of file
diff --git a/tests/issues/Issue379Test.php b/tests/issues/Issue379Test.php
index 55efcdee..fdb0436f 100644
--- a/tests/issues/Issue379Test.php
+++ b/tests/issues/Issue379Test.php
@@ -12,7 +12,7 @@
 
 namespace Tests\issues;
 
-use Tests\LiveMailboxTestCase;
+use Tests\live\LiveMailboxTestCase;
 use Webklex\PHPIMAP\Exceptions\AuthFailedException;
 use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
 use Webklex\PHPIMAP\Exceptions\EventNotFoundException;
@@ -49,9 +49,6 @@ class Issue379Test extends LiveMailboxTestCase {
      * @throws MaskNotFoundException
      */
     public function testIssue(): void {
-        if (!getenv("LIVE_MAILBOX") ?? false) {
-            $this->markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
-        }
         $folder = $this->getFolder('INBOX');
 
         $message = $this->appendMessageTemplate($folder, "plain.eml");
diff --git a/tests/issues/Issue383Test.php b/tests/issues/Issue383Test.php
index db82c1dc..0f20a396 100644
--- a/tests/issues/Issue383Test.php
+++ b/tests/issues/Issue383Test.php
@@ -12,7 +12,7 @@
 
 namespace Tests\issues;
 
-use Tests\LiveMailboxTestCase;
+use Tests\live\LiveMailboxTestCase;
 use Webklex\PHPIMAP\Exceptions\AuthFailedException;
 use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
 use Webklex\PHPIMAP\Exceptions\EventNotFoundException;
@@ -22,6 +22,7 @@
 use Webklex\PHPIMAP\Exceptions\MaskNotFoundException;
 use Webklex\PHPIMAP\Exceptions\ResponseException;
 use Webklex\PHPIMAP\Exceptions\RuntimeException;
+use Webklex\PHPIMAP\Folder;
 
 class Issue383Test extends LiveMailboxTestCase {
 
@@ -39,10 +40,6 @@ class Issue383Test extends LiveMailboxTestCase {
      * @throws MaskNotFoundException
      */
     public function testIssue(): void {
-        if (!getenv("LIVE_MAILBOX") ?? false) {
-            $this->markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
-        }
-
         $client = $this->getClient();
         $client->connect();
 
@@ -53,10 +50,10 @@ public function testIssue(): void {
         $this->deleteFolder($folder);
 
         $folder = $client->createFolder($folder_path, false);
-        $this->assertNotNull($folder);
+        self::assertInstanceOf(Folder::class, $folder);
 
         $folder = $this->getFolder($folder_path);
-        $this->assertNotNull($folder);
+        self::assertInstanceOf(Folder::class, $folder);
 
         $this->assertEquals('Entwürfe+', $folder->name);
         $this->assertEquals($folder_path, $folder->full_name);
diff --git a/tests/LiveMailboxTestCase.php b/tests/live/LiveMailboxTestCase.php
similarity index 93%
rename from tests/LiveMailboxTestCase.php
rename to tests/live/LiveMailboxTestCase.php
index 3bb9c9ae..562c3809 100644
--- a/tests/LiveMailboxTestCase.php
+++ b/tests/live/LiveMailboxTestCase.php
@@ -10,7 +10,7 @@
 *  -
 */
 
-namespace Tests;
+namespace Tests\live;
 
 use PHPUnit\Framework\TestCase;
 use Webklex\PHPIMAP\Client;
@@ -83,6 +83,9 @@ final protected function getManager(): ClientManager {
      * @throws MaskNotFoundException
      */
     final protected function getClient(): Client {
+        if (!getenv("LIVE_MAILBOX") ?? false) {
+            $this->markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test.");
+        }
         return $this->getManager()->account('default');
     }
 
@@ -111,13 +114,10 @@ final protected function getSpecialChars(): string {
      */
     final protected function getFolder(string $folder_path = "INDEX"): Folder {
         $client = $this->getClient();
-        $this->assertNotNull($client);
-
-        //Connect to the IMAP Server
-        $client->connect();
+        self::assertInstanceOf(Client::class, $client->connect());
 
         $folder = $client->getFolderByPath($folder_path);
-        $this->assertNotNull($folder);
+        self::assertInstanceOf(Folder::class, $folder);
 
         return $folder;
     }
@@ -159,7 +159,7 @@ final protected function appendMessage(Folder $folder, string $message): Message
         }
 
         $message = $folder->messages()->getMessageByUid($status['uidnext']);
-        $this->assertNotNull($message);
+        self::assertInstanceOf(Message::class, $message);
 
         return $message;
     }
@@ -183,7 +183,7 @@ final protected function appendMessage(Folder $folder, string $message): Message
      * @throws RuntimeException
      */
     final protected function appendMessageTemplate(Folder $folder, string $template): Message {
-        $content = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", $template]));
+        $content = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", $template]));
         return $this->appendMessage($folder, $content);
     }
 

From 49a78e63e4026bbdb081164da4f3172b8f23223e Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 20:00:37 +0100
Subject: [PATCH 025/203] live test location added

---
 phpunit.xml.dist | 1 +
 1 file changed, 1 insertion(+)

diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 1f9bd48c..76b56dfe 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -14,6 +14,7 @@
     
       tests
       tests/issues
+      tests/live
     
   
   

From f6df772664eba5d055652041f8b163ef7de3f33b Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 20:01:35 +0100
Subject: [PATCH 026/203] children accessor method added

---
 src/Folder.php | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/Folder.php b/src/Folder.php
index 9256492e..a1452a30 100755
--- a/src/Folder.php
+++ b/src/Folder.php
@@ -215,6 +215,15 @@ public function setChildren(FolderCollection $children): Folder {
         return $this;
     }
 
+    /**
+     * Get children.
+     *
+     * @return FolderCollection
+     */
+    public function getChildren(): FolderCollection {
+        return $this->children;
+    }
+
     /**
      * Decode name.
      * It converts UTF7-IMAP encoding to UTF-8.

From d3da1762665a8ac6b00acd34af05ca2dbfe493c3 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 20:01:58 +0100
Subject: [PATCH 027/203] Doc updated

---
 src/Client.php | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/Client.php b/src/Client.php
index c10fa8a2..b9a08053 100755
--- a/src/Client.php
+++ b/src/Client.php
@@ -495,19 +495,20 @@ public function disconnect(): Client {
      * Get a folder instance by a folder name
      * @param string $folder_name
      * @param string|null $delimiter
-     *
+     * @param bool $utf7
      * @return Folder|null
-     * @throws FolderFetchingException
-     * @throws ConnectionFailedException
      * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
      * @throws ImapBadRequestException
      * @throws ImapServerErrorException
-     * @throws RuntimeException
      * @throws ResponseException
+     * @throws RuntimeException
      */
     public function getFolder(string $folder_name, ?string $delimiter = null, bool $utf7 = false): ?Folder {
         // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names)
         $delimiter = is_null($delimiter) ? ClientManager::get('options.delimiter', "/") : $delimiter;
+
         if (str_contains($folder_name, (string)$delimiter)) {
             return $this->getFolderByPath($folder_name, $utf7);
         }

From 110a78c48621021c7af0abd7a1b60beb5f15d405 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 20:02:39 +0100
Subject: [PATCH 028/203] Live Client::class test added

---
 tests/live/ClientTest.php | 318 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 318 insertions(+)
 create mode 100644 tests/live/ClientTest.php

diff --git a/tests/live/ClientTest.php b/tests/live/ClientTest.php
new file mode 100644
index 00000000..307894a7
--- /dev/null
+++ b/tests/live/ClientTest.php
@@ -0,0 +1,318 @@
+getClient()->connect());
+    }
+
+    /**
+     * Test if the connection is working
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testIsConnected(): void {
+        $client = $this->getClient()->connect();
+
+        self::assertTrue($client->isConnected());
+    }
+
+    /**
+     * Test if the connection state can be determined
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testDisconnect(): void {
+        $client = $this->getClient()->connect();
+
+        self::assertFalse($client->disconnect()->isConnected());
+    }
+
+    /**
+     * Test to get the default inbox folder
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     * @throws FolderFetchingException
+     */
+    public function testGetFolder(): void {
+        $client = $this->getClient()->connect();
+
+        $folder = $client->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+    }
+
+    /**
+     * Test to get the default inbox folder by name
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetFolderByName(): void {
+        $client = $this->getClient()->connect();
+
+        $folder = $client->getFolderByName('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+    }
+
+    /**
+     * Test to get the default inbox folder by path
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetFolderByPath(): void {
+        $client = $this->getClient()->connect();
+
+        $folder = $client->getFolderByPath('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+    }
+
+    /**
+     * Test to get all folders
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetFolders(): void {
+        $client = $this->getClient()->connect();
+
+        $folders = $client->getFolders(false);
+        self::assertTrue($folders->count() > 0);
+    }
+
+    public function testGetFoldersWithStatus(): void {
+        $client = $this->getClient()->connect();
+
+        $folders = $client->getFoldersWithStatus(false);
+        self::assertTrue($folders->count() > 0);
+    }
+
+    public function testOpenFolder(): void {
+        $client = $this->getClient()->connect();
+
+        $status = $client->openFolder("INBOX");
+        self::assertTrue(isset($status["flags"]) && count($status["flags"]) > 0);
+        self::assertTrue(($status["uidnext"] ?? 0) > 0);
+        self::assertTrue(($status["uidvalidity"] ?? 0) > 0);
+        self::assertTrue(($status["recent"] ?? -1) >= 0);
+        self::assertTrue(($status["exists"] ?? -1) >= 0);
+    }
+
+    public function testCreateFolder(): void {
+        $client = $this->getClient()->connect();
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $folder_path = implode($delimiter, ['INBOX', $this->getSpecialChars()]);
+
+        $folder = $client->getFolder($folder_path);
+
+        $this->deleteFolder($folder);
+
+        $folder = $client->createFolder($folder_path, false);
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $folder = $this->getFolder($folder_path);
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $this->assertEquals($this->getSpecialChars(), $folder->name);
+        $this->assertEquals($folder_path, $folder->full_name);
+
+        $folder_path = implode($delimiter, ['INBOX', EncodingAliases::convert($this->getSpecialChars(), "utf-8", "utf7-imap")]);
+        $this->assertEquals($folder_path, $folder->path);
+
+        // Clean up
+        if ($this->deleteFolder($folder) === false) {
+            $this->fail("Could not delete folder: " . $folder->path);
+        }
+    }
+
+    public function testCheckFolder(): void {
+        $client = $this->getClient()->connect();
+
+        $status = $client->checkFolder("INBOX");
+        self::assertTrue(isset($status["flags"]) && count($status["flags"]) > 0);
+        self::assertTrue(($status["uidnext"] ?? 0) > 0);
+        self::assertTrue(($status["uidvalidity"] ?? 0) > 0);
+        self::assertTrue(($status["recent"] ?? -1) >= 0);
+        self::assertTrue(($status["exists"] ?? -1) >= 0);
+    }
+
+    public function testGetFolderPath(): void {
+        $client = $this->getClient()->connect();
+
+        self::assertIsArray($client->openFolder("INBOX"));
+        self::assertEquals("INBOX", $client->getFolderPath());
+    }
+
+    public function testId(): void {
+        $client = $this->getClient()->connect();
+
+        $info = $client->Id();
+        self::assertIsArray($info);
+        $valid = false;
+        foreach ($info as $value) {
+            if (str_starts_with($value, "OK")) {
+                $valid = true;
+                break;
+            }
+        }
+        self::assertTrue($valid);
+    }
+
+    public function testGetQuotaRoot(): void {
+        if (!getenv("LIVE_MAILBOX_QUOTA_SUPPORT")) {
+            $this->markTestSkipped("Quota support is not enabled");
+        }
+
+        $client = $this->getClient()->connect();
+
+        $quota = $client->getQuotaRoot("INBOX");
+        self::assertIsArray($quota);
+        self::assertTrue(count($quota) > 1);
+        self::assertIsArray($quota[0]);
+        self::assertEquals("INBOX", $quota[0][1]);
+        self::assertIsArray($quota[1]);
+        self::assertIsArray($quota[1][2]);
+        self::assertTrue($quota[1][2][2] > 0);
+    }
+
+    public function testSetTimeout(): void {
+        $client = $this->getClient()->connect();
+
+        self::assertInstanceOf(ProtocolInterface::class, $client->setTimeout(57));
+        self::assertEquals(57, $client->getTimeout());
+    }
+
+    public function testExpunge(): void {
+        $client = $this->getClient()->connect();
+
+        $client->openFolder("INBOX");
+        $status = $client->expunge();
+
+        self::assertIsArray($status);
+        self::assertIsArray($status[0]);
+        self::assertEquals("OK", $status[0][0]);
+    }
+
+    public function testGetDefaultMessageMask(): void {
+        $client = $this->getClient();
+
+        self::assertEquals(MessageMask::class, $client->getDefaultMessageMask());
+    }
+
+    public function testGetDefaultEvents(): void {
+        $client = $this->getClient();
+
+        self::assertIsArray($client->getDefaultEvents("message"));
+    }
+
+    public function testSetDefaultMessageMask(): void {
+        $client = $this->getClient();
+
+        self::assertInstanceOf(Client::class, $client->setDefaultMessageMask(AttachmentMask::class));
+        self::assertEquals(AttachmentMask::class, $client->getDefaultMessageMask());
+
+        $client->setDefaultMessageMask(MessageMask::class);
+    }
+
+    public function testGetDefaultAttachmentMask(): void {
+        $client = $this->getClient();
+
+        self::assertEquals(AttachmentMask::class, $client->getDefaultAttachmentMask());
+    }
+
+    public function testSetDefaultAttachmentMask(): void {
+        $client = $this->getClient();
+
+        self::assertInstanceOf(Client::class, $client->setDefaultAttachmentMask(MessageMask::class));
+        self::assertEquals(MessageMask::class, $client->getDefaultAttachmentMask());
+
+        $client->setDefaultAttachmentMask(AttachmentMask::class);
+    }
+}
\ No newline at end of file

From 1eabd93f89edfd0636e4667ef116e018594597b2 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 20:02:53 +0100
Subject: [PATCH 029/203] Live Folder::class test added

---
 tests/live/FolderTest.php | 419 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 419 insertions(+)
 create mode 100644 tests/live/FolderTest.php

diff --git a/tests/live/FolderTest.php b/tests/live/FolderTest.php
new file mode 100644
index 00000000..75d7774b
--- /dev/null
+++ b/tests/live/FolderTest.php
@@ -0,0 +1,419 @@
+getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        self::assertInstanceOf(WhereQuery::class, $folder->query());
+        self::assertInstanceOf(WhereQuery::class, $folder->search());
+        self::assertInstanceOf(WhereQuery::class, $folder->messages());
+    }
+
+    /**
+     * Test Folder::hasChildren()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     * @throws EventNotFoundException
+     */
+    public function testHasChildren(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $child_path = implode($delimiter, ['INBOX', 'test']);
+        if ($folder->getClient()->getFolder($child_path) === null) {
+            $folder->getClient()->createFolder($child_path, false);
+            $folder = $this->getFolder('INBOX');
+        }
+
+        self::assertTrue($folder->hasChildren());
+    }
+
+    /**
+     * Test Folder::setChildren()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetChildren(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $child_path = implode($delimiter, ['INBOX', 'test']);
+        if ($folder->getClient()->getFolder($child_path) === null) {
+            $folder->getClient()->createFolder($child_path, false);
+            $folder = $this->getFolder('INBOX');
+        }
+        self::assertTrue($folder->hasChildren());
+
+        $folder->setChildren(new FolderCollection());
+        self::assertTrue($folder->getChildren()->isEmpty());
+    }
+
+    /**
+     * Test Folder::getChildren()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetChildren(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $child_path = implode($delimiter, ['INBOX', 'test']);
+        if ($folder->getClient()->getFolder($child_path) === null) {
+            $folder->getClient()->createFolder($child_path, false);
+        }
+
+        $folder = $folder->getClient()->getFolders()->where('name', 'INBOX')->first();
+        self::assertInstanceOf(Folder::class, $folder);
+
+        self::assertTrue($folder->hasChildren());
+        self::assertFalse($folder->getChildren()->isEmpty());
+    }
+
+    /**
+     * Test Folder::move()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testMove(): void {
+        $client = $this->getClient();
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $folder_path = implode($delimiter, ['INBOX', 'test']);
+
+        $folder = $client->getFolder($folder_path);
+        if ($folder === null) {
+            $folder = $client->createFolder($folder_path, false);
+        }
+        $new_folder_path = implode($delimiter, ['INBOX', 'other']);
+        $new_folder = $client->getFolder($new_folder_path);
+        $new_folder?->delete(false);
+
+        $status = $folder->move($new_folder_path, false);
+        self::assertIsArray($status);
+        self::assertTrue(str_starts_with($status[0], 'OK'));
+
+        $new_folder = $client->getFolder($new_folder_path);
+        self::assertEquals($new_folder_path, $new_folder->path);
+        self::assertEquals('other', $new_folder->name);
+
+        if ($this->deleteFolder($new_folder) === false) {
+            $this->fail("Could not delete folder: " . $new_folder->path);
+        }
+    }
+
+    /**
+     * Test Folder::delete()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testDelete(): void {
+        $client = $this->getClient();
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $folder_path = implode($delimiter, ['INBOX', 'test']);
+
+        $folder = $client->getFolder($folder_path);
+        if ($folder === null) {
+            $folder = $client->createFolder($folder_path, false);
+        }
+        self::assertInstanceOf(Folder::class, $folder);
+
+        if ($this->deleteFolder($folder) === false) {
+            $this->fail("Could not delete folder: " . $folder->path);
+        }
+    }
+
+    /**
+     * Test Folder::overview()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     * @throws InvalidMessageDateException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     */
+    public function testOverview(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        // Test empty overview
+        $overview = $folder->overview();
+        self::assertIsArray($overview);
+
+        $message = $this->appendMessageTemplate($folder, "plain.eml");
+        self::assertInstanceOf(Message::class, $message);
+
+        $overview = $folder->overview();
+        self::assertIsArray($overview);
+
+        self::assertEquals($message->from, end($overview)["from"]);
+
+        // Clean up
+        $this->assertTrue($message->delete(true));
+    }
+
+    /**
+     * Test Folder::appendMessage()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testAppendMessage(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $message = $this->appendMessageTemplate($folder, "plain.eml");
+        self::assertInstanceOf(Message::class, $message);
+
+        self::assertEquals("Example", $message->subject);
+        self::assertEquals("to@someone-else.com", $message->to);
+        self::assertEquals("from@someone.com", $message->from);
+
+        // Clean up
+        $this->assertTrue($message->delete(true));
+    }
+
+    /**
+     * Test Folder::subscribe()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSubscribe(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $status = $folder->subscribe();
+        self::assertIsArray($status);
+        self::assertTrue(str_starts_with($status[0], 'OK'));
+
+        // Clean up
+        $folder->unsubscribe();
+    }
+
+    /**
+     * Test Folder::unsubscribe()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testUnsubscribe(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $folder->subscribe();
+
+        $status = $folder->subscribe();
+        self::assertIsArray($status);
+        self::assertTrue(str_starts_with($status[0], 'OK'));
+    }
+
+    /**
+     * Test Folder::examine()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testExamine(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $status = $folder->examine();
+        self::assertTrue(isset($status["flags"]) && count($status["flags"]) > 0);
+        self::assertTrue(($status["uidnext"] ?? 0) > 0);
+        self::assertTrue(($status["uidvalidity"] ?? 0) > 0);
+        self::assertTrue(($status["recent"] ?? -1) >= 0);
+        self::assertTrue(($status["exists"] ?? -1) >= 0);
+    }
+
+    /**
+     * Test Folder::getClient()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetClient(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+        self::assertInstanceOf(Client::class, $folder->getClient());
+    }
+
+    /**
+     * Test Folder::setDelimiter()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws MaskNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetDelimiter(): void {
+        $folder = $this->getFolder('INBOX');
+        self::assertInstanceOf(Folder::class, $folder);
+
+        $folder->setDelimiter("/");
+        self::assertEquals("/", $folder->delimiter);
+
+        $folder->setDelimiter(".");
+        self::assertEquals(".", $folder->delimiter);
+
+        $default_delimiter = $this->getManager()->get("options.delimiter", "/");
+        $folder->setDelimiter(null);
+        self::assertEquals($default_delimiter, $folder->delimiter);
+    }
+
+}
\ No newline at end of file

From 3fd790fc44dca31bc33350d2f6c12b004f2e8b83 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 20:03:06 +0100
Subject: [PATCH 030/203] Changelog updated

---
 CHANGELOG.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 553b8781..9fe4c08f 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
 ## [UNRELEASED]
 ### Fixed
 - Extended UTF-7 support added (RFC2060) #383
+- IMAP Quota root command fixed
+- Prevent line-breaks in folder path caused by special chars
+- Partial fix for #362 (allow overview response to be empty)
 
 ### Added
 - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357)

From dd69b886f14c7eef2c66c2526cfb6114917918f9 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 21:01:37 +0100
Subject: [PATCH 031/203] message number added to not found exception

---
 src/Connection/Protocols/ImapProtocol.php | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index f4f8eb62..6cdc467a 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -812,14 +812,13 @@ public function getUid(?int $id = null): Response {
      * @throws MessageNotFoundException
      */
     public function getMessageNumber(string $id): Response {
-        $ids = $this->getUid();
-        foreach ($ids as $k => $v) {
+        foreach ($this->getUid()->data() as $k => $v) {
             if ($v == $id) {
                 return Response::empty($this->debug)->setResult((int)$k);
             }
         }
 
-        throw new MessageNotFoundException('message number not found');
+        throw new MessageNotFoundException('message number not found: ' . $id);
     }
 
     /**

From 605a28de404462840ecf3419bbc82b23bbeb6f3c Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 21:02:27 +0100
Subject: [PATCH 032/203] UTF-7 detection improved

---
 src/EncodingAliases.php | 2 +-
 src/Message.php         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/EncodingAliases.php b/src/EncodingAliases.php
index 32d32e2f..9ebdd958 100644
--- a/src/EncodingAliases.php
+++ b/src/EncodingAliases.php
@@ -575,7 +575,7 @@ public static function getEncodings(): array {
      *
      * @return bool
      */
-    protected static function isUtf7(string $encoding): bool {
+    public static function isUtf7(string $encoding): bool {
         return str_contains(str_replace("-", "", strtolower($encoding)), "utf7");
     }
 }
diff --git a/src/Message.php b/src/Message.php
index 97f34a5d..f0253dc9 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -866,7 +866,7 @@ public function convertEncoding($str, string $from = "ISO-8859-2", string $to =
             return $str;
         }
 
-        if (function_exists('iconv') && $from != 'UTF-7' && $to != 'UTF-7') {
+        if (function_exists('iconv') && !EncodingAliases::isUtf7($from) && !EncodingAliases::isUtf7($to)) {
             return @iconv($from, $to . '//IGNORE', $str);
         } else {
             if (!$from) {

From 4345bf72a727e99b4dcd9a3a6783297f57ff59e8 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 21:07:17 +0100
Subject: [PATCH 033/203] Docs added

---
 src/Connection/Protocols/Protocol.php |  4 ++++
 src/Connection/Protocols/Response.php | 32 +++++++++++++++++++++++++++
 2 files changed, 36 insertions(+)

diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php
index 3f4b83ba..6fe88ee9 100644
--- a/src/Connection/Protocols/Protocol.php
+++ b/src/Connection/Protocols/Protocol.php
@@ -216,6 +216,8 @@ public function createStream($transport, string $host, int $port, int $timeout)
     }
 
     /**
+     * Get the current connection timeout
+     *
      * @return int
      */
     public function getConnectionTimeout(): int {
@@ -223,7 +225,9 @@ public function getConnectionTimeout(): int {
     }
 
     /**
+     * Set the connection timeout
      * @param int $connection_timeout
+     *
      * @return Protocol
      */
     public function setConnectionTimeout(int $connection_timeout): Protocol {
diff --git a/src/Connection/Protocols/Response.php b/src/Connection/Protocols/Response.php
index 068f082f..9771867a 100644
--- a/src/Connection/Protocols/Response.php
+++ b/src/Connection/Protocols/Response.php
@@ -64,6 +64,10 @@ class Response {
      */
     protected bool $debug = false;
 
+    /**
+     * Can the response be empty?
+     * @var bool $can_be_empty
+     */
     protected bool $can_be_empty = false;
 
     /**
@@ -334,6 +338,12 @@ public function successful(): bool {
     }
 
 
+    /**
+     * Check if the Response can be considered failed
+     * @param mixed $data
+     *
+     * @return bool
+     */
     public function verify_data(mixed $data): bool {
         if (is_array($data)) {
             foreach ($data as $line) {
@@ -355,6 +365,12 @@ public function verify_data(mixed $data): bool {
         return true;
     }
 
+    /**
+     * Verify a single line
+     * @param string $line
+     *
+     * @return bool
+     */
     public function verify_line(string $line): bool {
         return !str_starts_with($line, "TAG".$this->noun." BAD ") && !str_starts_with($line, "TAG".$this->noun." NO ");
     }
@@ -368,15 +384,31 @@ public function failed(): bool {
         return !$this->successful();
     }
 
+    /**
+     * Get the Response noun
+     *
+     * @return int
+     */
     public function Noun(): int {
         return $this->noun;
     }
 
+    /**
+     * Set the Response to be allowed to be empty
+     * @param bool $can_be_empty
+     *
+     * @return $this
+     */
     public function setCanBeEmpty(bool $can_be_empty): Response {
         $this->can_be_empty = $can_be_empty;
         return $this;
     }
 
+    /**
+     * Check if the Response can be empty
+     *
+     * @return bool
+     */
     public function canBeEmpty(): bool {
         return $this->can_be_empty;
     }

From e733eca156bd0fb679c9fe95edeef95492294953 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Tue, 7 Mar 2023 21:21:31 +0100
Subject: [PATCH 034/203] class docs updated

---
 src/Connection/Protocols/Response.php |  2 +
 src/Query/Query.php                   | 69 ++++++++++++++++++++++++---
 2 files changed, 64 insertions(+), 7 deletions(-)

diff --git a/src/Connection/Protocols/Response.php b/src/Connection/Protocols/Response.php
index 9771867a..9a30d56d 100644
--- a/src/Connection/Protocols/Response.php
+++ b/src/Connection/Protocols/Response.php
@@ -301,6 +301,7 @@ public function boolean(): bool {
 
     /**
      * Validate and retrieve the response data
+     *
      * @throws ResponseException
      */
     public function validatedData(): mixed {
@@ -309,6 +310,7 @@ public function validatedData(): mixed {
 
     /**
      * Validate the response date
+     *
      * @throws ResponseException
      */
     public function validate(): Response {
diff --git a/src/Query/Query.php b/src/Query/Query.php
index db356337..727e641f 100644
--- a/src/Query/Query.php
+++ b/src/Query/Query.php
@@ -707,6 +707,8 @@ public function limit(int $limit, int $page = 1): Query {
     }
 
     /**
+     * Get the current query collection
+     *
      * @return Collection
      */
     public function getQuery(): Collection {
@@ -714,7 +716,9 @@ public function getQuery(): Collection {
     }
 
     /**
+     * Set all query parameters
      * @param array $query
+     *
      * @return Query
      */
     public function setQuery(array $query): Query {
@@ -723,6 +727,8 @@ public function setQuery(array $query): Query {
     }
 
     /**
+     * Get the raw query
+     *
      * @return string
      */
     public function getRawQuery(): string {
@@ -730,7 +736,9 @@ public function getRawQuery(): string {
     }
 
     /**
+     * Set the raw query
      * @param string $raw_query
+     *
      * @return Query
      */
     public function setRawQuery(string $raw_query): Query {
@@ -739,6 +747,8 @@ public function setRawQuery(string $raw_query): Query {
     }
 
     /**
+     * Get all applied extensions
+     *
      * @return string[]
      */
     public function getExtensions(): array {
@@ -746,7 +756,9 @@ public function getExtensions(): array {
     }
 
     /**
+     * Set all extensions that should be used
      * @param string[] $extensions
+     *
      * @return Query
      */
     public function setExtensions(array $extensions): Query {
@@ -760,7 +772,9 @@ public function setExtensions(array $extensions): Query {
     }
 
     /**
+     * Set the client instance
      * @param Client $client
+     *
      * @return Query
      */
     public function setClient(Client $client): Query {
@@ -770,6 +784,7 @@ public function setClient(Client $client): Query {
 
     /**
      * Get the set fetch limit
+     *
      * @return ?int
      */
     public function getLimit(): ?int {
@@ -777,7 +792,9 @@ public function getLimit(): ?int {
     }
 
     /**
+     * Set the fetch limit
      * @param int $limit
+     *
      * @return Query
      */
     public function setLimit(int $limit): Query {
@@ -786,6 +803,8 @@ public function setLimit(int $limit): Query {
     }
 
     /**
+     * Get the set page
+     *
      * @return int
      */
     public function getPage(): int {
@@ -793,7 +812,9 @@ public function getPage(): int {
     }
 
     /**
+     * Set the page
      * @param int $page
+     *
      * @return Query
      */
     public function setPage(int $page): Query {
@@ -802,7 +823,9 @@ public function setPage(int $page): Query {
     }
 
     /**
+     * Set the fetch option flag
      * @param int $fetch_options
+     *
      * @return Query
      */
     public function setFetchOptions(int $fetch_options): Query {
@@ -811,7 +834,9 @@ public function setFetchOptions(int $fetch_options): Query {
     }
 
     /**
+     * Set the fetch option flag
      * @param int $fetch_options
+     *
      * @return Query
      */
     public function fetchOptions(int $fetch_options): Query {
@@ -819,6 +844,8 @@ public function fetchOptions(int $fetch_options): Query {
     }
 
     /**
+     * Get the fetch option flag
+     *
      * @return ?int
      */
     public function getFetchOptions(): ?int {
@@ -826,6 +853,8 @@ public function getFetchOptions(): ?int {
     }
 
     /**
+     * Get the fetch body flag
+     *
      * @return boolean
      */
     public function getFetchBody(): bool {
@@ -833,7 +862,9 @@ public function getFetchBody(): bool {
     }
 
     /**
+     * Set the fetch body flag
      * @param boolean $fetch_body
+     *
      * @return Query
      */
     public function setFetchBody(bool $fetch_body): Query {
@@ -842,7 +873,9 @@ public function setFetchBody(bool $fetch_body): Query {
     }
 
     /**
+     * Set the fetch body flag
      * @param boolean $fetch_body
+     *
      * @return Query
      */
     public function fetchBody(bool $fetch_body): Query {
@@ -850,6 +883,8 @@ public function fetchBody(bool $fetch_body): Query {
     }
 
     /**
+     * Get the fetch body flag
+     *
      * @return bool
      */
     public function getFetchFlags(): bool {
@@ -857,7 +892,9 @@ public function getFetchFlags(): bool {
     }
 
     /**
+     * Set the fetch flag
      * @param bool $fetch_flags
+     *
      * @return Query
      */
     public function setFetchFlags(bool $fetch_flags): Query {
@@ -866,7 +903,9 @@ public function setFetchFlags(bool $fetch_flags): Query {
     }
 
     /**
+     * Set the fetch order
      * @param string $fetch_order
+     *
      * @return Query
      */
     public function setFetchOrder(string $fetch_order): Query {
@@ -880,7 +919,9 @@ public function setFetchOrder(string $fetch_order): Query {
     }
 
     /**
+     * Set the fetch order
      * @param string $fetch_order
+     *
      * @return Query
      */
     public function fetchOrder(string $fetch_order): Query {
@@ -888,6 +929,8 @@ public function fetchOrder(string $fetch_order): Query {
     }
 
     /**
+     * Get the fetch order
+     *
      * @return string
      */
     public function getFetchOrder(): string {
@@ -895,6 +938,8 @@ public function getFetchOrder(): string {
     }
 
     /**
+     * Set the fetch order to ascending
+     *
      * @return Query
      */
     public function setFetchOrderAsc(): Query {
@@ -902,6 +947,8 @@ public function setFetchOrderAsc(): Query {
     }
 
     /**
+     * Set the fetch order to ascending
+     *
      * @return Query
      */
     public function fetchOrderAsc(): Query {
@@ -909,6 +956,8 @@ public function fetchOrderAsc(): Query {
     }
 
     /**
+     * Set the fetch order to descending
+     *
      * @return Query
      */
     public function setFetchOrderDesc(): Query {
@@ -916,6 +965,8 @@ public function setFetchOrderDesc(): Query {
     }
 
     /**
+     * Set the fetch order to descending
+     *
      * @return Query
      */
     public function fetchOrderDesc(): Query {
@@ -923,18 +974,20 @@ public function fetchOrderDesc(): Query {
     }
 
     /**
-     * @return Query
+     * Set soft fail mode
      * @var boolean $state
      *
+     * @return Query
      */
     public function softFail(bool $state = true): Query {
         return $this->setSoftFail($state);
     }
 
     /**
-     * @return Query
-     * @var boolean $state
+     * Set soft fail mode
      *
+     * @var boolean $state
+     * @return Query
      */
     public function setSoftFail(bool $state = true): Query {
         $this->soft_fail = $state;
@@ -943,6 +996,8 @@ public function setSoftFail(bool $state = true): Query {
     }
 
     /**
+     * Get soft fail mode
+     *
      * @return boolean
      */
     public function getSoftFail(): bool {
@@ -973,9 +1028,9 @@ protected function setError(int $uid, Exception $error): void {
 
     /**
      * Check if there are any errors / exceptions present
-     * @return boolean
      * @var ?integer $uid
      *
+     * @return boolean
      */
     public function hasErrors(?int $uid = null): bool {
         if ($uid !== null) {
@@ -986,9 +1041,9 @@ public function hasErrors(?int $uid = null): bool {
 
     /**
      * Check if there is an error / exception present
-     * @return boolean
      * @var integer $uid
      *
+     * @return boolean
      */
     public function hasError(int $uid): bool {
         return isset($this->errors[$uid]);
@@ -1014,9 +1069,9 @@ public function getErrors(): array {
 
     /**
      * Get a specific error / exception
-     * @return Exception|null
      * @var integer $uid
      *
+     * @return Exception|null
      */
     public function error(int $uid): ?Exception {
         return $this->getError($uid);
@@ -1024,9 +1079,9 @@ public function error(int $uid): ?Exception {
 
     /**
      * Get a specific error / exception
-     * @return ?Exception
      * @var integer $uid
      *
+     * @return ?Exception
      */
     public function getError(int $uid): ?Exception {
         if ($this->hasError($uid)) {

From f6cfe241c85eb2286284deb74d4e720e1cd45125 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 02:31:04 +0100
Subject: [PATCH 035/203] Method added to check if a message has a specific
 flag

---
 CHANGELOG.md    |  1 +
 src/Message.php | 11 +++++++++++
 2 files changed, 12 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9fe4c08f..bb36bdac 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
 
 ### Added
 - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357)
+- `Message::hasFlag()` method added to check if a message has a specific flag
 
 ### Breaking changes
 - NaN
diff --git a/src/Message.php b/src/Message.php
index f0253dc9..b1e65129 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -1395,6 +1395,17 @@ public function flags(): FlagCollection {
         return $this->getFlags();
     }
 
+    /**
+     * Check if a flag is set
+     *
+     * @param string $flag
+     * @return boolean
+     */
+    public function hasFlag(string $flag): bool {
+        $flag = str_replace("\\", "", strtolower($flag));
+        return $this->getFlags()->has($flag);
+    }
+
     /**
      * Get the fetched structure
      *

From f3c0c06d5ffa9d537a6e6dff1910be353942ba7b Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 02:34:21 +0100
Subject: [PATCH 036/203] config parameter type set to array

---
 CHANGELOG.md    | 1 +
 src/Message.php | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb36bdac..154ca9e9 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
 - IMAP Quota root command fixed
 - Prevent line-breaks in folder path caused by special chars
 - Partial fix for #362 (allow overview response to be empty)
+- `Message::setConfig()` config parameter type set to array
 
 ### Added
 - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357)
diff --git a/src/Message.php b/src/Message.php
index b1e65129..cf1b1690 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -1507,7 +1507,7 @@ public function setFolderPath($folder_path): Message {
      *
      * @return Message
      */
-    public function setConfig($config): Message {
+    public function setConfig(array $config): Message {
         $this->config = $config;
 
         return $this;

From 6ce11f950647363879392710eb4b53296dadd7a6 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 02:35:09 +0100
Subject: [PATCH 037/203] method added to get the current message configuration

---
 src/Message.php | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/Message.php b/src/Message.php
index cf1b1690..b878746d 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -1513,6 +1513,15 @@ public function setConfig(array $config): Message {
         return $this;
     }
 
+    /**
+     * Get the config
+     *
+     * @return array
+     */
+    public function getConfig(): array {
+        return $this->config;
+    }
+
     /**
      * Set the available flags
      * @param $available_flags

From c240831fb097875de5d7fc1ec1d3a5ef6943ce55 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 02:35:49 +0100
Subject: [PATCH 038/203] use default encoding detection method

---
 src/Message.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Message.php b/src/Message.php
index b878746d..b51693e0 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -892,7 +892,7 @@ public function getEncoding(object|string $structure): string {
         } elseif (property_exists($structure, 'charset')) {
             return EncodingAliases::get($structure->charset, "ISO-8859-2");
         } elseif (is_string($structure) === true) {
-            return mb_detect_encoding($structure);
+            return EncodingAliases::detectEncoding($structure);
         }
 
         return 'UTF-8';
@@ -1503,7 +1503,7 @@ public function setFolderPath($folder_path): Message {
 
     /**
      * Set the config
-     * @param $config
+     * @param array $config
      *
      * @return Message
      */

From 58f685e193b6ae3d74c7d73df5a79bb4cb575829 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 02:36:28 +0100
Subject: [PATCH 039/203] rfc references added

---
 src/Connection/Protocols/ImapProtocol.php | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index 6cdc467a..0e11d028 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -28,6 +28,8 @@
  * Class ImapProtocol
  *
  * @package Webklex\PHPIMAP\Connection\Protocols
+ *
+ * @reference https://www.rfc-editor.org/rfc/rfc2087.txt
  */
 class ImapProtocol extends Protocol {
 
@@ -1144,6 +1146,8 @@ public function noop(): Response {
      * @throws ImapBadRequestException
      * @throws ImapServerErrorException
      * @throws RuntimeException
+     *
+     * @Doc https://www.rfc-editor.org/rfc/rfc2087.txt
      */
     public function getQuota($username): Response {
         $command = "GETQUOTA";
@@ -1161,6 +1165,8 @@ public function getQuota($username): Response {
      * @throws ImapBadRequestException
      * @throws ImapServerErrorException
      * @throws RuntimeException
+     *
+     * @Doc https://www.rfc-editor.org/rfc/rfc2087.txt
      */
     public function getQuotaRoot(string $quota_root = 'INBOX'): Response {
         $command = "GETQUOTAROOT";

From 3b9bcbf185a019b16335c3aa5ce3d38c7ed95d91 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 02:36:51 +0100
Subject: [PATCH 040/203] assertion simplified

---
 tests/issues/Issue379Test.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/issues/Issue379Test.php b/tests/issues/Issue379Test.php
index fdb0436f..99b67921 100644
--- a/tests/issues/Issue379Test.php
+++ b/tests/issues/Issue379Test.php
@@ -55,7 +55,7 @@ public function testIssue(): void {
         $this->assertEquals(214, $message->getSize());
 
         // Clean up
-        $this->assertEquals(true, $message->delete(true));
+        $this->assertTrue($message->delete(true));
     }
 
 }
\ No newline at end of file

From aa233eeee48ecd13c1dc1a02d3fe277850398b87 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:20:12 +0100
Subject: [PATCH 041/203] method added to select a folder (send SELECT
 folder_name)

---
 src/Folder.php | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/src/Folder.php b/src/Folder.php
index a1452a30..ddae8525 100755
--- a/src/Folder.php
+++ b/src/Folder.php
@@ -528,6 +528,21 @@ public function examine(): array {
         return $this->client->getConnection()->examineFolder($this->path)->validatedData();
     }
 
+    /**
+     * Select the current folder
+     *
+     * @return array
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function select(): array {
+        return $this->client->getConnection()->selectFolder($this->path)->validatedData();
+    }
+
     /**
      * Get the current Client instance
      *

From 036ee2f6c393b54b64dfba0662d3ebbaebd58469 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:21:07 +0100
Subject: [PATCH 042/203] Treat `in_reply_to` header as address

---
 src/Header.php | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/Header.php b/src/Header.php
index 3e437aed..61b37c79 100644
--- a/src/Header.php
+++ b/src/Header.php
@@ -263,7 +263,16 @@ public function rfc822_parse_headers($raw_headers): object {
         }
 
         foreach ($headers as $key => $values) {
-            if (isset($imap_headers[$key])) continue;
+            if (isset($imap_headers[$key])) {
+                switch ((string)$key) {
+                    case 'in_reply_to':
+                        $value = $this->decodeAddresses($values);
+                        $imap_headers[$key . "address"] = implode(", ", $values);
+                        $imap_headers[$key] = $value;
+                        break;
+                }
+                continue;
+            }
             $value = null;
             switch ((string)$key) {
                 case 'from':
@@ -271,6 +280,7 @@ public function rfc822_parse_headers($raw_headers): object {
                 case 'cc':
                 case 'bcc':
                 case 'reply_to':
+                case 'in_reply_to':
                 case 'sender':
                     $value = $this->decodeAddresses($values);
                     $headers[$key . "address"] = implode(", ", $values);
@@ -521,7 +531,7 @@ private function decodeAddresses($values): array {
      * @param object $header
      */
     private function extractAddresses(object $header): void {
-        foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'sender'] as $key) {
+        foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'in_reply_to', 'sender'] as $key) {
             if (property_exists($header, $key)) {
                 $this->set($key, $this->parseAddresses($header->$key));
             }

From 9a0a837d1212b6a0f227393e0d92a02b9eac15a8 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:21:52 +0100
Subject: [PATCH 043/203] Reset the protocol uid cache if the session gets
 expunged

---
 src/Connection/Protocols/ImapProtocol.php | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php
index 0e11d028..b477b103 100644
--- a/src/Connection/Protocols/ImapProtocol.php
+++ b/src/Connection/Protocols/ImapProtocol.php
@@ -1122,6 +1122,7 @@ public function unsubscribeFolder(string $folder): Response {
      * @throws RuntimeException
      */
     public function expunge(): Response {
+        $this->uid_cache = [];
         return $this->requestAndResponse('EXPUNGE');
     }
 

From f5ca2083508cda0e694a89d2abcafad33d733dbb Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:23:15 +0100
Subject: [PATCH 044/203] Set the "seen" only if the flag isn't set and the
 fetch option isn't IMAP::FT_PEEK

---
 src/Message.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Message.php b/src/Message.php
index b51693e0..7b1885ce 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -641,7 +641,7 @@ public function peek(): void {
             if ($this->getFlags()->get("seen") == null) {
                 $this->unsetFlag("Seen");
             }
-        } elseif ($this->getFlags()->get("seen") != null) {
+        } elseif ($this->getFlags()->get("seen") == null) {
             $this->setFlag("Seen");
         }
     }

From 751d4687947f909efbe27fc94ad02eb50562c070 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:24:13 +0100
Subject: [PATCH 045/203] "Is" date comparison fixed

---
 src/Message.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Message.php b/src/Message.php
index 7b1885ce..042748b3 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -1429,7 +1429,7 @@ public function is(Message $message = null): bool {
         return $this->uid == $message->uid
             && $this->message_id->first() == $message->message_id->first()
             && $this->subject->first() == $message->subject->first()
-            && $this->date->toDate()->eq($message->date);
+            && $this->date->toDate()->eq($message->date->toDate());
     }
 
     /**

From 29f37aa2dd1d1665d626d1343499e7f882b75c76 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:25:09 +0100
Subject: [PATCH 046/203] method added to get all available flags

---
 src/Message.php | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/Message.php b/src/Message.php
index 042748b3..84564ba9 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -1534,6 +1534,15 @@ public function setAvailableFlags($available_flags): Message {
         return $this;
     }
 
+    /**
+     * Get the available flags
+     *
+     * @return array
+     */
+    public function getAvailableFlags(): array {
+        return $this->available_flags;
+    }
+
     /**
      * Set the attachment collection
      * @param $attachments

From 761bf2de690eb92862c0fafa54548bb368701f85 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:26:26 +0100
Subject: [PATCH 047/203] $client should be nullable

---
 src/Message.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Message.php b/src/Message.php
index 84564ba9..fd77b444 100755
--- a/src/Message.php
+++ b/src/Message.php
@@ -1581,7 +1581,7 @@ public function setFlags($flags): Message {
      */
     public function setClient($client): Message {
         $this->client = $client;
-        $this->client->openFolder($this->folder_path);
+        $this->client?->openFolder($this->folder_path);
 
         return $this;
     }

From f97ae8693e0cabe8d3ebc2d8ea0328bc55809b4c Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:26:50 +0100
Subject: [PATCH 048/203] select a folder instead of examining it

---
 tests/live/LiveMailboxTestCase.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/live/LiveMailboxTestCase.php b/tests/live/LiveMailboxTestCase.php
index 562c3809..c59e6773 100644
--- a/tests/live/LiveMailboxTestCase.php
+++ b/tests/live/LiveMailboxTestCase.php
@@ -141,7 +141,7 @@ final protected function getFolder(string $folder_path = "INDEX"): Folder {
      * @throws RuntimeException
      */
     final protected function appendMessage(Folder $folder, string $message): Message {
-        $status = $folder->examine();
+        $status = $folder->select();
         if (!isset($status['uidnext'])) {
             $this->fail("No UIDNEXT returned");
         }

From 58c1697cfb1361dc07dcd126a48aa0e2f154d7fe Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:27:09 +0100
Subject: [PATCH 049/203] Number of attributes fixed

---
 tests/HeaderTest.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php
index 7afec065..daf4303b 100644
--- a/tests/HeaderTest.php
+++ b/tests/HeaderTest.php
@@ -71,7 +71,7 @@ public function testHeaderParsing(): void {
         self::assertInstanceOf(Carbon::class, $date);
         self::assertSame("2022-12-26 08:07:14 GMT-0800", $date->format("Y-m-d H:i:s T"));
 
-        self::assertSame(48, count($header->getAttributes()));
+        self::assertSame(49, count($header->getAttributes()));
     }
 
     public function testRfc822ParseHeaders() {

From a4b3f4375bc14657bf6d2392eee469ae5f1f351f Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:27:44 +0100
Subject: [PATCH 050/203] Overview test improved

---
 tests/live/FolderTest.php | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/tests/live/FolderTest.php b/tests/live/FolderTest.php
index 75d7774b..78da7017 100644
--- a/tests/live/FolderTest.php
+++ b/tests/live/FolderTest.php
@@ -245,20 +245,23 @@ public function testOverview(): void {
         $folder = $this->getFolder('INBOX');
         self::assertInstanceOf(Folder::class, $folder);
 
+        $folder->select();
+
         // Test empty overview
         $overview = $folder->overview();
         self::assertIsArray($overview);
+        self::assertCount(0, $overview);
 
         $message = $this->appendMessageTemplate($folder, "plain.eml");
-        self::assertInstanceOf(Message::class, $message);
 
         $overview = $folder->overview();
+
         self::assertIsArray($overview);
+        self::assertCount(1, $overview);
 
-        self::assertEquals($message->from, end($overview)["from"]);
+        self::assertEquals($message->from->first()->full, end($overview)["from"]->toString());
 
-        // Clean up
-        $this->assertTrue($message->delete(true));
+        self::assertTrue($message->delete());
     }
 
     /**

From 2b654a875bb77775902f9653b032bad1d4db6b69 Mon Sep 17 00:00:00 2001
From: webklex 
Date: Wed, 8 Mar 2023 21:28:03 +0100
Subject: [PATCH 051/203] Live Message tests added

---
 tests/live/MessageTest.php | 2355 ++++++++++++++++++++++++++++++++++++
 1 file changed, 2355 insertions(+)
 create mode 100644 tests/live/MessageTest.php

diff --git a/tests/live/MessageTest.php b/tests/live/MessageTest.php
new file mode 100644
index 00000000..cacb894f
--- /dev/null
+++ b/tests/live/MessageTest.php
@@ -0,0 +1,2355 @@
+getFolder('INBOX');
+
+        $message = $this->appendMessageTemplate($folder, "plain.eml");
+        self::assertInstanceOf(Message::class, $message);
+
+        return $message;
+    }
+
+    /**
+     * Test Message::convertEncoding()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws ResponseException
+     * @throws RuntimeException
+     * @throws MessageNotFoundException
+     */
+    public function testConvertEncoding(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals("Entwürfe+", $message->convertEncoding("Entw&APw-rfe+", "UTF7-IMAP", "UTF-8"));
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::hasAttachments()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testHasAttachments(): void {
+        $message = $this->getDefaultMessage();
+        self::assertFalse($message->hasAttachments());
+
+        $folder = $message->getFolder();
+        self::assertInstanceOf(Folder::class, $folder);
+        self::assertTrue($message->delete());
+
+        $message = $this->appendMessageTemplate($folder, "example_attachment.eml");
+        self::assertInstanceOf(Message::class, $message);
+        self::assertTrue($message->hasAttachments());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getFetchOptions()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetFetchOptions(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals(IMAP::FT_PEEK, $message->getFetchOptions());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getMessageId()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetMessageId(): void {
+        $folder = $this->getFolder('INBOX');
+        $message = $this->appendMessageTemplate($folder, "example_attachment.eml");
+        self::assertInstanceOf(Message::class, $message);
+
+        self::assertEquals("d3a5e91963cb805cee975687d5acb1c6@swift.generated", $message->getMessageId());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getReplyTo()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetReplyTo(): void {
+        $folder = $this->getFolder('INBOX');
+        $message = $this->appendMessageTemplate($folder, "example_attachment.eml");
+        self::assertInstanceOf(Message::class, $message);
+
+        self::assertEquals("testreply_to ", $message->getReplyTo());
+        self::assertEquals("someone@domain.tld", $message->getReplyTo()->first()->mail);
+        self::assertEquals("testreply_to", $message->getReplyTo()->first()->personal);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::setSequence()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetSequence(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals($message->uid, $message->getSequenceId());
+
+        $message->setSequence(IMAP::ST_MSGN);
+        self::assertEquals($message->msgn, $message->getSequenceId());
+
+        $message->setSequence(null);
+        self::assertEquals($message->uid, $message->getSequenceId());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getEvent()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetEvent(): void {
+        $message = $this->getDefaultMessage();
+
+        $message->setEvent("message", "test", "test");
+        self::assertEquals("test", $message->getEvent("message", "test"));
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::__construct()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function test__construct(): void {
+        $message = $this->getDefaultMessage();
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::setFlag()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetFlag(): void {
+        $message = $this->getDefaultMessage();
+
+        self::assertTrue($message->setFlag("seen"));
+        self::assertTrue($message->getFlags()->has("seen"));
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getMsgn()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetMsgn(): void {
+        $client = $this->getClient();
+
+        $delimiter = $this->getManager()->get("options.delimiter");
+        $folder_path = implode($delimiter, ['INBOX', 'test']);
+
+        $folder = $client->getFolder($folder_path);
+        if ($folder !== null) {
+            self::assertTrue($this->deleteFolder($folder));
+        }
+        $folder = $client->createFolder($folder_path, false);
+
+        $message = $this->appendMessageTemplate($folder, "plain.eml");
+        self::assertInstanceOf(Message::class, $message);
+
+        self::assertEquals(1, $message->getMsgn());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+        self::assertTrue($this->deleteFolder($folder));
+    }
+
+    /**
+     * Test Message::peek()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testPeek(): void {
+        $message = $this->getDefaultMessage();
+        self::assertFalse($message->getFlags()->has("seen"));
+        self::assertEquals(IMAP::FT_PEEK, $message->getFetchOptions());
+        $message->peek();
+        self::assertFalse($message->getFlags()->has("seen"));
+
+        $message->setFetchOption(IMAP::FT_UID);
+        self::assertEquals(IMAP::FT_UID, $message->getFetchOptions());
+        $message->peek();
+        self::assertTrue($message->getFlags()->has("seen"));
+
+        // Cleanup
+        self::assertTrue($message->delete());
+
+    }
+
+    /**
+     * Test Message::unsetFlag()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testUnsetFlag(): void {
+        $message = $this->getDefaultMessage();
+
+        self::assertFalse($message->getFlags()->has("seen"));
+
+        self::assertTrue($message->setFlag("seen"));
+        self::assertTrue($message->getFlags()->has("seen"));
+
+        self::assertTrue($message->unsetFlag("seen"));
+        self::assertFalse($message->getFlags()->has("seen"));
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::setSequenceId()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetSequenceId(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals($message->uid, $message->getSequenceId());
+
+        $original_sequence = $message->getSequenceId();
+
+        $message->setSequenceId(1, IMAP::ST_MSGN);
+        self::assertEquals(1, $message->getSequenceId());
+
+        $message->setSequenceId(1);
+        self::assertEquals(1, $message->getSequenceId());
+
+        $message->setSequenceId($original_sequence);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+
+    }
+
+    /**
+     * Test Message::getTo()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetTo(): void {
+        $message = $this->getDefaultMessage();
+        $folder = $message->getFolder();
+        self::assertInstanceOf(Folder::class, $folder);
+
+        self::assertEquals("to@someone-else.com", $message->getTo());
+        self::assertTrue($message->delete());
+
+        $message = $this->appendMessageTemplate($folder, "example_attachment.eml");
+        self::assertInstanceOf(Message::class, $message);
+
+        self::assertEquals("testnameto ", $message->getTo());
+        self::assertEquals("testnameto", $message->getTo()->first()->personal);
+        self::assertEquals("someone@domain.tld", $message->getTo()->first()->mail);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::setUid()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetUid(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals($message->uid, $message->getSequenceId());
+
+        $original_sequence = $message->getSequenceId();
+
+        $message->setUid(789);
+        self::assertEquals(789, $message->uid);
+
+        $message->setUid($original_sequence);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+
+    }
+
+    /**
+     * Test Message::getUid()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetUid(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals($message->uid, $message->getSequenceId());
+
+        $original_sequence = $message->uid;
+
+        $message->setUid(789);
+        self::assertEquals(789, $message->uid);
+        self::assertEquals(789, $message->getUid());
+
+        $message->setUid($original_sequence);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::hasTextBody()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testHasTextBody(): void {
+        $message = $this->getDefaultMessage();
+        self::assertTrue($message->hasTextBody());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::__get()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function test__get(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals($message->uid, $message->getSequenceId());
+        self::assertEquals("Example", $message->subject);
+        self::assertEquals("to@someone-else.com", $message->to);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getDate()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetDate(): void {
+        $message = $this->getDefaultMessage();
+        self::assertInstanceOf(Carbon::class, $message->getDate()->toDate());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::setMask()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetMask(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals(MessageMask::class, $message->getMask());
+
+        $message->setMask(AttachmentMask::class);
+        self::assertEquals(AttachmentMask::class, $message->getMask());
+
+        $message->setMask(MessageMask::class);
+        self::assertEquals(MessageMask::class, $message->getMask());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getSequenceId()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetSequenceId(): void {
+        $message = $this->getDefaultMessage();
+        self::assertEquals($message->uid, $message->getSequenceId());
+
+        $original_sequence = $message->getSequenceId();
+
+        $message->setSequenceId(789, IMAP::ST_MSGN);
+        self::assertEquals(789, $message->getSequenceId());
+
+        $message->setSequenceId(789);
+        self::assertEquals(789, $message->getSequenceId());
+
+        $message->setSequenceId($original_sequence);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::setConfig()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetConfig(): void {
+        $message = $this->getDefaultMessage();
+
+        $config = $message->getConfig();
+        self::assertIsArray($config);
+
+        $message->setConfig(["foo" => "bar"]);
+        self::assertArrayHasKey("foo", $message->getConfig());
+
+        $message->setConfig($config);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getEvents()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetEvents(): void {
+        $message = $this->getDefaultMessage();
+
+        $events = $message->getEvents();
+        self::assertIsArray($events);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::setFetchOption()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testSetFetchOption(): void {
+        $message = $this->getDefaultMessage();
+
+        $fetch_option = $message->fetch_options;
+
+        $message->setFetchOption(IMAP::FT_UID);
+        self::assertEquals(IMAP::FT_UID, $message->fetch_options);
+
+        $message->setFetchOption(IMAP::FT_PEEK);
+        self::assertEquals(IMAP::FT_PEEK, $message->fetch_options);
+
+        $message->setFetchOption(IMAP::FT_UID | IMAP::FT_PEEK);
+        self::assertEquals(IMAP::FT_UID | IMAP::FT_PEEK, $message->fetch_options);
+
+        $message->setFetchOption($fetch_option);
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::getMsglist()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testGetMsglist(): void {
+        $message = $this->getDefaultMessage();
+
+        self::assertEquals(0, (int)$message->getMsglist()->toString());
+
+        // Cleanup
+        self::assertTrue($message->delete());
+    }
+
+    /**
+     * Test Message::decodeString()
+     *
+     * @return void
+     * @throws AuthFailedException
+     * @throws ConnectionFailedException
+     * @throws EventNotFoundException
+     * @throws FolderFetchingException
+     * @throws ImapBadRequestException
+     * @throws ImapServerErrorException
+     * @throws InvalidMessageDateException
+     * @throws MaskNotFoundException
+     * @throws MessageContentFetchingException
+     * @throws MessageFlagException
+     * @throws MessageHeaderFetchingException
+     * @throws MessageNotFoundException
+     * @throws ResponseException
+     * @throws RuntimeException
+     */
+    public function testDecodeString(): void {
+        $message = $this->getDefaultMessage();
+
+        $string = '

Test

'; + self::assertEquals('

Test

', $message->decodeString($string, IMAP::MESSAGE_ENC_QUOTED_PRINTABLE)); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::attachments() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testAttachments(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "example_attachment.eml"); + self::assertTrue($message->hasAttachments()); + self::assertSameSize([1], $message->attachments()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getMask() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetMask(): void { + $message = $this->getDefaultMessage(); + self::assertEquals(MessageMask::class, $message->getMask()); + + $message->setMask(AttachmentMask::class); + self::assertEquals(AttachmentMask::class, $message->getMask()); + + $message->setMask(MessageMask::class); + self::assertEquals(MessageMask::class, $message->getMask()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::hasHTMLBody() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testHasHTMLBody(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "1366671050@github.com.eml"); + self::assertTrue($message->hasHTMLBody()); + + // Cleanup + self::assertTrue($message->delete()); + + $message = $this->getDefaultMessage(); + self::assertFalse($message->hasHTMLBody()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setEvents() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetEvents(): void { + $message = $this->getDefaultMessage(); + + $events = $message->getEvents(); + self::assertIsArray($events); + + $message->setEvents(["foo" => "bar"]); + self::assertArrayHasKey("foo", $message->getEvents()); + + $message->setEvents($events); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::__set() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function test__set(): void { + $message = $this->getDefaultMessage(); + + $message->foo = "bar"; + self::assertEquals("bar", $message->getFoo()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getHTMLBody() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetHTMLBody(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "1366671050@github.com.eml"); + self::assertTrue($message->hasHTMLBody()); + self::assertIsString($message->getHTMLBody()); + + // Cleanup + self::assertTrue($message->delete()); + + $message = $this->getDefaultMessage(); + self::assertFalse($message->hasHTMLBody()); + self::assertEmpty($message->getHTMLBody()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getSequence() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetSequence(): void { + $message = $this->getDefaultMessage(); + self::assertEquals(IMAP::ST_UID, $message->getSequence()); + + $original_sequence = $message->getSequence(); + + $message->setSequence(IMAP::ST_MSGN); + self::assertEquals(IMAP::ST_MSGN, $message->getSequence()); + + $message->setSequence($original_sequence); + self::assertEquals(IMAP::ST_UID, $message->getSequence()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::restore() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testRestore(): void { + $message = $this->getDefaultMessage(); + + $message->setFlag("deleted"); + self::assertTrue($message->hasFlag("deleted")); + + $message->restore(); + self::assertFalse($message->hasFlag("deleted")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getPriority() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetPriority(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "example_attachment.eml"); + self::assertEquals(1, $message->getPriority()->first()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setAttachments() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetAttachments(): void { + $message = $this->getDefaultMessage(); + + $message->setAttachments(new AttachmentCollection(["foo" => "bar"])); + self::assertIsArray($message->attachments()->toArray()); + self::assertTrue($message->attachments()->has("foo")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFrom() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFrom(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("from@someone.com", $message->getFrom()->first()->mail); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setEvent() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetEvent(): void { + $message = $this->getDefaultMessage(); + + $message->setEvent("message", "bar", "foo"); + self::assertArrayHasKey("bar", $message->getEvents()["message"]); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getInReplyTo() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetInReplyTo(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("", $message->getInReplyTo()); + + // Cleanup + self::assertTrue($message->delete()); + + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "1366671050@github.com.eml"); + self::assertEquals("Webklex/php-imap/issues/349@github.com", $message->getInReplyTo()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::copy() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testCopy(): void { + $message = $this->getDefaultMessage(); + $client = $message->getClient(); + self::assertInstanceOf(Client::class, $client); + + $delimiter = $this->getManager()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'test']); + + $folder = $client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = $client->createFolder($folder_path, false); + self::assertInstanceOf(Folder::class, $folder); + + $new_message = $message->copy($folder->path, true); + self::assertInstanceOf(Message::class, $new_message); + self::assertEquals($folder->path, $new_message->getFolder()->path); + + // Cleanup + self::assertTrue($message->delete()); + self::assertTrue($new_message->delete()); + + } + + /** + * Test Message::getBodies() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetBodies(): void { + $message = $this->getDefaultMessage(); + self::assertIsArray($message->getBodies()); + self::assertCount(1, $message->getBodies()); + + // Cleanup + self::assertTrue($message->delete()); + + } + + /** + * Test Message::getFlags() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFlags(): void { + $message = $this->getDefaultMessage(); + self::assertIsArray($message->getFlags()->all()); + + self::assertFalse($message->hasFlag("seen")); + + self::assertTrue($message->setFlag("seen")); + self::assertTrue($message->getFlags()->has("seen")); + self::assertTrue($message->hasFlag("seen")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::addFlag() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testAddFlag(): void { + $message = $this->getDefaultMessage(); + self::assertFalse($message->hasFlag("seen")); + + self::assertTrue($message->addFlag("seen")); + self::assertTrue($message->hasFlag("seen")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getSubject() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetSubject(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("Example", $message->getSubject()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getClient() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetClient(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(Client::class, $message->getClient()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setFetchFlagsOption() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetFetchFlagsOption(): void { + $message = $this->getDefaultMessage(); + + self::assertTrue($message->getFetchFlagsOption()); + $message->setFetchFlagsOption(false); + self::assertFalse($message->getFetchFlagsOption()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::mask() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testMask(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(MessageMask::class, $message->mask()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setMsglist() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetMsglist(): void { + $message = $this->getDefaultMessage(); + $message->setMsglist("foo"); + self::assertEquals("foo", $message->getMsglist()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::flags() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testFlags(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(FlagCollection::class, $message->flags()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getAttributes() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetAttributes(): void { + $message = $this->getDefaultMessage(); + self::assertIsArray($message->getAttributes()); + self::assertArrayHasKey("subject", $message->getAttributes()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getAttachments() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetAttachments(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(AttachmentCollection::class, $message->getAttachments()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getRawBody() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetRawBody(): void { + $message = $this->getDefaultMessage(); + self::assertIsString($message->getRawBody()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::is() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testIs(): void { + $message = $this->getDefaultMessage(); + self::assertTrue($message->is($message)); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setFlags() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetFlags(): void { + $message = $this->getDefaultMessage(); + $message->setFlags(new FlagCollection()); + self::assertFalse($message->hasFlag("recent")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::make() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws ResponseException + * @throws RuntimeException + * @throws ReflectionException + */ + public function testMake(): void { + $folder = $this->getFolder('INBOX'); + $folder->getClient()->openFolder($folder->path); + + $email = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "1366671050@github.com.eml"])); + if(!str_contains($email, "\r\n")){ + $email = str_replace("\n", "\r\n", $email); + } + + $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); + $raw_body = substr($email, strlen($raw_header)+8); + + $message = Message::make(0, null, $folder->getClient(), $raw_header, $raw_body, [0 => "\\Seen"], IMAP::ST_UID); + self::assertInstanceOf(Message::class, $message); + } + + /** + * Test Message::setAvailableFlags() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetAvailableFlags(): void { + $message = $this->getDefaultMessage(); + + $message->setAvailableFlags(["foo"]); + self::assertSameSize(["foo"], $message->getAvailableFlags()); + self::assertEquals("foo", $message->getAvailableFlags()[0]); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getSender() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetSender(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "example_attachment.eml"); + self::assertEquals("testsender ", $message->getSender()); + self::assertEquals("testsender", $message->getSender()->first()->personal); + self::assertEquals("someone@domain.tld", $message->getSender()->first()->mail); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::fromFile() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException + */ + public function testFromFile(): void { + $this->getManager(); + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "1366671050@github.com.eml"]); + $message = Message::fromFile($filename); + self::assertInstanceOf(Message::class, $message); + } + + /** + * Test Message::getStructure() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetStructure(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(Structure::class, $message->getStructure()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::get() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + * @throws MessageSizeFetchingException + */ + public function testGet(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("Example", $message->get("subject")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getSize() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetSize(): void { + $message = $this->getDefaultMessage(); + self::assertEquals(214, $message->getSize()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getHeader() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetHeader(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(Header::class, $message->getHeader()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getReferences() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetReferences(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "1366671050@github.com.eml"); + self::assertIsArray($message->getReferences()->all()); + self::assertEquals("", $message->getReferences()->first()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setFolderPath() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetFolderPath(): void { + $message = $this->getDefaultMessage(); + + $folder_path = $message->getFolderPath(); + + $message->setFolderPath("foo"); + self::assertEquals("foo", $message->getFolderPath()); + + $message->setFolderPath($folder_path); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getTextBody() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetTextBody(): void { + $message = $this->getDefaultMessage(); + self::assertIsString($message->getTextBody()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::move() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testMove(): void { + $message = $this->getDefaultMessage(); + $client = $message->getClient(); + self::assertInstanceOf(Client::class, $client); + + $delimiter = $this->getManager()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'test']); + + $folder = $client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = $client->createFolder($folder_path, false); + self::assertInstanceOf(Folder::class, $folder); + + $message = $message->move($folder->path, true); + self::assertInstanceOf(Message::class, $message); + self::assertEquals($folder->path, $message->getFolder()->path); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFolderPath() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFolderPath(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("INBOX", $message->getFolderPath()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFolder() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFolder(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(Folder::class, $message->getFolder()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFetchBodyOption() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFetchBodyOption(): void { + $message = $this->getDefaultMessage(); + self::assertTrue($message->getFetchBodyOption()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setFetchBodyOption() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetFetchBodyOption(): void { + $message = $this->getDefaultMessage(); + + $message->setFetchBodyOption(false); + self::assertFalse($message->getFetchBodyOption()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFetchFlagsOption() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFetchFlagsOption(): void { + $message = $this->getDefaultMessage(); + self::assertTrue($message->getFetchFlagsOption()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::__call() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function test__call(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("Example", $message->getSubject()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setClient() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetClient(): void { + $message = $this->getDefaultMessage(); + $client = $message->getClient(); + self::assertInstanceOf(Client::class, $client); + + $message->setClient(null); + self::assertNull($message->getClient()); + + $message->setClient($client); + self::assertInstanceOf(Client::class, $message->getClient()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setMsgn() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetMsgn(): void { + $message = $this->getDefaultMessage(); + + $uid = $message->getUid(); + $message->setMsgn(789); + self::assertEquals(789, $message->getMsgn()); + $message->setUid($uid); + + // Cleanup + self::assertTrue($message->delete()); + } +} From f16984afcf5d86f3daeb58be3632e21b889d8d3a Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 8 Mar 2023 21:28:37 +0100 Subject: [PATCH 052/203] Changelog updated --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 154ca9e9..4720852e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,19 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Prevent line-breaks in folder path caused by special chars - Partial fix for #362 (allow overview response to be empty) - `Message::setConfig()` config parameter type set to array +- Treat `in_reply_to` header as address +- Reset the protocol uid cache if the session gets expunged +- Set the "seen" flag only if the flag isn't set and the fetch option isn't IMAP::FT_PEEK +- `Message::is()` date comparison fixed +- `Message::$client` could not be set to null ### Added - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357) - `Message::hasFlag()` method added to check if a message has a specific flag +- `Message::getConfig()` method added to get the current message configuration +- `Folder::select()` method added to select a folder +- `Message::getAvailableFlags()` method added to get all available flags +- Live mailbox tests added ### Breaking changes - NaN From 2fc3995a5c75d854b096e95188d00226d4e826e9 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:14:50 +0100 Subject: [PATCH 053/203] method added to map all attribute values --- CHANGELOG.md | 1 + src/Attribute.php | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4720852e..d8003383 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Folder::select()` method added to select a folder - `Message::getAvailableFlags()` method added to get all available flags - Live mailbox tests added +- `Attribute::map()` method added to map all attribute values ### Breaking changes - NaN diff --git a/src/Attribute.php b/src/Attribute.php index 7e77a469..c50cab75 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -314,4 +314,12 @@ public function offsetSet(mixed $offset, mixed $value): void { public function offsetUnset(mixed $offset): void { $this->remove($offset); } + + /** + * @param callable $callback + * @return array + */ + public function map(callable $callback): array { + return array_map($callback, $this->values); + } } \ No newline at end of file From b3ffb4a91447302d325bd7e14543986334b0366a Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:16:15 +0100 Subject: [PATCH 054/203] in_reply_to and references parsing fixed --- CHANGELOG.md | 2 +- src/Header.php | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8003383..260cea14 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,11 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Prevent line-breaks in folder path caused by special chars - Partial fix for #362 (allow overview response to be empty) - `Message::setConfig()` config parameter type set to array -- Treat `in_reply_to` header as address - Reset the protocol uid cache if the session gets expunged - Set the "seen" flag only if the flag isn't set and the fetch option isn't IMAP::FT_PEEK - `Message::is()` date comparison fixed - `Message::$client` could not be set to null +- in_reply_to and references parsing fixed ### Added - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357) diff --git a/src/Header.php b/src/Header.php index 61b37c79..fcbf132d 100644 --- a/src/Header.php +++ b/src/Header.php @@ -185,11 +185,16 @@ protected function parse(): void { $this->set("subject", $this->decode($header->subject)); } if (property_exists($header, 'references')) { - $this->set("references", $this->decode($header->references)); + $this->set("references", array_map(function ($item) { + return str_replace(['<', '>'], '', $item); + }, explode(" ", $header->references))); } if (property_exists($header, 'message_id')) { $this->set("message_id", str_replace(['<', '>'], '', $header->message_id)); } + if (property_exists($header, 'in_reply_to')) { + $this->set("in_reply_to", str_replace(['<', '>'], '', $header->in_reply_to)); + } $this->parseDate($header); foreach ($header as $key => $value) { @@ -264,13 +269,6 @@ public function rfc822_parse_headers($raw_headers): object { foreach ($headers as $key => $values) { if (isset($imap_headers[$key])) { - switch ((string)$key) { - case 'in_reply_to': - $value = $this->decodeAddresses($values); - $imap_headers[$key . "address"] = implode(", ", $values); - $imap_headers[$key] = $value; - break; - } continue; } $value = null; @@ -280,7 +278,6 @@ public function rfc822_parse_headers($raw_headers): object { case 'cc': case 'bcc': case 'reply_to': - case 'in_reply_to': case 'sender': $value = $this->decodeAddresses($values); $headers[$key . "address"] = implode(", ", $values); @@ -531,7 +528,7 @@ private function decodeAddresses($values): array { * @param object $header */ private function extractAddresses(object $header): void { - foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'in_reply_to', 'sender'] as $key) { + foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'sender'] as $key) { if (property_exists($header, $key)) { $this->set($key, $this->parseAddresses($header->$key)); } From a590b3490769572cfab2157e38c65cd7fb2dfe1a Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:23:25 +0100 Subject: [PATCH 055/203] Prevent message body parsing from adding empty lines --- CHANGELOG.md | 1 + src/Message.php | 27 ++++++++++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 260cea14..cb510017 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Message::is()` date comparison fixed - `Message::$client` could not be set to null - in_reply_to and references parsing fixed +- Prevent message body parsing from adding empty lines ### Added - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357) diff --git a/src/Message.php b/src/Message.php index fd77b444..38b56c97 100755 --- a/src/Message.php +++ b/src/Message.php @@ -326,7 +326,7 @@ public static function fromFile($filename): Message { $email = str_replace("\n", "\r\n", $email); } $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); - $raw_body = substr($email, strlen($raw_header)+8); + $raw_body = substr($email, strlen($raw_header)+4); $instance->parseRawHeader($raw_header); $instance->parseRawBody($raw_body); @@ -713,14 +713,27 @@ private function fetchPart(Part $part): void { $content = $this->convertEncoding($content, $encoding); } - $subtype = strtolower($part->subtype ?? ''); - $subtype = $subtype == "plain" || $subtype == "" ? "text" : $subtype; + $this->addBody($part->subtype ?? '', $content); + } + } - if (isset($this->bodies[$subtype])) { - $this->bodies[$subtype] .= "\n" . $content; - } else { - $this->bodies[$subtype] = $content; + /** + * Add a body to the message + * @param string $subtype + * @param string $content + * + * @return void + */ + protected function addBody(string $subtype, string $content): void { + $subtype = strtolower($subtype); + $subtype = $subtype == "plain" || $subtype == "" ? "text" : $subtype; + + if (isset($this->bodies[$subtype]) && $this->bodies[$subtype] !== null && $this->bodies[$subtype] !== "") { + if ($content !== "") { + $this->bodies[$subtype] .= "\n".$content; } + } else { + $this->bodies[$subtype] = $content; } } From c1a39e144625d8491fa2349a5b98c37da71a67f9 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:25:53 +0100 Subject: [PATCH 056/203] Don't parse regular inline message parts without name or filename as attachment --- CHANGELOG.md | 1 + src/Part.php | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb510017..2b9cdcdb 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Message::$client` could not be set to null - in_reply_to and references parsing fixed - Prevent message body parsing from adding empty lines +- Don't parse regular inline message parts without name or filename as attachment ### Added - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357) diff --git a/src/Part.php b/src/Part.php index b5c25032..27533e30 100644 --- a/src/Part.php +++ b/src/Part.php @@ -289,6 +289,10 @@ public function isAttachment(): bool { return false; } } + + if ($this->disposition === "inline" && $this->filename == null && $this->name == null) { + return false; + } return true; } From 2c6475d81ba77fd2b73a2cdbd98e7fbf465922a3 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:26:46 +0100 Subject: [PATCH 057/203] Part number fixed --- tests/MessageTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MessageTest.php b/tests/MessageTest.php index eaf6737c..88abd40c 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -180,7 +180,7 @@ public function testLoadMessageFromFile(): void { self::assertSame("znk551MP3TP3WPp9Kl1gnLErrWEgkJFAtvaKqkTgrk3dKI8dX38YT8BaVxRcOERN", $attachment->content); self::assertSame("application/octet-stream", $attachment->content_type); self::assertSame("6mfFxiU5Yhv9WYJx.txt", $attachment->name); - self::assertSame(1, $attachment->part_number); + self::assertSame(2, $attachment->part_number); self::assertSame("text", $attachment->type); self::assertNotEmpty($attachment->id); self::assertSame(90, $attachment->size); From 8ebf333aae0a688af7b2bbb55e68232a849b5024 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:27:09 +0100 Subject: [PATCH 058/203] reference value updated --- tests/live/MessageTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/live/MessageTest.php b/tests/live/MessageTest.php index cacb894f..d7850917 100644 --- a/tests/live/MessageTest.php +++ b/tests/live/MessageTest.php @@ -2016,7 +2016,7 @@ public function testGetReferences(): void { $message = $this->appendMessageTemplate($folder, "1366671050@github.com.eml"); self::assertIsArray($message->getReferences()->all()); - self::assertEquals("", $message->getReferences()->first()); + self::assertEquals("Webklex/php-imap/issues/349@github.com", $message->getReferences()->first()); // Cleanup self::assertTrue($message->delete()); From 3f96ac1a67d3bb65508be102eae866e2055899b9 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:27:20 +0100 Subject: [PATCH 059/203] Missing text added --- tests/issues/Issue275Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/issues/Issue275Test.php b/tests/issues/Issue275Test.php index 3cb5c5de..4049998d 100644 --- a/tests/issues/Issue275Test.php +++ b/tests/issues/Issue275Test.php @@ -22,7 +22,7 @@ public function testIssueEmail1() { $message = Message::fromFile($filename); self::assertSame("Testing 123", (string)$message->subject); - self::assertSame("testing123 this is a body", $message->getTextBody()); + self::assertSame("Asdf testing123 this is a body", $message->getTextBody()); } public function testIssueEmail2() { From 90bf0fc78ea85de31e46862f594e190dfe080e9c Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 06:27:46 +0100 Subject: [PATCH 060/203] Attribute count updated --- tests/HeaderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index daf4303b..7afec065 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -71,7 +71,7 @@ public function testHeaderParsing(): void { self::assertInstanceOf(Carbon::class, $date); self::assertSame("2022-12-26 08:07:14 GMT-0800", $date->format("Y-m-d H:i:s T")); - self::assertSame(49, count($header->getAttributes())); + self::assertSame(48, count($header->getAttributes())); } public function testRfc822ParseHeaders() { From 89183802c8fff5d84931afc7ed574a56fabc3914 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 07:13:24 +0100 Subject: [PATCH 061/203] return false if the body is empty --- CHANGELOG.md | 1 + src/Message.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9cdcdb..a77f6cd6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - in_reply_to and references parsing fixed - Prevent message body parsing from adding empty lines - Don't parse regular inline message parts without name or filename as attachment +- `Message::hasTextBody()` and `Message::hasHtmlBody()` should return `false` if the body is empty ### Added - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357) diff --git a/src/Message.php b/src/Message.php index 38b56c97..157c1f7e 100755 --- a/src/Message.php +++ b/src/Message.php @@ -454,7 +454,7 @@ public function get($name): mixed { * @return bool */ public function hasTextBody(): bool { - return isset($this->bodies['text']); + return isset($this->bodies['text']) && $this->bodies['text'] !== ""; } /** @@ -476,7 +476,7 @@ public function getTextBody(): string { * @return bool */ public function hasHTMLBody(): bool { - return isset($this->bodies['html']); + return isset($this->bodies['html']) && $this->bodies['html'] !== ""; } /** From 8d007516bdb609b800e7cf8189fd81b05ab63043 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 07:13:47 +0100 Subject: [PATCH 062/203] Changelog updated --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a77f6cd6..97d367da 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- Extended UTF-7 support added (RFC2060) #383 - IMAP Quota root command fixed - Prevent line-breaks in folder path caused by special chars - Partial fix for #362 (allow overview response to be empty) @@ -21,6 +20,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Message::hasTextBody()` and `Message::hasHtmlBody()` should return `false` if the body is empty ### Added +- Extended UTF-7 support added (RFC2060) #383 - `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357) - `Message::hasFlag()` method added to check if a message has a specific flag - `Message::getConfig()` method added to get the current message configuration From a56424474c33e18382bc969c70d975c14ab707f0 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 07:42:15 +0100 Subject: [PATCH 063/203] check if a header attribute / value exist --- CHANGELOG.md | 1 + src/Header.php | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97d367da..3bcf6890 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Message::getAvailableFlags()` method added to get all available flags - Live mailbox tests added - `Attribute::map()` method added to map all attribute values +- `Header::has()` method added to check if a header attribute / value exist ### Breaking changes - NaN diff --git a/src/Header.php b/src/Header.php index fcbf132d..56b3587a 100644 --- a/src/Header.php +++ b/src/Header.php @@ -110,6 +110,17 @@ public function get($name): Attribute { return new Attribute($name); } + /** + * Check if a specific attribute exists + * @param string $name + * + * @return bool + */ + public function has(string $name): bool { + $name = str_replace(["-", " "], "_", strtolower($name)); + return isset($this->attributes[$name]); + } + /** * Set a specific attribute * @param string $name From 8d6ef345c5e47d03295a57de6f2a432642fa9802 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 9 Mar 2023 07:43:39 +0100 Subject: [PATCH 064/203] part attributes are now accessible via linked attribute --- CHANGELOG.md | 1 + src/Attachment.php | 2 ++ src/Part.php | 11 ++++++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bcf6890..1af4be9b 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Live mailbox tests added - `Attribute::map()` method added to map all attribute values - `Header::has()` method added to check if a header attribute / value exist +- All part attributes are now accessible via linked attribute ### Breaking changes - NaN diff --git a/src/Attachment.php b/src/Attachment.php index c6afe30f..a6e10430 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -223,6 +223,8 @@ protected function fetch(): void { $this->setName($this->part->subtype); } } + + $this->attributes = array_merge($this->part->getHeader()->getAttributes(), $this->attributes); } /** diff --git a/src/Part.php b/src/Part.php index 27533e30..1759b8de 100644 --- a/src/Part.php +++ b/src/Part.php @@ -290,10 +290,19 @@ public function isAttachment(): bool { } } - if ($this->disposition === "inline" && $this->filename == null && $this->name == null) { + if ($this->disposition === "inline" && $this->filename == null && $this->name == null && !$this->header->has("content_id")) { return false; } return true; } + /** + * Get the part header + * + * @return Header|null + */ + public function getHeader(): ?Header { + return $this->header; + } + } From 67b9ee32a9551b0478fe73ebeb1e9f712ec0dd06 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 10 Mar 2023 00:40:31 +0100 Subject: [PATCH 065/203] Restore a message from string --- CHANGELOG.md | 1 + src/Message.php | 33 ++++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1af4be9b..82a5ab54 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Attribute::map()` method added to map all attribute values - `Header::has()` method added to check if a header attribute / value exist - All part attributes are now accessible via linked attribute +- Restore a message from string `Message::fromString()` ### Breaking changes - NaN diff --git a/src/Message.php b/src/Message.php index 157c1f7e..ff7475c6 100755 --- a/src/Message.php +++ b/src/Message.php @@ -309,6 +309,30 @@ public static function make(int $uid, ?int $msglist, Client $client, string $raw * @throws MaskNotFoundException */ public static function fromFile($filename): Message { + $blob = file_get_contents($filename); + if ($blob === false) { + throw new RuntimeException("Unable to read file"); + } + return self::fromString($blob); + } + + /** + * Create a new message instance by reading and loading a string + * @param string $blob + * + * @return Message + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException + */ + public static function fromString(string $blob): Message { $reflection = new ReflectionClass(self::class); /** @var Message $instance */ $instance = $reflection->newInstanceWithoutConstructor(); @@ -321,12 +345,11 @@ public static function fromFile($filename): Message { throw new MaskNotFoundException("Unknown message mask provided"); } - $email = file_get_contents($filename); - if(!str_contains($email, "\r\n")){ - $email = str_replace("\n", "\r\n", $email); + if(!str_contains($blob, "\r\n")){ + $blob = str_replace("\n", "\r\n", $blob); } - $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); - $raw_body = substr($email, strlen($raw_header)+4); + $raw_header = substr($blob, 0, strpos($blob, "\r\n\r\n")); + $raw_body = substr($blob, strlen($raw_header)+4); $instance->parseRawHeader($raw_header); $instance->parseRawBody($raw_body); From 1459a296b4be38cc5f73b1031544e5fba65910a2 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:25:57 +0100 Subject: [PATCH 066/203] use accessor instead of magic method --- src/Message.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Message.php b/src/Message.php index ff7475c6..5cd9ff7a 100755 --- a/src/Message.php +++ b/src/Message.php @@ -982,11 +982,9 @@ public function thread(Folder $sent_folder = null, MessageCollection &$thread = $this->fetchThreadByInReplyTo($thread, $this->message_id, $folder, $folder, $sent_folder); $this->fetchThreadByInReplyTo($thread, $this->message_id, $sent_folder, $folder, $sent_folder); - if (is_array($this->in_reply_to)) { - foreach ($this->in_reply_to as $in_reply_to) { - $this->fetchThreadByMessageId($thread, $in_reply_to, $folder, $folder, $sent_folder); - $this->fetchThreadByMessageId($thread, $in_reply_to, $sent_folder, $folder, $sent_folder); - } + foreach ($this->in_reply_to->all() as $in_reply_to) { + $this->fetchThreadByMessageId($thread, $in_reply_to, $folder, $folder, $sent_folder); + $this->fetchThreadByMessageId($thread, $in_reply_to, $sent_folder, $folder, $sent_folder); } return $thread; From a165e2c35caf677766eaaa50dbf3345e7c291e9a Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:37:07 +0100 Subject: [PATCH 067/203] Only explode the tag line if it contains a space --- src/Connection/Protocols/ImapProtocol.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index b477b103..d9b49895 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -147,7 +147,9 @@ protected function assumedNextLine(Response $response, string $start): bool { */ protected function nextTaggedLine(Response $response, ?string &$tag): string { $line = $this->nextLine($response); - list($tag, $line) = explode(' ', $line, 2); + if (str_contains($line, ' ')) { + list($tag, $line) = explode(' ', $line, 2); + } return $line ?? ''; } @@ -300,7 +302,7 @@ public function readResponse(Response $response, string $tag, bool $dontParse = // last line has response code if ($tokens[0] == 'OK') { return $lines ?: [true]; - } elseif ($tokens[0] == 'NO') { + } elseif ($tokens[0] == 'NO' || $tokens[0] == 'BAD' || $tokens[0] == 'BYE') { throw new ImapServerErrorException(); } From 53e69bbe367100613d62952cb60b9d9aff1f6b1e Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:38:30 +0100 Subject: [PATCH 068/203] "empty response" detection extended to catch an empty response caused by a broken resource --- src/Connection/Protocols/ImapProtocol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index d9b49895..55297af0 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -117,7 +117,7 @@ public function nextLine(Response $response): string { while (($next_char = fread($this->stream, 1)) !== false && !in_array($next_char, ["","\n"])) { $line .= $next_char; } - if ($line === "" && $next_char === false) { + if ($line === "" && ($next_char === false || $next_char === "")) { throw new RuntimeException('empty response'); } $line .= "\n"; From a09a45be5be789e30426d1fda58b80fa343e4d31 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:41:29 +0100 Subject: [PATCH 069/203] prevent iconv decoding from failing --- src/Header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index 56b3587a..b5ebf2ec 100644 --- a/src/Header.php +++ b/src/Header.php @@ -433,7 +433,7 @@ private function decode(mixed $value): mixed { $value = \imap_utf8($value); } } elseif ($decoder === 'iconv' && $this->is_uft8($value)) { - $value = iconv_mime_decode($value); + $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); } if ($this->is_uft8($value)) { From d0cdb0803ee9e4a2fc9cde5ac5f8f106cf262589 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:42:16 +0100 Subject: [PATCH 070/203] Date decoding rules extended to support more date formats --- src/Header.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Header.php b/src/Header.php index b5ebf2ec..7d96b838 100644 --- a/src/Header.php +++ b/src/Header.php @@ -693,12 +693,20 @@ private function parseDate(object $header): void { if (str_contains($date, ' ')) { $date = str_replace(' ', ' ', $date); } + if (str_contains($date, ' UT ')) { + $date = str_replace(' UT ', ' UTC ', $date); + } $parsed_date = Carbon::parse($date); } catch (\Exception $e) { switch (true) { case preg_match('/([0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}\-[0-9]{1,2}\.[0-9]{1,2}.[0-9]{1,2})+$/i', $date) > 0: $date = Carbon::createFromFormat("Y.m.d-H.i.s", $date); break; + case preg_match('/([0-9]{2} [A-Z]{3} [0-9]{4} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2} [+-][0-9]{1,4} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2} [+-][0-9]{1,4})+$/i', $date) > 0: + $parts = explode(' ', $date); + array_splice($parts, -2); + $date = implode(' ', $parts); + break; case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: $date .= 'C'; From 647021478ce11fe65ec71fae222d38d3e2efe21e Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:42:56 +0100 Subject: [PATCH 071/203] changed the decode method to public --- src/Header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index 7d96b838..49aa4397 100644 --- a/src/Header.php +++ b/src/Header.php @@ -413,7 +413,7 @@ private function is_uft8($value): bool { * * @return mixed */ - private function decode(mixed $value): mixed { + public function decode(mixed $value): mixed { if (is_array($value)) { return $this->decodeArray($value); } From aaf186980e255a36ed0eb5edd98b3af0bae6d3b7 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:43:59 +0100 Subject: [PATCH 072/203] active_folder getter and setter added --- src/Client.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Client.php b/src/Client.php index b9a08053..33a05a42 100755 --- a/src/Client.php +++ b/src/Client.php @@ -660,6 +660,25 @@ public function openFolder(string $folder_path, bool $force_select = false): arr return $this->connection->selectFolder($folder_path)->validatedData(); } + /** + * Set active folder + * @param string|null $folder_path + * + * @return void + */ + public function setActiveFolder(?string $folder_path = null): void { + $this->active_folder = $folder_path; + } + + /** + * Get active folder + * + * @return string|null + */ + public function getActiveFolder(): ?string { + return $this->active_folder; + } + /** * Create a new Folder * @param string $folder_path From 1dad65fed741392ab9177935aaebc4d7df960706 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:44:46 +0100 Subject: [PATCH 073/203] Unset the currently active folder if it gets deleted --- src/Client.php | 3 +++ src/Folder.php | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/Client.php b/src/Client.php index 33a05a42..a4e004ba 100755 --- a/src/Client.php +++ b/src/Client.php @@ -731,6 +731,9 @@ public function deleteFolder(string $folder_path, bool $expunge = true): array { $this->checkConnection(); $folder = $this->getFolderByPath($folder_path); + if ($this->active_folder == $folder->path){ + $this->active_folder = null; + } $status = $this->getConnection()->deleteFolder($folder->path)->validatedData(); if ($expunge) $this->expunge(); diff --git a/src/Folder.php b/src/Folder.php index ddae8525..0089dc05 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -375,6 +375,10 @@ public function rename(string $new_name, bool $expunge = true): array { */ public function delete(bool $expunge = true): array { $status = $this->client->getConnection()->deleteFolder($this->path)->validatedData(); + if ($this->client->getActiveFolder() == $this->path){ + $this->client->setActiveFolder(null); + } + if ($expunge) $this->client->expunge(); $event = $this->getEvent("folder", "deleted"); From 8e0bec80ae856fa569ec3e8ac23313825044bbe5 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:46:26 +0100 Subject: [PATCH 074/203] Method to check if an encoding is supported added --- src/EncodingAliases.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/EncodingAliases.php b/src/EncodingAliases.php index 9ebdd958..888fc7fa 100644 --- a/src/EncodingAliases.php +++ b/src/EncodingAliases.php @@ -578,4 +578,14 @@ public static function getEncodings(): array { public static function isUtf7(string $encoding): bool { return str_contains(str_replace("-", "", strtolower($encoding)), "utf7"); } + + /** + * Check if an encoding is supported + * @param string $encoding + * + * @return bool + */ + public static function has(string $encoding): bool { + return isset(self::$aliases[strtolower($encoding)]); + } } From 0492379288374908bac9c74cebca2c19cb405f25 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:47:51 +0100 Subject: [PATCH 075/203] Attachment name and filename parsing fixed and improved to support more formats --- src/Attachment.php | 62 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index a6e10430..2c9c23cc 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -29,7 +29,9 @@ * @property string content_type * @property string id * @property string name - * @property string disposition + * @property string description + * @property string filename + * @property ?string disposition * @property string img_src * * @method integer getPartNumber() @@ -78,6 +80,8 @@ class Attachment { 'content_type' => null, 'id' => null, 'name' => null, + 'filename' => null, + 'description' => null, 'disposition' => null, 'img_src' => null, 'size' => null, @@ -209,21 +213,31 @@ protected function fetch(): void { $this->disposition = $this->part->disposition; if (($filename = $this->part->filename) !== null) { - $this->setName($filename); - } elseif (($name = $this->part->name) !== null) { - $this->setName($name); - }else { - $this->setName("undefined"); + $this->filename = $this->decodeName($filename); + } + + if (($name = $this->part->name) !== null) { + $this->name = $this->decodeName($name); + } + if (!$this->name && $this->filename != "") { + $this->name = $this->filename; } if (IMAP::ATTACHMENT_TYPE_MESSAGE == $this->part->type) { if ($this->part->ifdescription) { - $this->setName($this->part->description); - } else { - $this->setName($this->part->subtype); + if (!$this->name) { + $this->name = $this->part->description; + } + $this->description = $this->part->description; + } else if (!$this->name) { + $this->name = $this->part->subtype; } } + if (!$this->filename) { + $this->filename = $this->name; + } + $this->attributes = array_merge($this->part->getHeader()->getAttributes(), $this->attributes); } @@ -241,18 +255,36 @@ public function save(string $path, string $filename = null): bool { } /** - * Set the attachment name and try to decode it + * Decode a given name * @param $name + * + * @return string */ - public function setName($name): void { - $decoder = $this->config['decoder']['attachment']; + public function decodeName($name): string { if ($name !== null) { + if (str_contains($name, "''")) { + $parts = explode("''", $name); + if (EncodingAliases::has($parts[0])) { + $name = implode("''", array_slice($parts, 1)); + } + } + + $decoder = $this->config['decoder']['message']; if($decoder === 'utf-8' && extension_loaded('imap')) { - $this->name = \imap_utf8($name); - }else{ - $this->name = mb_decode_mimeheader($name); + $name = \imap_utf8($name); + } + + if (preg_match('/=\?([^?]+)\?(Q|B)\?(.+)\?=/i', $name, $matches)) { + $name = $this->part->getHeader()->decode($name); + } + + // check if $name is url encoded + if (preg_match('/%[0-9A-F]{2}/i', $name)) { + $name = urldecode($name); } + return $name; } + return ""; } /** From 5f800e1b1034392f512dba21d745204aa8f1a905 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:49:34 +0100 Subject: [PATCH 076/203] Fixture tests added --- phpunit.xml.dist | 1 + .../AttachmentEncodedFilenameTest.php | 51 +++++ tests/fixtures/AttachmentLongFilenameTest.php | 76 +++++++ .../fixtures/AttachmentNoDispositionTest.php | 51 +++++ tests/fixtures/BccTest.php | 43 ++++ tests/fixtures/BooleanDecodedContentTest.php | 54 +++++ tests/fixtures/DateTemplateTest.php | 112 ++++++++++ tests/fixtures/EmailAddressTest.php | 39 ++++ tests/fixtures/EmbeddedEmailTest.php | 63 ++++++ ...lWithoutContentDispositionEmbeddedTest.php | 73 +++++++ ...ddedEmailWithoutContentDispositionTest.php | 94 ++++++++ tests/fixtures/ExampleBounceTest.php | 97 +++++++++ tests/fixtures/FixtureTestCase.php | 93 ++++++++ tests/fixtures/FourNestedEmailsTest.php | 54 +++++ tests/fixtures/GbkCharsetTest.php | 37 ++++ tests/fixtures/HtmlOnlyTest.php | 37 ++++ .../ImapMimeHeaderDecodeReturnsFalseTest.php | 37 ++++ tests/fixtures/InlineAttachmentTest.php | 58 +++++ tests/fixtures/KsC56011987HeadersTest.php | 46 ++++ tests/fixtures/MailThatIsAttachmentTest.php | 62 ++++++ tests/fixtures/MissingDateTest.php | 37 ++++ tests/fixtures/MissingFromTest.php | 37 ++++ tests/fixtures/MixedFilenameTest.php | 60 ++++++ .../MultipleHtmlPartsAndAttachmentsTest.php | 74 +++++++ .../MultipleNestedAttachmentsTest.php | 67 ++++++ .../NestesEmbeddedWithAttachmentTest.php | 67 ++++++ tests/fixtures/NullContentCharsetTest.php | 39 ++++ tests/fixtures/PecTest.php | 79 +++++++ tests/fixtures/PlainOnlyTest.php | 37 ++++ tests/fixtures/PlainTextAttachmentTest.php | 53 +++++ tests/fixtures/ReferencesTest.php | 54 +++++ tests/fixtures/SimpleMultipartTest.php | 37 ++++ .../fixtures/StructuredWithAttachmentTest.php | 54 +++++ tests/fixtures/UndefinedCharsetHeaderTest.php | 59 +++++ .../UndisclosedRecipientsMinusTest.php | 42 ++++ .../UndisclosedRecipientsSpaceTest.php | 42 ++++ tests/fixtures/UnknownEncodingTest.php | 37 ++++ .../fixtures/WithoutCharsetPlainOnlyTest.php | 37 ++++ .../WithoutCharsetSimpleMultipartTest.php | 37 ++++ .../messages/attachment_encoded_filename.eml | 10 + tests/messages/attachment_long_filename.eml | 55 +++++ tests/messages/attachment_no_disposition.eml | 9 + tests/messages/bcc.eml | 12 ++ tests/messages/boolean_decoded_content.eml | 31 +++ tests/messages/date-template.eml | 8 + tests/messages/email_address.eml | 9 + tests/messages/embedded_email.eml | 63 ++++++ ...l_without_content_disposition-embedded.eml | 61 ++++++ ...dded_email_without_content_disposition.eml | 141 ++++++++++++ tests/messages/example_bounce.eml | 96 +++++++++ tests/messages/four_nested_emails.eml | 69 ++++++ tests/messages/gbk_charset.eml | 9 + tests/messages/html_only.eml | 9 + .../imap_mime_header_decode_returns_false.eml | 9 + tests/messages/inline_attachment.eml | 41 ++++ tests/messages/ks_c_5601-1987_headers.eml | 10 + tests/messages/mail_that_is_attachment.eml | 28 +++ tests/messages/missing_date.eml | 7 + tests/messages/missing_from.eml | 7 + tests/messages/mixed_filename.eml | 25 +++ .../multiple_html_parts_and_attachments.eml | 203 ++++++++++++++++++ .../messages/multiple_nested_attachments.eml | 84 ++++++++ .../nestes_embedded_with_attachment.eml | 145 +++++++++++++ tests/messages/null_content_charset.eml | 8 + tests/messages/pec.eml | 65 ++++++ tests/messages/plain_only.eml | 9 + tests/messages/plain_text_attachment.eml | 21 ++ tests/messages/references.eml | 11 + tests/messages/simple_multipart.eml | 23 ++ tests/messages/structured_with_attachment.eml | 24 +++ tests/messages/thread_my_topic.eml | 10 + tests/messages/thread_re_my_topic.eml | 11 + tests/messages/thread_unrelated.eml | 10 + tests/messages/undefined_charset_header.eml | 20 ++ .../messages/undisclosed_recipients_minus.eml | 8 + .../messages/undisclosed_recipients_space.eml | 8 + tests/messages/unknown_encoding.eml | 24 +++ tests/messages/without_charset_plain_only.eml | 8 + .../without_charset_simple_multipart.eml | 23 ++ 79 files changed, 3551 insertions(+) create mode 100644 tests/fixtures/AttachmentEncodedFilenameTest.php create mode 100644 tests/fixtures/AttachmentLongFilenameTest.php create mode 100644 tests/fixtures/AttachmentNoDispositionTest.php create mode 100644 tests/fixtures/BccTest.php create mode 100644 tests/fixtures/BooleanDecodedContentTest.php create mode 100644 tests/fixtures/DateTemplateTest.php create mode 100644 tests/fixtures/EmailAddressTest.php create mode 100644 tests/fixtures/EmbeddedEmailTest.php create mode 100644 tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php create mode 100644 tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php create mode 100644 tests/fixtures/ExampleBounceTest.php create mode 100644 tests/fixtures/FixtureTestCase.php create mode 100644 tests/fixtures/FourNestedEmailsTest.php create mode 100644 tests/fixtures/GbkCharsetTest.php create mode 100644 tests/fixtures/HtmlOnlyTest.php create mode 100644 tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php create mode 100644 tests/fixtures/InlineAttachmentTest.php create mode 100644 tests/fixtures/KsC56011987HeadersTest.php create mode 100644 tests/fixtures/MailThatIsAttachmentTest.php create mode 100644 tests/fixtures/MissingDateTest.php create mode 100644 tests/fixtures/MissingFromTest.php create mode 100644 tests/fixtures/MixedFilenameTest.php create mode 100644 tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php create mode 100644 tests/fixtures/MultipleNestedAttachmentsTest.php create mode 100644 tests/fixtures/NestesEmbeddedWithAttachmentTest.php create mode 100644 tests/fixtures/NullContentCharsetTest.php create mode 100644 tests/fixtures/PecTest.php create mode 100644 tests/fixtures/PlainOnlyTest.php create mode 100644 tests/fixtures/PlainTextAttachmentTest.php create mode 100644 tests/fixtures/ReferencesTest.php create mode 100644 tests/fixtures/SimpleMultipartTest.php create mode 100644 tests/fixtures/StructuredWithAttachmentTest.php create mode 100644 tests/fixtures/UndefinedCharsetHeaderTest.php create mode 100644 tests/fixtures/UndisclosedRecipientsMinusTest.php create mode 100644 tests/fixtures/UndisclosedRecipientsSpaceTest.php create mode 100644 tests/fixtures/UnknownEncodingTest.php create mode 100644 tests/fixtures/WithoutCharsetPlainOnlyTest.php create mode 100644 tests/fixtures/WithoutCharsetSimpleMultipartTest.php create mode 100644 tests/messages/attachment_encoded_filename.eml create mode 100644 tests/messages/attachment_long_filename.eml create mode 100644 tests/messages/attachment_no_disposition.eml create mode 100644 tests/messages/bcc.eml create mode 100644 tests/messages/boolean_decoded_content.eml create mode 100644 tests/messages/date-template.eml create mode 100644 tests/messages/email_address.eml create mode 100644 tests/messages/embedded_email.eml create mode 100644 tests/messages/embedded_email_without_content_disposition-embedded.eml create mode 100644 tests/messages/embedded_email_without_content_disposition.eml create mode 100644 tests/messages/example_bounce.eml create mode 100644 tests/messages/four_nested_emails.eml create mode 100644 tests/messages/gbk_charset.eml create mode 100644 tests/messages/html_only.eml create mode 100644 tests/messages/imap_mime_header_decode_returns_false.eml create mode 100644 tests/messages/inline_attachment.eml create mode 100644 tests/messages/ks_c_5601-1987_headers.eml create mode 100644 tests/messages/mail_that_is_attachment.eml create mode 100644 tests/messages/missing_date.eml create mode 100644 tests/messages/missing_from.eml create mode 100644 tests/messages/mixed_filename.eml create mode 100644 tests/messages/multiple_html_parts_and_attachments.eml create mode 100644 tests/messages/multiple_nested_attachments.eml create mode 100644 tests/messages/nestes_embedded_with_attachment.eml create mode 100644 tests/messages/null_content_charset.eml create mode 100644 tests/messages/pec.eml create mode 100644 tests/messages/plain_only.eml create mode 100644 tests/messages/plain_text_attachment.eml create mode 100644 tests/messages/references.eml create mode 100644 tests/messages/simple_multipart.eml create mode 100644 tests/messages/structured_with_attachment.eml create mode 100644 tests/messages/thread_my_topic.eml create mode 100644 tests/messages/thread_re_my_topic.eml create mode 100644 tests/messages/thread_unrelated.eml create mode 100644 tests/messages/undefined_charset_header.eml create mode 100644 tests/messages/undisclosed_recipients_minus.eml create mode 100644 tests/messages/undisclosed_recipients_space.eml create mode 100644 tests/messages/unknown_encoding.eml create mode 100644 tests/messages/without_charset_plain_only.eml create mode 100644 tests/messages/without_charset_simple_multipart.eml diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 76b56dfe..02d56a89 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -13,6 +13,7 @@ tests + tests/fixtures tests/issues tests/live diff --git a/tests/fixtures/AttachmentEncodedFilenameTest.php b/tests/fixtures/AttachmentEncodedFilenameTest.php new file mode 100644 index 00000000..33d97fde --- /dev/null +++ b/tests/fixtures/AttachmentEncodedFilenameTest.php @@ -0,0 +1,51 @@ +getFixture("attachment_encoded_filename.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("multipart/mixed", $message->content_type->last()); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->filename); + self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/vnd.ms-excel", $attachment->content_type); + self::assertEquals("a0ef7cfbc05b73dbcb298fe0bc224b41900cdaf60f9904e3fea5ba6c7670013c", hash("sha256", $attachment->content)); + self::assertEquals(146, $attachment->size); + self::assertEquals(0, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/AttachmentLongFilenameTest.php b/tests/fixtures/AttachmentLongFilenameTest.php new file mode 100644 index 00000000..b3c3e903 --- /dev/null +++ b/tests/fixtures/AttachmentLongFilenameTest.php @@ -0,0 +1,76 @@ +getFixture("attachment_long_filename.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("multipart/mixed", $message->content_type->last()); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + $attachments = $message->attachments(); + self::assertCount(3, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("Buchungsbestätigung- Rechnung-Geschäftsbedingungen-Nr.B123-45 - XXXX xxxxxxxxxxxxxxxxx XxxX, Lüdxxxxxxxx - VM Klaus XXXXXX - xxxxxxxx.pdf", $attachment->name); + self::assertEquals("Buchungsbestätigung- Rechnung-Geschäftsbedingungen-Nr.B123-45 - XXXXX xxxxxxxxxxxxxxxxx XxxX, Lüxxxxxxxxxx - VM Klaus XXXXXX - xxxxxxxx.pdf", $attachment->filename); + self::assertEquals('text', $attachment->type); + self::assertEquals("text/plain", $attachment->content_type); + self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); + self::assertEquals(4, $attachment->size); + self::assertEquals(0, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals('01_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); + self::assertEquals("f7b5181985862431bfc443d26e3af2371e20a0afd676eeb9b9595a26d42e0b73", hash("sha256", $attachment->filename)); + self::assertEquals('text', $attachment->type); + self::assertEquals("text/plain", $attachment->content_type); + self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); + self::assertEquals(4, $attachment->size); + self::assertEquals(1, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[2]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals('02_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); + self::assertEquals('02_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->filename); + self::assertEquals('text', $attachment->type); + self::assertEquals("text/plain", $attachment->content_type); + self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); + self::assertEquals(4, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/AttachmentNoDispositionTest.php b/tests/fixtures/AttachmentNoDispositionTest.php new file mode 100644 index 00000000..5a3fadd8 --- /dev/null +++ b/tests/fixtures/AttachmentNoDispositionTest.php @@ -0,0 +1,51 @@ +getFixture("attachment_no_disposition.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("multipart/mixed", $message->content_type->last()); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->filename); + self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/vnd.ms-excel", $attachment->content_type); + self::assertEquals("a0ef7cfbc05b73dbcb298fe0bc224b41900cdaf60f9904e3fea5ba6c7670013c", hash("sha256", $attachment->content)); + self::assertEquals(146, $attachment->size); + self::assertEquals(0, $attachment->part_number); + self::assertNull($attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/BccTest.php b/tests/fixtures/BccTest.php new file mode 100644 index 00000000..8de14f62 --- /dev/null +++ b/tests/fixtures/BccTest.php @@ -0,0 +1,43 @@ +getFixture("bcc.eml"); + + self::assertEquals("test", $message->subject); + self::assertEquals("", $message->return_path); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("text/plain", $message->content_type); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals("to@here.com", $message->to); + self::assertEquals("A_€@{è_Z ", $message->bcc); + self::assertEquals("sender@here.com", $message->sender); + self::assertEquals("reply-to@here.com", $message->reply_to); + } +} \ No newline at end of file diff --git a/tests/fixtures/BooleanDecodedContentTest.php b/tests/fixtures/BooleanDecodedContentTest.php new file mode 100644 index 00000000..ae9b797f --- /dev/null +++ b/tests/fixtures/BooleanDecodedContentTest.php @@ -0,0 +1,54 @@ +getFixture("boolean_decoded_content.eml"); + + self::assertEquals("Nuu", $message->subject); + self::assertEquals("Here is the problem mail\r\n \r\nBody text", $message->getTextBody()); + self::assertEquals("Here is the problem mail\r\n \r\nBody text", $message->getHTMLBody()); + + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals("to@here.com", $message->to); + + $attachments = $message->getAttachments(); + self::assertCount(1, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("Example Domain.pdf", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/pdf", $attachment->content_type); + self::assertEquals("1c449aaab4f509012fa5eaa180fd017eb7724ccacabdffc1c6066d3756dcde5c", hash("sha256", $attachment->content)); + self::assertEquals(53, $attachment->size); + self::assertEquals(3, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/DateTemplateTest.php b/tests/fixtures/DateTemplateTest.php new file mode 100644 index 00000000..4bf8f114 --- /dev/null +++ b/tests/fixtures/DateTemplateTest.php @@ -0,0 +1,112 @@ + "2019-04-05 10:10:49", + "04 Jan 2018 10:12:47 UT" => "2018-01-04 10:12:47", + "22 Jun 18 03:56:36 PM -05:00 (GMT -05:00)" => "2018-06-22 20:56:36", + "Sat, 31 Aug 2013 20:08:23 +0580" => "2013-08-31 14:38:23", + "Fri, 1 Feb 2019 01:30:04 +0600 (+06)" => "2019-01-31 19:30:04", + "Mon, 4 Feb 2019 04:03:49 -0300 (-03)" => "2019-02-04 07:03:49", + "Sun, 6 Apr 2008 21:24:33 UT" => "2008-04-06 21:24:33", + "Wed, 11 Sep 2019 15:23:06 +0600 (+06)" => "2019-09-11 09:23:06", + "14 Sep 2019 00:10:08 UT +0200" => "2019-09-14 00:10:08", + "Tue, 08 Nov 2022 18:47:20 +0000 14:03:33 +0000" => "2022-11-08 18:47:20", + "Sat, 10, Dec 2022 09:35:19 +0100" => "2022-12-10 08:35:19", + ]; + + /** + * Test the fixture date-template.eml + * + * @return void + * @throws InvalidMessageDateException + * @throws ReflectionException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws RuntimeException + */ + public function testFixture() : void { + try { + $message = $this->getFixture("date-template.eml"); + $this->fail("Expected InvalidMessageDateException"); + } catch (InvalidMessageDateException $e) { + self::assertTrue(true); + } + + self::$manager->setConfig([ + "options" => [ + "fallback_date" => "2021-01-01 00:00:00", + ], + ]); + $message = $this->getFixture("date-template.eml"); + + self::assertEquals("test", $message->subject); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2021-01-01 00:00:00", $message->date->first()->timezone("UTC")->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", (string)$message->from); + self::assertEquals("to@here.com", $message->to); + + self::$manager->setConfig([ + "options" => [ + "fallback_date" => null, + ], + ]); + + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "date-template.eml"]); + $blob = file_get_contents($filename); + self::assertNotFalse($blob); + + foreach ($this->dates as $date => $expected) { + $message = Message::fromString(str_replace("%date_raw_header%", $date, $blob)); + self::assertEquals("test", $message->subject); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals($expected, $message->date->first()->timezone("UTC")->format("Y-m-d H:i:s"), "Date \"$date\" should be \"$expected\""); + self::assertEquals("from@there.com", (string)$message->from); + self::assertEquals("to@here.com", $message->to); + } + } +} \ No newline at end of file diff --git a/tests/fixtures/EmailAddressTest.php b/tests/fixtures/EmailAddressTest.php new file mode 100644 index 00000000..d4e403d7 --- /dev/null +++ b/tests/fixtures/EmailAddressTest.php @@ -0,0 +1,39 @@ +getFixture("email_address.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("123@example.com", $message->message_id); + self::assertEquals("Hi\r\nHow are you?", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertFalse($message->date->first()); + self::assertEquals("no_host@UNKNOWN", (string)$message->from); + self::assertEquals("", $message->to); + self::assertEquals("This one: is \"right\" , No-address@UNKNOWN", $message->cc); + } +} \ No newline at end of file diff --git a/tests/fixtures/EmbeddedEmailTest.php b/tests/fixtures/EmbeddedEmailTest.php new file mode 100644 index 00000000..4d8baf77 --- /dev/null +++ b/tests/fixtures/EmbeddedEmailTest.php @@ -0,0 +1,63 @@ +getFixture("embedded_email.eml"); + + self::assertEquals("embedded message", $message->subject); + self::assertEquals([ + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' + ], $message->received->toArray()); + self::assertEquals("7e5798da5747415e5b82fdce042ab2a6@cerstor.cz", $message->message_id); + self::assertEquals("demo@cerstor.cz", $message->return_path); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Roundcube Webmail/1.0.0", $message->user_agent); + self::assertEquals("email that contains embedded message", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2016-01-29 13:25:40", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("demo@cerstor.cz", $message->from); + self::assertEquals("demo@cerstor.cz", $message->x_sender); + self::assertEquals("demo@cerstor.cz", $message->to); + + $attachments = $message->getAttachments(); + self::assertCount(1, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("demo.eml", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("a1f965f10a9872e902a82dde039a237e863f522d238a1cb1968fe3396dbcac65", hash("sha256", $attachment->content)); + self::assertEquals(893, $attachment->size); + self::assertEquals(1, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php new file mode 100644 index 00000000..e7629bc3 --- /dev/null +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php @@ -0,0 +1,73 @@ +getFixture("embedded_email_without_content_disposition-embedded.eml"); + + self::assertEquals("embedded_message_subject", $message->subject); + self::assertEquals([ + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' + ], $message->received->toArray()); + self::assertEquals("AC39946EBF5C034B87BABD5343E96979012671D40E38@VM002.cerk.cc", $message->message_id); + self::assertEquals("pl-PL, nl-NL", $message->accept_language); + self::assertEquals("pl-PL", $message->content_language); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("some txt", $message->getTextBody()); + self::assertEquals("\r\n

some txt

\r\n", $message->getHTMLBody()); + + self::assertEquals("2019-04-05 10:10:49", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("demo@cerstor.cz", $message->from); + self::assertEquals("demo@cerstor.cz", $message->to); + + $attachments = $message->getAttachments(); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file1.xlsx", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); + self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); + self::assertEquals(40, $attachment->size); + self::assertEquals(3, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file2.xlsx", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); + self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); + self::assertEquals(40, $attachment->size); + self::assertEquals(4, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php new file mode 100644 index 00000000..c62e743c --- /dev/null +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -0,0 +1,94 @@ +getFixture("embedded_email_without_content_disposition.eml"); + + self::assertEquals("Subject", $message->subject); + self::assertEquals([ + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' + ], $message->received->toArray()); + self::assertEquals("AC39946EBF5C034B87BABD5343E96979012671D9F7E4@VM002.cerk.cc", $message->message_id); + self::assertEquals("pl-PL, nl-NL", $message->accept_language); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("TexT\r\n\r\n[cid:file.jpg]", $message->getTextBody()); + self::assertEquals("

TexT

", $message->getHTMLBody()); + + self::assertEquals("2019-04-05 11:48:50", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("demo@cerstor.cz", $message->from); + self::assertEquals("demo@cerstor.cz", $message->to); + + $attachments = $message->getAttachments(); + self::assertCount(4, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file.jpg", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("image/jpeg", $attachment->content_type); + self::assertEquals("6b7fa434f92a8b80aab02d9bf1a12e49ffcae424e4013a1c4f68b67e3d2bbcd0", hash("sha256", $attachment->content)); + self::assertEquals(96, $attachment->size); + self::assertEquals(3, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("2476c8b91a93c6b2fe1bfff593cb55956c2fe8e7ca6de9ad2dc9d101efe7a867", hash("sha256", $attachment->content)); + self::assertEquals(2073, $attachment->size); + self::assertEquals(5, $attachment->part_number); + self::assertNull($attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[2]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file3.xlsx", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); + self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); + self::assertEquals(40, $attachment->size); + self::assertEquals(6, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[3]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file4.zip", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/x-zip-compressed", $attachment->content_type); + self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); + self::assertEquals(40, $attachment->size); + self::assertEquals(7, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/ExampleBounceTest.php b/tests/fixtures/ExampleBounceTest.php new file mode 100644 index 00000000..d032f316 --- /dev/null +++ b/tests/fixtures/ExampleBounceTest.php @@ -0,0 +1,97 @@ +getFixture("example_bounce.eml"); + + self::assertEquals("<>", $message->return_path); + self::assertEquals([ + 0 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>); Thu, 02 Mar 2023 05:27:29 +0100', + 1 => 'from somewhere06.your-server.de ([1b21:2f8:e0a:50e4::2]) by somewhere.your-server.de with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384 (Exim 4.94.2) id 1pXaXR-0006xQ-BN for demo@foo.de; Thu, 02 Mar 2023 05:27:29 +0100', + 2 => 'from [192.168.0.10] (helo=sslproxy01.your-server.de) by somewhere06.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-000LYP-9R for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 3 => 'from localhost ([127.0.0.1] helo=sslproxy01.your-server.de) by sslproxy01.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-0008gy-7x for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 4 => 'from Debian-exim by sslproxy01.your-server.de with local (Exim 4.92) id 1pXaXO-0008gb-6g for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 5 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>)', + ], $message->received->all()); + self::assertEquals("demo@foo.de", $message->envelope_to); + self::assertEquals("Thu, 02 Mar 2023 05:27:29 +0100", $message->delivery_date); + self::assertEquals([ + 0 => 'somewhere.your-server.de; iprev=pass (somewhere06.your-server.de) smtp.remote-ip=1b21:2f8:e0a:50e4::2; spf=none smtp.mailfrom=<>; dmarc=skipped', + 1 => 'somewhere.your-server.de' + ], $message->authentication_results->all()); + self::assertEquals([ + 0 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>); Thu, 02 Mar 2023 05:27:29 +0100', + 1 => 'from somewhere06.your-server.de ([1b21:2f8:e0a:50e4::2]) by somewhere.your-server.de with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384 (Exim 4.94.2) id 1pXaXR-0006xQ-BN for demo@foo.de; Thu, 02 Mar 2023 05:27:29 +0100', + 2 => 'from [192.168.0.10] (helo=sslproxy01.your-server.de) by somewhere06.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-000LYP-9R for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 3 => 'from localhost ([127.0.0.1] helo=sslproxy01.your-server.de) by sslproxy01.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-0008gy-7x for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 4 => 'from Debian-exim by sslproxy01.your-server.de with local (Exim 4.92) id 1pXaXO-0008gb-6g for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 5 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>)', + ], $message->received->all()); + self::assertEquals("ding@ding.de", $message->x_failed_recipients); + self::assertEquals("auto-replied", $message->auto_submitted); + self::assertEquals("Mail Delivery System ", $message->from); + self::assertEquals("demo@foo.de", $message->to); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Mail delivery failed", $message->subject); + self::assertEquals("E1pXaXO-0008gb-6g@sslproxy01.your-server.de", $message->message_id); + self::assertEquals("2023-03-02 04:27:26", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("Clear (ClamAV 0.103.8/26827/Wed Mar 1 09:28:49 2023)", $message->x_virus_scanned); + self::assertEquals("0.0 (/)", $message->x_spam_score); + self::assertEquals("bar-demo@foo.de", $message->delivered_to); + self::assertEquals("multipart/report", $message->content_type->last()); + self::assertEquals("5d4847c21c8891e73d62c8246f260a46496958041a499f33ecd47444fdaa591b", hash("sha256", $message->getTextBody())); + self::assertFalse($message->hasHTMLBody()); + + $attachments = $message->attachments(); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("", $attachment->filename); + self::assertEquals("", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/delivery-status", $attachment->content_type); + self::assertEquals("85ac09d1d74b2d85853084dc22abcad205a6bfde62d6056e3a933ffe7e82e45c", hash("sha256", $attachment->content)); + self::assertEquals(267, $attachment->size); + self::assertEquals(1, $attachment->part_number); + self::assertNull($attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("", $attachment->filename); + self::assertEquals("", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("7525331f5fab23ea77f595b995336aca7b8dad12db00ada14abebe7fe5b96e10", hash("sha256", $attachment->content)); + self::assertEquals(776, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertNull($attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/FixtureTestCase.php b/tests/fixtures/FixtureTestCase.php new file mode 100644 index 00000000..8660bedc --- /dev/null +++ b/tests/fixtures/FixtureTestCase.php @@ -0,0 +1,93 @@ + [ + "debug" => $_ENV["LIVE_MAILBOX_DEBUG"] ?? false, + ], + 'accounts' => [ + 'default' => [ + 'host' => getenv("LIVE_MAILBOX_HOST"), + 'port' => getenv("LIVE_MAILBOX_PORT"), + 'encryption' => getenv("LIVE_MAILBOX_ENCRYPTION"), + 'validate_cert' => getenv("LIVE_MAILBOX_VALIDATE_CERT"), + 'username' => getenv("LIVE_MAILBOX_USERNAME"), + 'password' => getenv("LIVE_MAILBOX_PASSWORD"), + 'protocol' => 'imap', //might also use imap, [pop3 or nntp (untested)] + ], + ], + ]); + return self::$manager; + } + + /** + * Get a fixture message + * @param string $template + * + * @return Message + * @throws ReflectionException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws RuntimeException + */ + final public function getFixture(string $template) : Message { + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", $template]); + $message = Message::fromFile($filename); + self::assertInstanceOf(Message::class, $message); + + return $message; + } +} \ No newline at end of file diff --git a/tests/fixtures/FourNestedEmailsTest.php b/tests/fixtures/FourNestedEmailsTest.php new file mode 100644 index 00000000..5511c05e --- /dev/null +++ b/tests/fixtures/FourNestedEmailsTest.php @@ -0,0 +1,54 @@ +getFixture("four_nested_emails.eml"); + + self::assertEquals("3-third-subject", $message->subject); + self::assertEquals("3-third-content", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertFalse($message->date->first()); + self::assertEquals("test@example.com", $message->from->first()->mail); + self::assertEquals("test@example.com", $message->to->first()->mail); + + $attachments = $message->getAttachments(); + self::assertCount(1, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("2-second-email.eml", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("85012e6a26d064a0288ee62618b3192687385adb4a4e27e48a28f738a325ca46", hash("sha256", $attachment->content)); + self::assertEquals(1376, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + } +} \ No newline at end of file diff --git a/tests/fixtures/GbkCharsetTest.php b/tests/fixtures/GbkCharsetTest.php new file mode 100644 index 00000000..7492d908 --- /dev/null +++ b/tests/fixtures/GbkCharsetTest.php @@ -0,0 +1,37 @@ +getFixture("gbk_charset.eml"); + + self::assertEquals("Nuu", $message->subject); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/HtmlOnlyTest.php b/tests/fixtures/HtmlOnlyTest.php new file mode 100644 index 00000000..90ef44e3 --- /dev/null +++ b/tests/fixtures/HtmlOnlyTest.php @@ -0,0 +1,37 @@ +getFixture("html_only.eml"); + + self::assertEquals("Nuu", $message->subject); + self::assertEquals("Hi", $message->getHTMLBody()); + self::assertFalse($message->hasTextBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php b/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php new file mode 100644 index 00000000..6f2629ff --- /dev/null +++ b/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php @@ -0,0 +1,37 @@ +getFixture("imap_mime_header_decode_returns_false.eml"); + + self::assertEquals("?p?#]ݰ?[??W̌ N? ?LL?̍L??NL˜", $message->subject->first()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/InlineAttachmentTest.php b/tests/fixtures/InlineAttachmentTest.php new file mode 100644 index 00000000..ae538b37 --- /dev/null +++ b/tests/fixtures/InlineAttachmentTest.php @@ -0,0 +1,58 @@ +getFixture("inline_attachment.eml"); + + self::assertEquals("", $message->subject); + self::assertFalse($message->hasTextBody()); + self::assertEquals('', $message->getHTMLBody()); + + self::assertFalse($message->date->first()); + self::assertFalse($message->from->first()); + self::assertFalse($message->to->first()); + + + $attachments = $message->attachments(); + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(1, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("image/png", $attachment->content_type); + self::assertEquals("6568c9e9c35a7fa06f236e89f704d8c9b47183a24f2c978dba6c92e2747e3a13", hash("sha256", $attachment->content)); + self::assertEquals(1486, $attachment->size); + self::assertEquals(1, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertEquals("", $attachment->content_id); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/KsC56011987HeadersTest.php b/tests/fixtures/KsC56011987HeadersTest.php new file mode 100644 index 00000000..944e9bfb --- /dev/null +++ b/tests/fixtures/KsC56011987HeadersTest.php @@ -0,0 +1,46 @@ +getFixture("ks_c_5601-1987_headers.eml"); + + self::assertEquals("RE: 회원님께 Ersi님이 메시지를 보냈습니다.", $message->subject); + self::assertEquals("=?ks_c_5601-1987?B?yLi/+LTUsrIgRXJzabTUwMwguN69w8H2uKYgurizwr3AtM+02S4=?=", $message->thread_topic); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Content", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("to@here.com", $message->to->first()->mail); + + + $from = $message->from->first(); + self::assertEquals("김 현진", $from->personal); + self::assertEquals("from", $from->mailbox); + self::assertEquals("there.com", $from->host); + self::assertEquals("from@there.com", $from->mail); + self::assertEquals("김 현진 ", $from->full); + } +} \ No newline at end of file diff --git a/tests/fixtures/MailThatIsAttachmentTest.php b/tests/fixtures/MailThatIsAttachmentTest.php new file mode 100644 index 00000000..e28dc938 --- /dev/null +++ b/tests/fixtures/MailThatIsAttachmentTest.php @@ -0,0 +1,62 @@ +getFixture("mail_that_is_attachment.eml"); + + self::assertEquals("Report domain: yyy.cz Submitter: google.com Report-ID: 2244696771454641389", $message->subject); + self::assertEquals("2244696771454641389@google.com", $message->message_id); + self::assertEquals("1.0", $message->mime_version); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2015-02-15 10:21:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("xxx@yyy.cz", $message->to->first()->mail); + self::assertEquals("xxx@yyy.cz", $message->sender->first()->mail); + + $from = $message->from->first(); + self::assertEquals("noreply-dmarc-support via xxx", $from->personal); + self::assertEquals("xxx", $from->mailbox); + self::assertEquals("yyy.cz", $from->host); + self::assertEquals("xxx@yyy.cz", $from->mail); + self::assertEquals("noreply-dmarc-support via xxx ", $from->full); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("google.com!yyy.cz!1423872000!1423958399.zip", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/zip", $attachment->content_type); + self::assertEquals("c0d4f47b6fde124cea7460c3e509440d1a062705f550b0502b8ba0cbf621c97a", hash("sha256", $attachment->content)); + self::assertEquals(1062, $attachment->size); + self::assertEquals(0, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/MissingDateTest.php b/tests/fixtures/MissingDateTest.php new file mode 100644 index 00000000..93595adc --- /dev/null +++ b/tests/fixtures/MissingDateTest.php @@ -0,0 +1,37 @@ +getFixture("missing_date.eml"); + + self::assertEquals("Nuu", $message->getSubject()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertFalse($message->date->first()); + self::assertEquals("from@here.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/MissingFromTest.php b/tests/fixtures/MissingFromTest.php new file mode 100644 index 00000000..5d57340c --- /dev/null +++ b/tests/fixtures/MissingFromTest.php @@ -0,0 +1,37 @@ +getFixture("missing_from.eml"); + + self::assertEquals("Nuu", $message->getSubject()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertFalse($message->from->first()); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/MixedFilenameTest.php b/tests/fixtures/MixedFilenameTest.php new file mode 100644 index 00000000..fc1ffa88 --- /dev/null +++ b/tests/fixtures/MixedFilenameTest.php @@ -0,0 +1,60 @@ +getFixture("mixed_filename.eml"); + + self::assertEquals("Свежий прайс-лист", $message->subject); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2018-02-02 19:23:06", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + + $from = $message->from->first(); + self::assertEquals("Прайсы || ПартКом", $from->personal); + self::assertEquals("support", $from->mailbox); + self::assertEquals("part-kom.ru", $from->host); + self::assertEquals("support@part-kom.ru", $from->mail); + self::assertEquals("Прайсы || ПартКом ", $from->full); + + self::assertEquals("foo@bar.com", $message->to->first()); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("Price4VladDaKar.xlsx", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/octet-stream", $attachment->content_type); + self::assertEquals("b832983842b0ad65db69e4c7096444c540a2393e2d43f70c2c9b8b9fceeedbb1", hash('sha256', $attachment->content)); + self::assertEquals(94, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php b/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php new file mode 100644 index 00000000..c8f72bb8 --- /dev/null +++ b/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php @@ -0,0 +1,74 @@ +getFixture("multiple_html_parts_and_attachments.eml"); + + self::assertEquals("multiple_html_parts_and_attachments", $message->subject); + self::assertEquals("This is the first html part\r\n\r\n\r\n\r\nThis is the second html part\r\n\r\n\r\n\r\nThis is the last html part\r\nhttps://www.there.com", $message->getTextBody()); + self::assertEquals("This is the first html part

\n

This is the second html part

\n

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



\r\n
", $message->getHTMLBody()); + + self::assertEquals("2023-02-16 09:19:02", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + + $from = $message->from->first(); + self::assertEquals("FromName", $from->personal); + self::assertEquals("from", $from->mailbox); + self::assertEquals("there.com", $from->host); + self::assertEquals("from@there.com", $from->mail); + self::assertEquals("FromName ", $from->full); + + self::assertEquals("to@there.com", $message->to->first()); + + $attachments = $message->attachments(); + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("attachment1.pdf", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/pdf", $attachment->content_type); + self::assertEquals("c162adf19e0f67e26ef0b7f791b33a60b2c23b175560a505dc7f9ec490206e49", hash("sha256", $attachment->content)); + self::assertEquals(4814, $attachment->size); + self::assertEquals(4, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("attachment2.pdf", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/pdf", $attachment->content_type); + self::assertEquals("a337b37e9d3edb172a249639919f0eee3d344db352046d15f8f9887e55855a25", hash("sha256", $attachment->content)); + self::assertEquals(5090, $attachment->size); + self::assertEquals(6, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/MultipleNestedAttachmentsTest.php b/tests/fixtures/MultipleNestedAttachmentsTest.php new file mode 100644 index 00000000..e7b9cbdf --- /dev/null +++ b/tests/fixtures/MultipleNestedAttachmentsTest.php @@ -0,0 +1,67 @@ +getFixture("multiple_nested_attachments.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("------------------------------------------------------------------------", $message->getTextBody()); + self::assertEquals("\r\n \r\n\r\n \r\n \r\n \r\n


\r\n

\r\n
\r\n \r\n \r\n  \"\"\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n

\r\n

\r\n
\r\n
\r\n \r\n", $message->getHTMLBody()); + + self::assertEquals("2018-01-15 09:54:09", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertFalse($message->from->first()); + self::assertFalse($message->to->first()); + + $attachments = $message->attachments(); + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("mleokdgdlgkkecep.png", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("image/png", $attachment->content_type); + self::assertEquals("e0e99b0bd6d5ea3ced99add53cc98b6f8eea6eae8ddd773fd06f3489289385fb", hash("sha256", $attachment->content)); + self::assertEquals(114, $attachment->size); + self::assertEquals(5, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("FF4D00-1.png", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("image/png", $attachment->content_type); + self::assertEquals("e0e99b0bd6d5ea3ced99add53cc98b6f8eea6eae8ddd773fd06f3489289385fb", hash("sha256", $attachment->content)); + self::assertEquals(114, $attachment->size); + self::assertEquals(8, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/NestesEmbeddedWithAttachmentTest.php b/tests/fixtures/NestesEmbeddedWithAttachmentTest.php new file mode 100644 index 00000000..a1db7378 --- /dev/null +++ b/tests/fixtures/NestesEmbeddedWithAttachmentTest.php @@ -0,0 +1,67 @@ +getFixture("nestes_embedded_with_attachment.eml"); + + self::assertEquals("Nuu", $message->subject); + self::assertEquals("Dear Sarah", $message->getTextBody()); + self::assertEquals("\r\n\r\n
Dear Sarah,
\r\n", $message->getHTMLBody()); + + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + + $attachments = $message->attachments(); + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("first.eml", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("From: from@there.com\r\nTo: to@here.com\r\nSubject: FIRST\r\nDate: Sat, 28 Apr 2018 14:37:16 -0400\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"----=_NextPart_000_222_000\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextPart_000_222_000\r\nContent-Type: multipart/alternative;\r\n boundary=\"----=_NextPart_000_222_111\"\r\n\r\n\r\n------=_NextPart_000_222_111\r\nContent-Type: text/plain;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nPlease respond directly to this email to update your RMA\r\n\r\n\r\n2018-04-17T11:04:03-04:00\r\n------=_NextPart_000_222_111\r\nContent-Type: text/html;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n\r\n\r\n
Please respond directly to this =\r\nemail to=20\r\nupdate your RMA
\r\n\r\n------=_NextPart_000_222_111--\r\n\r\n------=_NextPart_000_222_000\r\nContent-Type: image/png;\r\n name=\"chrome.png\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment;\r\n filename=\"chrome.png\"\r\n\r\niVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0Xk\r\nNTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8\r\nkkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwalju\r\nZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklH\r\npEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZF\r\no0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzv\r\nvhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGd\r\nyKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWf\r\nwtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+u\r\nSi6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm\r\n5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUA\r\nLabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxh\r\neD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T\r\n5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7\r\ns7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3\r\na/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU\r\n9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RH\r\niTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZN\r\nJ3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSu\r\nQmCC\r\n\r\n------=_NextPart_000_222_000--", $attachment->content); + self::assertEquals(2535, $attachment->size); + self::assertEquals(5, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("second.eml", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("From: from@there.com\r\nTo: to@here.com\r\nSubject: SECOND\r\nDate: Sat, 28 Apr 2018 13:37:30 -0400\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative;\r\n boundary=\"----=_NextPart_000_333_000\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextPart_000_333_000\r\nContent-Type: text/plain;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nT whom it may concern:\r\n------=_NextPart_000_333_000\r\nContent-Type: text/html;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n\r\n\r\n
T whom it may concern:
\r\n\r\n\r\n------=_NextPart_000_333_000--", $attachment->content); + self::assertEquals(631, $attachment->size); + self::assertEquals(6, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/NullContentCharsetTest.php b/tests/fixtures/NullContentCharsetTest.php new file mode 100644 index 00000000..f8e6c63f --- /dev/null +++ b/tests/fixtures/NullContentCharsetTest.php @@ -0,0 +1,39 @@ +getFixture("null_content_charset.eml"); + + self::assertEquals("test", $message->getSubject()); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertEquals("1.0", $message->mime_version); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/PecTest.php b/tests/fixtures/PecTest.php new file mode 100644 index 00000000..01fe17c5 --- /dev/null +++ b/tests/fixtures/PecTest.php @@ -0,0 +1,79 @@ +getFixture("pec.eml"); + + self::assertEquals("Certified", $message->subject); + self::assertEquals("Signed", $message->getTextBody()); + self::assertEquals("Signed", $message->getHTMLBody()); + + self::assertEquals("2017-10-02 10:13:43", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("test@example.com", $message->from->first()->mail); + self::assertEquals("test@example.com", $message->to->first()->mail); + + $attachments = $message->attachments(); + + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(3, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("data.xml", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/xml", $attachment->content_type); + self::assertEquals("", $attachment->content); + self::assertEquals(8, $attachment->size); + self::assertEquals(4, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("postacert.eml", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("To: test@example.com\r\nFrom: test@example.com\r\nSubject: test-subject\r\nDate: Mon, 2 Oct 2017 12:13:50 +0200\r\nContent-Type: text/plain; charset=iso-8859-15; format=flowed\r\nContent-Transfer-Encoding: 7bit\r\n\r\ntest-content", $attachment->content); + self::assertEquals(216, $attachment->size); + self::assertEquals(5, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[2]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("smime.p7s", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/x-pkcs7-signature", $attachment->content_type); + self::assertEquals("1", $attachment->content); + self::assertEquals(4, $attachment->size); + self::assertEquals(7, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/PlainOnlyTest.php b/tests/fixtures/PlainOnlyTest.php new file mode 100644 index 00000000..b3a65bfe --- /dev/null +++ b/tests/fixtures/PlainOnlyTest.php @@ -0,0 +1,37 @@ +getFixture("plain_only.eml"); + + self::assertEquals("Nuu", $message->getSubject()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/PlainTextAttachmentTest.php b/tests/fixtures/PlainTextAttachmentTest.php new file mode 100644 index 00000000..6106b558 --- /dev/null +++ b/tests/fixtures/PlainTextAttachmentTest.php @@ -0,0 +1,53 @@ +getFixture("plain_text_attachment.eml"); + + self::assertEquals("Plain text attachment", $message->subject); + self::assertEquals("Test", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2018-08-21 07:05:14", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("a.txt", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertNull($attachment->content_type); + self::assertEquals("Hi!", $attachment->content); + self::assertEquals(4, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/ReferencesTest.php b/tests/fixtures/ReferencesTest.php new file mode 100644 index 00000000..18473d61 --- /dev/null +++ b/tests/fixtures/ReferencesTest.php @@ -0,0 +1,54 @@ +getFixture("references.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("Hi\r\nHow are you?", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertFalse($message->date->first()); + + self::assertEquals("b9e87bd5e661a645ed6e3b832828fcc5@example.com", $message->in_reply_to); + self::assertEquals("", $message->from->first()->personal); + self::assertEquals("UNKNOWN", $message->from->first()->host); + self::assertEquals("no_host@UNKNOWN", $message->from->first()->mail); + self::assertFalse($message->to->first()); + + self::assertEquals([ + "231d9ac57aec7d8c1a0eacfeab8af6f3@example.com", + "08F04024-A5B3-4FDE-BF2C-6710DE97D8D9@example.com" + ], $message->getReferences()->all()); + + self::assertEquals([ + 'This one: is "right" ', + 'No-address@UNKNOWN' + ], $message->cc->map(function($address){ + /** @var \Webklex\PHPIMAP\Address $address */ + return $address->full; + })); + } +} \ No newline at end of file diff --git a/tests/fixtures/SimpleMultipartTest.php b/tests/fixtures/SimpleMultipartTest.php new file mode 100644 index 00000000..d2a2d885 --- /dev/null +++ b/tests/fixtures/SimpleMultipartTest.php @@ -0,0 +1,37 @@ +getFixture("simple_multipart.eml"); + + self::assertEquals("test", $message->getSubject()); + self::assertEquals("MyPlain", $message->getTextBody()); + self::assertEquals("MyHtml", $message->getHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/StructuredWithAttachmentTest.php b/tests/fixtures/StructuredWithAttachmentTest.php new file mode 100644 index 00000000..a49d3609 --- /dev/null +++ b/tests/fixtures/StructuredWithAttachmentTest.php @@ -0,0 +1,54 @@ +getFixture("structured_with_attachment.eml"); + + self::assertEquals("Test", $message->getSubject()); + self::assertEquals("Test", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2017-09-29 08:55:23", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("MyFile.txt", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals("text/plain", $attachment->content_type); + self::assertEquals("MyFileContent", $attachment->content); + self::assertEquals(20, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/tests/fixtures/UndefinedCharsetHeaderTest.php b/tests/fixtures/UndefinedCharsetHeaderTest.php new file mode 100644 index 00000000..acb3029c --- /dev/null +++ b/tests/fixtures/UndefinedCharsetHeaderTest.php @@ -0,0 +1,59 @@ +getFixture("undefined_charset_header.eml"); + + self::assertEquals("", $message->get("x-real-to")); + self::assertEquals("1.0", $message->get("mime-version")); + self::assertEquals("Mon, 27 Feb 2017 13:21:44 +0930", $message->get("Resent-Date")); + self::assertEquals("", $message->get("Resent-From")); + self::assertEquals("BlaBla", $message->get("X-Stored-In")); + self::assertEquals("", $message->get("Return-Path")); + self::assertEquals([ + 'from by bla.bla (CommuniGate Pro RULE 6.1.13) with RULE id 14057804; Mon, 27 Feb 2017 13:21:44 +0930', + 'from by bla.bla (CommuniGate Pro RULE 6.1.13) with RULE id 14057804' + ], $message->get("Received")->all()); + self::assertEquals(")", $message->getHTMLBody()); + self::assertFalse($message->hasTextBody()); + self::assertEquals("2017-02-27 03:51:29", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + + $from = $message->from->first(); + self::assertInstanceOf(Address::class, $from); + + self::assertEquals("myGov", $from->personal); + self::assertEquals("info", $from->mailbox); + self::assertEquals("bla.bla", $from->host); + self::assertEquals("info@bla.bla", $from->mail); + self::assertEquals("myGov ", $from->full); + + self::assertEquals("sales@bla.bla", $message->to->first()->mail); + self::assertEquals("Submit your tax refund | Australian Taxation Office.", $message->subject); + self::assertEquals("201702270351.BGF77614@bla.bla", $message->message_id); + } +} \ No newline at end of file diff --git a/tests/fixtures/UndisclosedRecipientsMinusTest.php b/tests/fixtures/UndisclosedRecipientsMinusTest.php new file mode 100644 index 00000000..f9ee7999 --- /dev/null +++ b/tests/fixtures/UndisclosedRecipientsMinusTest.php @@ -0,0 +1,42 @@ +getFixture("undisclosed_recipients_minus.eml"); + + self::assertEquals("test", $message->subject); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals([ + "undisclosed-recipients", + "" + ], $message->to->map(function ($item) { + return $item->mailbox; + })); + } +} \ No newline at end of file diff --git a/tests/fixtures/UndisclosedRecipientsSpaceTest.php b/tests/fixtures/UndisclosedRecipientsSpaceTest.php new file mode 100644 index 00000000..b86321eb --- /dev/null +++ b/tests/fixtures/UndisclosedRecipientsSpaceTest.php @@ -0,0 +1,42 @@ +getFixture("undisclosed_recipients_space.eml"); + + self::assertEquals("test", $message->subject); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals([ + "Undisclosed recipients", + "" + ], $message->to->map(function ($item) { + return $item->mailbox; + })); + } +} \ No newline at end of file diff --git a/tests/fixtures/UnknownEncodingTest.php b/tests/fixtures/UnknownEncodingTest.php new file mode 100644 index 00000000..3a570cbd --- /dev/null +++ b/tests/fixtures/UnknownEncodingTest.php @@ -0,0 +1,37 @@ +getFixture("unknown_encoding.eml"); + + self::assertEquals("test", $message->getSubject()); + self::assertEquals("MyPlain", $message->getTextBody()); + self::assertEquals("MyHtml", $message->getHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/WithoutCharsetPlainOnlyTest.php b/tests/fixtures/WithoutCharsetPlainOnlyTest.php new file mode 100644 index 00000000..497334d5 --- /dev/null +++ b/tests/fixtures/WithoutCharsetPlainOnlyTest.php @@ -0,0 +1,37 @@ +getFixture("without_charset_plain_only.eml"); + + self::assertEquals("Nuu", $message->getSubject()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/fixtures/WithoutCharsetSimpleMultipartTest.php b/tests/fixtures/WithoutCharsetSimpleMultipartTest.php new file mode 100644 index 00000000..2a9ea2a0 --- /dev/null +++ b/tests/fixtures/WithoutCharsetSimpleMultipartTest.php @@ -0,0 +1,37 @@ +getFixture("without_charset_simple_multipart.eml"); + + self::assertEquals("test", $message->getSubject()); + self::assertEquals("MyPlain", $message->getTextBody()); + self::assertEquals("MyHtml", $message->getHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/tests/messages/attachment_encoded_filename.eml b/tests/messages/attachment_encoded_filename.eml new file mode 100644 index 00000000..231d0452 --- /dev/null +++ b/tests/messages/attachment_encoded_filename.eml @@ -0,0 +1,10 @@ +Content-Type: multipart/mixed; + boundary="BOUNDARY" + +--BOUNDARY +Content-Type: application/vnd.ms-excel; name="=?UTF-8?Q?Prost=C5=99eno=5F2014=5Fposledn=C3=AD_voln=C3=A9_term=C3=ADny.xls?="; charset="UTF-8" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="=?UTF-8?Q?Prost=C5=99eno=5F2014=5Fposledn=C3=AD_voln=C3=A9_term=C3=ADny.xls?=" + +0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAPgADAP7/CQAGAAAAAAAAAAAAAAACAAAAwgAAAAAA +AAAAEAAA/v///wAAAAD+////AAAAAMAAAADBAAAA//////////////////////////////// diff --git a/tests/messages/attachment_long_filename.eml b/tests/messages/attachment_long_filename.eml new file mode 100644 index 00000000..8630280f --- /dev/null +++ b/tests/messages/attachment_long_filename.eml @@ -0,0 +1,55 @@ +Content-Type: multipart/mixed; + boundary="BOUNDARY" + +--BOUNDARY +Content-Type: text/plain; + name*0*=utf-8''Buchungsbest%C3%A4tigung-%20Rechnung-Gesch%C3%A4ftsbedingung; + name*1*=en-Nr.B123-45%20-%20XXXX%20xxxxxxxxxxxxxxxxx%20XxxX%2C%20L%C3%BCd; + name*2*=xxxxxxxx%20-%20VM%20Klaus%20XXXXXX%20-%20xxxxxxxx.pdf +Content-Disposition: attachment; + filename*0*=utf-8''Buchungsbest%C3%A4tigung-%20Rechnung-Gesch%C3%A4ftsbedin; + filename*1*=gungen-Nr.B123-45%20-%20XXXXX%20xxxxxxxxxxxxxxxxx%20XxxX%2C; + filename*2*=%20L%C3%BCxxxxxxxxxx%20-%20VM%20Klaus%20XXXXXX%20-%20xxxxxxxx.p; + filename*3*=df +Content-Transfer-Encoding: base64 + +SGkh +--BOUNDARY +Content-Type: text/plain; charset=UTF-8; + name="=?UTF-8?B?MDFfQeKCrMOgw6TEhdCx2YrYr0BaLTAxMjM0NTY3ODktcXdlcnR5dWlv?= + =?UTF-8?Q?pasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzx?= + =?UTF-8?Q?cvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstu?= + =?UTF-8?Q?vz.txt?=" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename*0*=iso-8859-15''%30%31%5F%41%A4%E0%E4%3F%3F%3F%3F%40%5A%2D%30%31; + filename*1*=%32%33%34%35%36%37%38%39%2D%71%77%65%72%74%79%75%69%6F%70%61; + filename*2*=%73%64%66%67%68%6A%6B%6C%7A%78%63%76%62%6E%6D%6F%70%71%72%73; + filename*3*=%74%75%76%7A%2D%30%31%32%33%34%35%36%37%38%39%2D%71%77%65%72; + filename*4*=%74%79%75%69%6F%70%61%73%64%66%67%68%6A%6B%6C%7A%78%63%76%62; + filename*5*=%6E%6D%6F%70%71%72%73%74%75%76%7A%2D%30%31%32%33%34%35%36%37; + filename*6*=%38%39%2D%71%77%65%72%74%79%75%69%6F%70%61%73%64%66%67%68%6A; + filename*7*=%6B%6C%7A%78%63%76%62%6E%6D%6F%70%71%72%73%74%75%76%7A%2E%74; + filename*8*=%78%74 + +SGkh +--BOUNDARY +Content-Type: text/plain; charset=UTF-8; + name="=?UTF-8?B?MDJfQeKCrMOgw6TEhdCx2YrYr0BaLTAxMjM0NTY3ODktcXdlcnR5dWlv?= + =?UTF-8?Q?pasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzx?= + =?UTF-8?Q?cvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstu?= + =?UTF-8?Q?vz.txt?=" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename*0*=UTF-8''%30%32%5F%41%E2%82%AC%C3%A0%C3%A4%C4%85%D0%B1%D9%8A%D8; + filename*1*=%AF%40%5A%2D%30%31%32%33%34%35%36%37%38%39%2D%71%77%65%72%74; + filename*2*=%79%75%69%6F%70%61%73%64%66%67%68%6A%6B%6C%7A%78%63%76%62%6E; + filename*3*=%6D%6F%70%71%72%73%74%75%76%7A%2D%30%31%32%33%34%35%36%37%38; + filename*4*=%39%2D%71%77%65%72%74%79%75%69%6F%70%61%73%64%66%67%68%6A%6B; + filename*5*=%6C%7A%78%63%76%62%6E%6D%6F%70%71%72%73%74%75%76%7A%2D%30%31; + filename*6*=%32%33%34%35%36%37%38%39%2D%71%77%65%72%74%79%75%69%6F%70%61; + filename*7*=%73%64%66%67%68%6A%6B%6C%7A%78%63%76%62%6E%6D%6F%70%71%72%73; + filename*8*=%74%75%76%7A%2E%74%78%74 + +SGkh +--BOUNDARY-- diff --git a/tests/messages/attachment_no_disposition.eml b/tests/messages/attachment_no_disposition.eml new file mode 100644 index 00000000..ce0ea3e9 --- /dev/null +++ b/tests/messages/attachment_no_disposition.eml @@ -0,0 +1,9 @@ +Content-Type: multipart/mixed; + boundary="BOUNDARY" + +--BOUNDARY +Content-Type: application/vnd.ms-excel; name="=?UTF-8?Q?Prost=C5=99eno=5F2014=5Fposledn=C3=AD_voln=C3=A9_term=C3=ADny.xls?="; charset="UTF-8" +Content-Transfer-Encoding: base64 + +0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAPgADAP7/CQAGAAAAAAAAAAAAAAACAAAAwgAAAAAA +AAAAEAAA/v///wAAAAD+////AAAAAMAAAADBAAAA//////////////////////////////// diff --git a/tests/messages/bcc.eml b/tests/messages/bcc.eml new file mode 100644 index 00000000..1a6f16a8 --- /dev/null +++ b/tests/messages/bcc.eml @@ -0,0 +1,12 @@ +Return-Path: +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: to@here.com +Bcc: =?UTF-8?B?QV/igqxAe8OoX1o=?= +Reply-To: reply-to@here.com +Sender: sender@here.com + +Hi! diff --git a/tests/messages/boolean_decoded_content.eml b/tests/messages/boolean_decoded_content.eml new file mode 100644 index 00000000..91818038 --- /dev/null +++ b/tests/messages/boolean_decoded_content.eml @@ -0,0 +1,31 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: multipart/mixed; boundary="=-vyqYb0SSRwuGFKv/Trdf" + +--=-vyqYb0SSRwuGFKv/Trdf +Content-Type: multipart/alternative; boundary="=-ewUvwipK68Y6itClYNpy" + +--=-ewUvwipK68Y6itClYNpy +Content-Type: text/plain; charset="us-ascii" + +Here is the problem mail + +Body text +--=-ewUvwipK68Y6itClYNpy +Content-Type: text/html; charset="us-ascii" + +Here is the problem mail + +Body text +--=-ewUvwipK68Y6itClYNpy-- + +--=-vyqYb0SSRwuGFKv/Trdf +Content-Type: application/pdf; name="Example Domain.pdf" +Content-Disposition: attachment; filename="Example Domain.pdf" +Content-Transfer-Encoding: base64 + +nnDusSNdG92w6Fuw61fMjAxOF8wMy0xMzMyNTMzMTkzLnBkZg==?= + +--=-vyqYb0SSRwuGFKv/Trdf-- diff --git a/tests/messages/date-template.eml b/tests/messages/date-template.eml new file mode 100644 index 00000000..9354fd10 --- /dev/null +++ b/tests/messages/date-template.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: %date_raw_header% +From: from@there.com +To: to@here.com + +Hi! diff --git a/tests/messages/email_address.eml b/tests/messages/email_address.eml new file mode 100644 index 00000000..62b76d70 --- /dev/null +++ b/tests/messages/email_address.eml @@ -0,0 +1,9 @@ +Message-ID: <123@example.com> +From: no_host +Cc: "This one: is \"right\"" , No-address +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi +How are you? diff --git a/tests/messages/embedded_email.eml b/tests/messages/embedded_email.eml new file mode 100644 index 00000000..293bc28e --- /dev/null +++ b/tests/messages/embedded_email.eml @@ -0,0 +1,63 @@ +Return-Path: demo@cerstor.cz +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:25:40 +0100 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=_5d4b2de51e6aad0435b4bfbd7a23594a" +Date: Fri, 29 Jan 2016 14:25:40 +0100 +From: demo@cerstor.cz +To: demo@cerstor.cz +Subject: embedded message +Message-ID: <7e5798da5747415e5b82fdce042ab2a6@cerstor.cz> +X-Sender: demo@cerstor.cz +User-Agent: Roundcube Webmail/1.0.0 + +--=_5d4b2de51e6aad0435b4bfbd7a23594a +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; charset=US-ASCII; + format=flowed + +email that contains embedded message +--=_5d4b2de51e6aad0435b4bfbd7a23594a +Content-Transfer-Encoding: 8bit +Content-Type: message/rfc822; + name=demo.eml +Content-Disposition: attachment; + filename=demo.eml; + size=889 + +Return-Path: demo@cerstor.cz +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:22:13 +0100 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=_995890bdbf8bd158f2cbae0e8d966000" +Date: Fri, 29 Jan 2016 14:22:13 +0100 +From: demo-from@cerstor.cz +To: demo-to@cerstor.cz +Subject: demo +Message-ID: <4cbaf57cb00891c53b32e1d63367740c@cerstor.cz> +X-Sender: demo@cerstor.cz +User-Agent: Roundcube Webmail/1.0.0 + +--=_995890bdbf8bd158f2cbae0e8d966000 +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; charset=US-ASCII; + format=flowed + +demo text +--=_995890bdbf8bd158f2cbae0e8d966000 +Content-Transfer-Encoding: base64 +Content-Type: text/plain; + name=testfile.txt +Content-Disposition: attachment; + filename=testfile.txt; + size=29 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= +--=_995890bdbf8bd158f2cbae0e8d966000-- + + +--=_5d4b2de51e6aad0435b4bfbd7a23594a-- diff --git a/tests/messages/embedded_email_without_content_disposition-embedded.eml b/tests/messages/embedded_email_without_content_disposition-embedded.eml new file mode 100644 index 00000000..df5fa40b --- /dev/null +++ b/tests/messages/embedded_email_without_content_disposition-embedded.eml @@ -0,0 +1,61 @@ +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:25:40 +0100 +From: demo@cerstor.cz +To: demo@cerstor.cz +Date: Fri, 5 Apr 2019 12:10:49 +0200 +Subject: embedded_message_subject +Message-ID: +Accept-Language: pl-PL, nl-NL +Content-Language: pl-PL +Content-Type: multipart/mixed; + boundary="_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_" +MIME-Version: 1.0 + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: multipart/alternative; + boundary="_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_" + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: text/plain; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + +some txt + + + + + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: text/html; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + + +

some txt

+ + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_-- + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file1.xlsx" +Content-Description: file1.xlsx +Content-Disposition: attachment; filename="file1.xlsx"; size=29; + creation-date="Fri, 05 Apr 2019 10:06:01 GMT"; + modification-date="Fri, 05 Apr 2019 10:10:49 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file2.xlsx" +Content-Description: file2 +Content-Disposition: attachment; filename="file2.xlsx"; size=29; + creation-date="Fri, 05 Apr 2019 10:10:19 GMT"; + modification-date="Wed, 03 Apr 2019 11:04:32 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_-- diff --git a/tests/messages/embedded_email_without_content_disposition.eml b/tests/messages/embedded_email_without_content_disposition.eml new file mode 100644 index 00000000..c5c8cffe --- /dev/null +++ b/tests/messages/embedded_email_without_content_disposition.eml @@ -0,0 +1,141 @@ +Return-Path: demo@cerstor.cz +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:25:40 +0100 +From: demo@cerstor.cz +To: demo@cerstor.cz +Date: Fri, 5 Apr 2019 13:48:50 +0200 +Subject: Subject +Message-ID: +Accept-Language: pl-PL, nl-NL +Content-Type: multipart/mixed; + boundary="_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_" +MIME-Version: 1.0 + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: multipart/related; + boundary="_007_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_"; + type="multipart/alternative" + +--_007_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: multipart/alternative; + boundary="_000_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_" + +--_000_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: text/plain; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + +TexT + +[cid:file.jpg] + +--_000_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: text/html; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + +

TexT

= + +--_000_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_-- + +--_007_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: image/jpeg; name= + "file.jpg" +Content-Description: file.jpg +Content-Disposition: inline; filename= + "file.jpg"; + size=54558; creation-date="Fri, 05 Apr 2019 11:48:58 GMT"; + modification-date="Fri, 05 Apr 2019 11:48:58 GMT" +Content-ID: +Content-Transfer-Encoding: base64 + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg== + +--_007_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_-- + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: message/rfc822 + +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:25:40 +0100 +From: demo@cerstor.cz +To: demo@cerstor.cz +Date: Fri, 5 Apr 2019 12:10:49 +0200 +Subject: embedded_message_subject +Message-ID: +Accept-Language: pl-PL, nl-NL +Content-Language: pl-PL +Content-Type: multipart/mixed; + boundary="_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_" +MIME-Version: 1.0 + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: multipart/alternative; + boundary="_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_" + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: text/plain; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + +some txt + + + + + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: text/html; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + + +

some txt

+ + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_-- + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file1.xlsx" +Content-Description: file1.xlsx +Content-Disposition: attachment; filename="file1.xlsx"; size=29; + creation-date="Fri, 05 Apr 2019 10:06:01 GMT"; + modification-date="Fri, 05 Apr 2019 10:10:49 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file2.xlsx" +Content-Description: file2 +Content-Disposition: attachment; filename="file2.xlsx"; size=29; + creation-date="Fri, 05 Apr 2019 10:10:19 GMT"; + modification-date="Wed, 03 Apr 2019 11:04:32 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_-- + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file3.xlsx" +Content-Description: file3.xlsx +Content-Disposition: attachment; filename="file3.xlsx"; + size=90672; creation-date="Fri, 05 Apr 2019 11:46:30 GMT"; + modification-date="Thu, 28 Mar 2019 08:07:58 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: application/x-zip-compressed; name="file4.zip" +Content-Description: file4.zip +Content-Disposition: attachment; filename="file4.zip"; size=29; + creation-date="Fri, 05 Apr 2019 11:48:45 GMT"; + modification-date="Fri, 05 Apr 2019 11:35:24 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_-- diff --git a/tests/messages/example_bounce.eml b/tests/messages/example_bounce.eml new file mode 100644 index 00000000..a1f5683f --- /dev/null +++ b/tests/messages/example_bounce.eml @@ -0,0 +1,96 @@ +Return-Path: <> +Received: from somewhere.your-server.de + by somewhere.your-server.de with LMTP + id 3TP8LrElAGSOaAAAmBr1xw + (envelope-from <>); Thu, 02 Mar 2023 05:27:29 +0100 +Envelope-to: demo@foo.de +Delivery-date: Thu, 02 Mar 2023 05:27:29 +0100 +Authentication-Results: somewhere.your-server.de; + iprev=pass (somewhere06.your-server.de) smtp.remote-ip=1b21:2f8:e0a:50e4::2; + spf=none smtp.mailfrom=<>; + dmarc=skipped +Received: from somewhere06.your-server.de ([1b21:2f8:e0a:50e4::2]) + by somewhere.your-server.de with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384 + (Exim 4.94.2) + id 1pXaXR-0006xQ-BN + for demo@foo.de; Thu, 02 Mar 2023 05:27:29 +0100 +Received: from [192.168.0.10] (helo=sslproxy01.your-server.de) + by somewhere06.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) + (Exim 4.92) + id 1pXaXO-000LYP-9R + for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100 +Received: from localhost ([127.0.0.1] helo=sslproxy01.your-server.de) + by sslproxy01.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) + (Exim 4.92) + id 1pXaXO-0008gy-7x + for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100 +Received: from Debian-exim by sslproxy01.your-server.de with local (Exim 4.92) + id 1pXaXO-0008gb-6g + for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100 +X-Failed-Recipients: ding@ding.de +Auto-Submitted: auto-replied +From: Mail Delivery System +To: demo@foo.de +Content-Type: multipart/report; report-type=delivery-status; boundary=1677731246-eximdsn-678287796 +MIME-Version: 1.0 +Subject: Mail delivery failed +Message-Id: +Date: Thu, 02 Mar 2023 05:27:26 +0100 +X-Virus-Scanned: Clear (ClamAV 0.103.8/26827/Wed Mar 1 09:28:49 2023) +X-Spam-Score: 0.0 (/) +Delivered-To: bar-demo@foo.de + +--1677731246-eximdsn-678287796 +Content-type: text/plain; charset=us-ascii + +This message was created automatically by mail delivery software. + +A message sent by + + + +could not be delivered to one or more of its recipients. The following +address(es) failed: + + ding@ding.de + host 36.143.65.153 [36.143.65.153] + SMTP error from remote mail server after pipelined end of data: + 550-Verification failed for + 550-Unrouteable address + 550 Sender verify failed + +--1677731246-eximdsn-678287796 +Content-type: message/delivery-status + +Reporting-MTA: dns; sslproxy01.your-server.de + +Action: failed +Final-Recipient: rfc822;ding@ding.de +Status: 5.0.0 +Remote-MTA: dns; 36.143.65.153 +Diagnostic-Code: smtp; 550-Verification failed for + 550-Unrouteable address + 550 Sender verify failed + +--1677731246-eximdsn-678287796 +Content-type: message/rfc822 + +Return-path: +Received: from [31.18.107.47] (helo=127.0.0.1) + by sslproxy01.your-server.de with esmtpsa (TLSv1.3:TLS_AES_256_GCM_SHA384:256) + (Exim 4.92) + (envelope-from ) + id 1pXaXO-0008eK-11 + for ding@ding.de; Thu, 02 Mar 2023 05:27:26 +0100 +Date: Thu, 2 Mar 2023 05:27:25 +0100 +To: ding +From: =?iso-8859-1?Q?S=C3=BCderbar_Foo_=28SI=29_GmbH?= +Subject: Test +Message-ID: +X-Mailer: PHPMailer 6.7.1 (https://github.com/PHPMailer/PHPMailer) +Return-Path: bounce@foo.de +MIME-Version: 1.0 +Content-Type: text/html; charset=iso-8859-1 +X-Authenticated-Sender: demo@foo.de + +
Hallo, dies ist ein Beispiel-Text.
\ No newline at end of file diff --git a/tests/messages/four_nested_emails.eml b/tests/messages/four_nested_emails.eml new file mode 100644 index 00000000..0b34e986 --- /dev/null +++ b/tests/messages/four_nested_emails.eml @@ -0,0 +1,69 @@ +To: test@example.com +From: test@example.com +Subject: 3-third-subject +Content-Type: multipart/mixed; + boundary="------------2E5D78A17C812FEFF825F7D5" + +This is a multi-part message in MIME format. +--------------2E5D78A17C812FEFF825F7D5 +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +3-third-content +--------------2E5D78A17C812FEFF825F7D5 +Content-Type: message/rfc822; + name="2-second-email.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="2-second-email.eml" + +To: test@example.com +From: test@example.com +Subject: 2-second-subject +Content-Type: multipart/mixed; + boundary="------------9919377E37A03209B057D47F" + +This is a multi-part message in MIME format. +--------------9919377E37A03209B057D47F +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +2-second-content +--------------9919377E37A03209B057D47F +Content-Type: message/rfc822; + name="1-first-email.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="1-first-email.eml" + +To: test@example.com +From: test@example.com +Subject: 1-first-subject +Content-Type: multipart/mixed; + boundary="------------0919377E37A03209B057D47A" + +This is a multi-part message in MIME format. +--------------0919377E37A03209B057D47A +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +1-first-content +--------------0919377E37A03209B057D47A +Content-Type: message/rfc822; + name="0-zero-email.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="0-zero-email.eml" + +To: test@example.com +From: test@example.com +Subject: 0-zero-subject +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +0-zero-content +--------------0919377E37A03209B057D47A-- + +--------------9919377E37A03209B057D47F-- + +--------------2E5D78A17C812FEFF825F7D5-- diff --git a/tests/messages/gbk_charset.eml b/tests/messages/gbk_charset.eml new file mode 100644 index 00000000..b40936bd --- /dev/null +++ b/tests/messages/gbk_charset.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="X-GBK" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/tests/messages/html_only.eml b/tests/messages/html_only.eml new file mode 100644 index 00000000..ce3ee17c --- /dev/null +++ b/tests/messages/html_only.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/html; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file diff --git a/tests/messages/imap_mime_header_decode_returns_false.eml b/tests/messages/imap_mime_header_decode_returns_false.eml new file mode 100644 index 00000000..af3a7f36 --- /dev/null +++ b/tests/messages/imap_mime_header_decode_returns_false.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: =?UTF-8?B?nnDusSNdG92w6Fuw61fMjAxOF8wMy0xMzMyNTMzMTkzLnBkZg==?= +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/tests/messages/inline_attachment.eml b/tests/messages/inline_attachment.eml new file mode 100644 index 00000000..be60f598 --- /dev/null +++ b/tests/messages/inline_attachment.eml @@ -0,0 +1,41 @@ +Content-Type: multipart/mixed; + boundary="----=_Part_1114403_1160068121.1505882828080" + +------=_Part_1114403_1160068121.1505882828080 +Content-Type: multipart/related; + boundary="----=_Part_1114404_576719783.1505882828080" + +------=_Part_1114404_576719783.1505882828080 +Content-Type: text/html;charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + + +------=_Part_1114404_576719783.1505882828080 +Content-Type: image/png +Content-Disposition: inline +Content-Transfer-Encoding: base64 +Content-ID: + +iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0Xk +NTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8 +kkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwalju +ZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklH +pEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZF +o0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzv +vhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGd +yKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWf +wtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+u +Si6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm +5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUA +LabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxh +eD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T +5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7 +s7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3 +a/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU +9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RH +iTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZN +J3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSu +QmCC +------=_Part_1114404_576719783.1505882828080-- + +------=_Part_1114403_1160068121.1505882828080-- diff --git a/tests/messages/ks_c_5601-1987_headers.eml b/tests/messages/ks_c_5601-1987_headers.eml new file mode 100644 index 00000000..a8de3d08 --- /dev/null +++ b/tests/messages/ks_c_5601-1987_headers.eml @@ -0,0 +1,10 @@ +Subject: =?ks_c_5601-1987?B?UkU6IMi4v/i01LKyIEVyc2m01MDMILjevcPB9rimILq4s8K9wLTP?= + =?ks_c_5601-1987?B?tNku?= +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +Thread-Topic: =?ks_c_5601-1987?B?yLi/+LTUsrIgRXJzabTUwMwguN69w8H2uKYgurizwr3AtM+02S4=?= +From: =?ks_c_5601-1987?B?seggx/bB+A==?= +To: to@here.com + +Content diff --git a/tests/messages/mail_that_is_attachment.eml b/tests/messages/mail_that_is_attachment.eml new file mode 100644 index 00000000..89215cf8 --- /dev/null +++ b/tests/messages/mail_that_is_attachment.eml @@ -0,0 +1,28 @@ +Sender: xxx@yyy.cz +MIME-Version: 1.0 +Message-ID: <2244696771454641389@google.com> +Date: Sun, 15 Feb 2015 10:21:51 +0000 +Subject: Report domain: yyy.cz Submitter: google.com Report-ID: 2244696771454641389 +From: noreply-dmarc-support via xxx +To: xxx@yyy.cz +Content-Type: application/zip; + name="google.com!yyy.cz!1423872000!1423958399.zip" +Content-Disposition: attachment; + filename="google.com!yyy.cz!1423872000!1423958399.zip" +Content-Transfer-Encoding: base64 +Reply-To: noreply-dmarc-support@google.com + +UEsDBAoAAAAIABRPT0bdJB+DSwIAALgKAAAuAAAAZ29vZ2xlLmNvbSFzdW5mb3guY3ohMTQyMzg3 +MjAwMCExNDIzOTU4Mzk5LnhtbO1WwY6bMBC971dEuQcDIQSQQ3rqF7RnZIwh7oJt2WY32a+viQ2h +u9moqnarqOop8Gbmjd/4OQbuj127eCJSUc52y8DzlwvCMK8oa3bL79++rpLlYp8/wJqQqkT4MX9Y +LKAkgktddESjCmk0YAblsikY6kjecN60xMO8g2ACbQ7pEG1zxg1De1pVHZJ4pXox0H2Zl9k8V3PU +EhWYM42wLiireX7QWmQAuErvUgkQKCkDiKlnIj1x2tunXRjF8SbxDfFbMtvFaaJVHoZRFKfxdhtE +myiOgnWSQnAJ23SjmxQSscYpM1BJGsryIArXyTb0fdPMImOcsOocTTfJOjWUw7slA7+yTd3mA4aC +txSfCtGXLVUHMi2Em1GxXPVGytHDL4bMIjaMqkfa5RIC++BAJeozNvxaSOSS/CBYQyAcoi6QGjGB +dR4MyoaH80qvrcrMEnM5LlDy52kEivcSk4KKPIz9bVYnpZ9Fvr/OsB9kWbgOTa8pZSzCvGemLQT2 +YYRdZ/KE2t6MrxoDw0yoElxRbTxtvMaImckMmeUNIxFIKZMwTceJr11gGtFM7aueZr9GjZBWhGla +U3OiprIDQRWRRS15N9+nOex43lRD1OtDIYnqW30hfLXY2xZw7h4YnCT3Mqma08GZ3g+gvhgMvFYy +JI82+R3HpL4XbDdesIm84SB/tE9Gr99wSm3+k646xQbu0Sl/uptW0Sfu5tXzH6b3dP7vd1f/+vl/ +KU83eRnpzbX6uY5JzMeJZ25PLwji920S/r8m/tVrAoLLR+hPUEsBAgoACgAAAAgAFE9PRt0kH4NL +AgAAuAoAAC4AAAAAAAAAAAAAAAAAAAAAAGdvb2dsZS5jb20hc3VuZm94LmN6ITE0MjM4NzIwMDAh +MTQyMzk1ODM5OS54bWxQSwUGAAAAAAEAAQBcAAAAlwIAAAAA \ No newline at end of file diff --git a/tests/messages/missing_date.eml b/tests/messages/missing_date.eml new file mode 100644 index 00000000..5f8a6032 --- /dev/null +++ b/tests/messages/missing_date.eml @@ -0,0 +1,7 @@ +From: from@here.com +To: to@here.com +Subject: Nuu +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/tests/messages/missing_from.eml b/tests/messages/missing_from.eml new file mode 100644 index 00000000..e09dd9d9 --- /dev/null +++ b/tests/messages/missing_from.eml @@ -0,0 +1,7 @@ +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/tests/messages/mixed_filename.eml b/tests/messages/mixed_filename.eml new file mode 100644 index 00000000..4a353da3 --- /dev/null +++ b/tests/messages/mixed_filename.eml @@ -0,0 +1,25 @@ +Date: Fri, 02 Feb 2018 22:23:06 +0300 +To: foo@bar.com +Subject: =?windows-1251?B?0eLl5ujpIO/w4OnxLevo8fI=?= +From: =?windows-1251?B?z/Dg6fH7IHx8IM/g8PLK7uw=?= +Content-Transfer-Encoding: quoted-printable +Content-Type: multipart/mixed; + boundary="=_743251f7a933f6b30c004fcb14eabb57" +Content-Disposition: inline + +This is a message in Mime Format. If you see this, your mail reader does not support this format. + +--=_743251f7a933f6b30c004fcb14eabb57 +Content-Type: text/html; charset=windows-1251 +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + + +--=_743251f7a933f6b30c004fcb14eabb57 +Content-Type: application/octet-stream +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="Price4VladDaKar.xlsx" + +UEsDBBQAAgAIAHOyQkwNlxQhWwEAAAYFAAATAAAAW0NvbnRlbnRfVHlwZXNdLnhtbK2UTU7D +CwALANECAAAT1E4AAAA= +--=_743251f7a933f6b30c004fcb14eabb57-- diff --git a/tests/messages/multiple_html_parts_and_attachments.eml b/tests/messages/multiple_html_parts_and_attachments.eml new file mode 100644 index 00000000..5ccbd2c3 --- /dev/null +++ b/tests/messages/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 +sE/7l106384RVptCIZNfOJwIoUlLJbfnr41VqhHz0l4fehht54ityA1WzLbvHxGyeFnTEQfahBSE +L03cURis3CH2HB7y6aTcFwce4zQHxK5Vt0Og3ntk8JA5q5iiY2j+wqwZynHQ7DWs5/Dx/sHx6e8E +Hwyam1FUOuDit+1HJayQiWamx/9yp5myp8ShajV6D0UpZHHKqapeSkUo9nX8VN/AWZlvDyvqODF/ +Xmbsr/W+1u0oR+AFVWIqCYTP2N2gsfNgsA1RR5mKwBpwLynzYz0QLUbtUyr/GVkvBR5F6DNmz4y0 +eQaL2aptYWgZnEWn8G3cL+i8faMkQuaOiVyTJn0rPonY+urgYlT8XzjXAS1QP9anEnsuZrPP9kuQ +5I0x4oj2iXrVM/5lsgAk7w0vxAxjAkKAmBgfK+/3wsBjSDiy9hN2NulT+Wz/ExyiCoXvU37laFmh +1IEl2dPQem5HXOS/trYzCRCbPlcJOSCQZyLyxxFWm0IiXDzfjpUreCEIdbS8n4pvVAyD7Ra9GcRO +/pFFYIs7CYb7rSutqUgEJzlhSZKxC5ejXbZxMizbF8trl9qwhZyCcbC+U9fLvKLF/sblD+9ca4Uc +LcdUXtkbg8KjA2G5LND4kqiwqZujmQLJrqJhoE366aG4F7ev+iABJYz7Ts0RrbEdETr9b9w9Bl/O +kWZqQ9RRJgEpRLgvWDtT2LjVc2CpA2o6YzEGm7t3tMphXNgm7txZaFTQ1AcVquarLqhSc6ORACYJ +D5Ki/fn2++Tbm0GHhBXtzSuthV2UNZ1QFA1hgRcDKhJs6UlfVM1HfiEq+e8amYew+kx3WCy5Oxyo +qCg7ZLiayNiOHkSneZdShVgwg3RC5QirTSGhWtcXH14yMakJDCkm3Nkfb7YQA2MxBovNiYo+1NUu +kVtYPythjwI1tRdar/wSUP5BgButS3CnNSvtG5p2xeYoQqVvjVD2LweWsEjY9QUhqICdzFeDyAj3 +lwRiwESUaOUI8W7wQTWR53Z0S6FpRlK7eI2QSyK0Ieoow0WKM6vyHMSXmMZidJbsAlV8qSGUtZEL +VsZnzoIVqCctXwTt+VCXsCBgvKsNqHWtQK2HAmpaLaEoR8wCtpQyYxw54Y+B50RATW237k6E6p+K +NCExmfjcOIo2jxClcoTVpsArLCmJOEk5U2juakhJEbiw0bE1WIow7iybd76sgdaPRHxTNk2CZqmi +4v+brz+OSllMWYb2RKBhUOcfuP98osGpWWQi2scP9RfTcyLvs3rayvkEbzt+l9QifQoboo6y7298 +AErUMYq8HoRHI22FB4DIBWrV+HR2w+6xrESvxtPT7q9NoKa3QvNgkG7FFFHLliDhCKA9EWgY1HwF +sFrKL91TdsEnP6HIQyn1nhN5Qq12p+PASRGEJ9DZs3/60BAbR1htCumnvHX1+EL3jtkAJnyQwvI1 +Ch0wc52UKZ3lrCnObJO3E99QYC61mu5fXOaHr9/0bHxp96u3hw2P/5zXCLderoAnAg2DmlnkBHnI +cV+yd4Xw3mNS+o7sxXMiHWp9O2Kv/uXO0hHYhTHoxTZEHWWCA+8EFVwRGdvCWW23qDwknDj9HVZy +1WBJUfoXWgYFs6AoiA4LLa8p8kr6rBjwkuRLRGahMRlfBzQdKb8YGFr5TMvMv9/6ZfDwo1wLzfMg +IjlRpuCfPJydEafauhEKFz6C1ETGdlR8iMScqSqmQTT4E0aP70RzT9UNxUZneacB4JmabsoXmue6 +Yq9caHuoNODLmq5YKuE/TjAjb+Z9xYMuX2j7oMH17Xjp65jBNoWXMV0zKwI2RB1l1giu0EsEHGG1 +KbyM6ZpZEbAh6spcBFwEXARcBB4aBH4HWDJl2AplbmRzdHJlYW0KZW5kb2JqCjcgMCBvYmoKPDwg +L04gMyAvQWx0ZXJuYXRlIC9EZXZpY2VSR0IgL0xlbmd0aCAzNDMgL0ZpbHRlciAvRmxhdGVEZWNv +ZGUgPj4Kc3RyZWFtCngBdZA7SwNBFIXPxsiCpkghVim2sRBWiZsgdpKoiJBiiYqPbrKJiZDHsFkR +G7HzD4iVYCdprWKhIP4CQVCxshC1FwI+wnomUTaF3uHOfHPm3N3LBUIRIWU5DKBS9dzsfNpYXVs3 +9BeEEIWOJEzh1GXKtjO04PdUHETrFpq63Yypb8VOP7Kv+eP9+7Q5rYvGYuD7kwbyhbrDly+m5UjX +A7Q42d72pOI98pDLpsgHiotdbijOdfm841nKztBzTY46JZEnP5LNXI9e7OFKeUv9V4XqPlKoLqte +h5kxzGIOGS4DNixOYQJT1PBPTbJTM4MaJHbgYhNFlOCxOkVFoowCeQFVOBiHSbYQZybUrFnLCGYY +aLUIMPnMx6tAE0/A2e7PSAxVCYzQEz0CLt6lcEVX4661wvWNhNW5a4NNoP/Q999WAH0UaN/5/mfT +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-- diff --git a/tests/messages/multiple_nested_attachments.eml b/tests/messages/multiple_nested_attachments.eml new file mode 100644 index 00000000..ad48d47b --- /dev/null +++ b/tests/messages/multiple_nested_attachments.eml @@ -0,0 +1,84 @@ +Date: Mon, 15 Jan 2018 10:54:09 +0100 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 + Thunderbird/52.5.0 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------85793CE1578A77318D5A90A6" +Content-Language: en-US + +This is a multi-part message in MIME format. +--------------85793CE1578A77318D5A90A6 +Content-Type: multipart/alternative; + boundary="------------32D598A7FC3F8A8E228998B0" + + +--------------32D598A7FC3F8A8E228998B0 +Content-Type: text/plain; charset=utf-8; format=flowed +Content-Transfer-Encoding: 7bit + + +------------------------------------------------------------------------ + + + + +--------------32D598A7FC3F8A8E228998B0 +Content-Type: multipart/related; + boundary="------------261DC39B47CFCDB73BCE3C18" + + +--------------261DC39B47CFCDB73BCE3C18 +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: 8bit + + + + + + + +


+

+
+ + + Â +
+ + + + + + + +

+

+
+
+ + + +--------------261DC39B47CFCDB73BCE3C18 +Content-Type: image/png; + name="mleokdgdlgkkecep.png" +Content-Transfer-Encoding: base64 +Content-ID: +Content-Disposition: inline; + filename="mleokdgdlgkkecep.png" + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAACklE +QVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg== +--------------261DC39B47CFCDB73BCE3C18-- + +--------------32D598A7FC3F8A8E228998B0-- + +--------------85793CE1578A77318D5A90A6 +Content-Type: image/png; + name="FF4D00-1.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="FF4D00-1.png" + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAACklE +QVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg== +--------------85793CE1578A77318D5A90A6-- \ No newline at end of file diff --git a/tests/messages/nestes_embedded_with_attachment.eml b/tests/messages/nestes_embedded_with_attachment.eml new file mode 100644 index 00000000..44498103 --- /dev/null +++ b/tests/messages/nestes_embedded_with_attachment.eml @@ -0,0 +1,145 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_000_000" + +This is a multi-part message in MIME format. + +------=_NextPart_000_000_000 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_111_000" + + +------=_NextPart_000_111_000 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Dear Sarah + +------=_NextPart_000_111_000 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + +
Dear Sarah,
+ + +------=_NextPart_000_111_000-- + +------=_NextPart_000_000_000 +Content-Type: message/rfc822; + name="first.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="first.eml" + +From: from@there.com +To: to@here.com +Subject: FIRST +Date: Sat, 28 Apr 2018 14:37:16 -0400 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_222_000" + +This is a multi-part message in MIME format. + +------=_NextPart_000_222_000 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_222_111" + + +------=_NextPart_000_222_111 +Content-Type: text/plain; + charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +Please respond directly to this email to update your RMA + + +2018-04-17T11:04:03-04:00 +------=_NextPart_000_222_111 +Content-Type: text/html; + charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + + + +
Please respond directly to this = +email to=20 +update your RMA
+ +------=_NextPart_000_222_111-- + +------=_NextPart_000_222_000 +Content-Type: image/png; + name="chrome.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="chrome.png" + +iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0Xk +NTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8 +kkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwalju +ZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklH +pEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZF +o0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzv +vhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGd +yKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWf +wtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+u +Si6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm +5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUA +LabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxh +eD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T +5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7 +s7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3 +a/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU +9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RH +iTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZN +J3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSu +QmCC + +------=_NextPart_000_222_000-- + +------=_NextPart_000_000_000 +Content-Type: message/rfc822; + name="second.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="second.eml" + +From: from@there.com +To: to@here.com +Subject: SECOND +Date: Sat, 28 Apr 2018 13:37:30 -0400 +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_333_000" + +This is a multi-part message in MIME format. + +------=_NextPart_000_333_000 +Content-Type: text/plain; + charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +T whom it may concern: +------=_NextPart_000_333_000 +Content-Type: text/html; + charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + + + +
T whom it may concern:
+ + +------=_NextPart_000_333_000-- + +------=_NextPart_000_000_000-- + diff --git a/tests/messages/null_content_charset.eml b/tests/messages/null_content_charset.eml new file mode 100644 index 00000000..0f19eb44 --- /dev/null +++ b/tests/messages/null_content_charset.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: to@here.com + +Hi! \ No newline at end of file diff --git a/tests/messages/pec.eml b/tests/messages/pec.eml new file mode 100644 index 00000000..15dfacca --- /dev/null +++ b/tests/messages/pec.eml @@ -0,0 +1,65 @@ +To: test@example.com +From: test@example.com +Subject: Certified +Date: Mon, 2 Oct 2017 12:13:43 +0200 +Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha1"; boundary="----258A05BDE519DE69AAE8D59024C32F5E" + +This is an S/MIME signed message + +------258A05BDE519DE69AAE8D59024C32F5E +Content-Type: multipart/mixed; boundary="----------=_1506939223-24530-42" +Content-Transfer-Encoding: binary + +------------=_1506939223-24530-42 +Content-Type: multipart/alternative; + boundary="----------=_1506939223-24530-43" +Content-Transfer-Encoding: binary + +------------=_1506939223-24530-43 +Content-Type: text/plain; charset="iso-8859-1" +Content-Disposition: inline +Content-Transfer-Encoding: quoted-printable + +Signed + +------------=_1506939223-24530-43 +Content-Type: text/html; charset="iso-8859-1" +Content-Disposition: inline +Content-Transfer-Encoding: quoted-printable + +Signed + +------------=_1506939223-24530-43-- + +------------=_1506939223-24530-42 +Content-Type: application/xml; name="data.xml" +Content-Disposition: inline; filename="data.xml" +Content-Transfer-Encoding: base64 + +PHhtbC8+ + +------------=_1506939223-24530-42 +Content-Type: message/rfc822; name="postacert.eml" +Content-Disposition: inline; filename="postacert.eml" +Content-Transfer-Encoding: 7bit + +To: test@example.com +From: test@example.com +Subject: test-subject +Date: Mon, 2 Oct 2017 12:13:50 +0200 +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +test-content + +------------=_1506939223-24530-42-- + +------258A05BDE519DE69AAE8D59024C32F5E +Content-Type: application/x-pkcs7-signature; name="smime.p7s" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7s" + +MQ== + +------258A05BDE519DE69AAE8D59024C32F5E-- + diff --git a/tests/messages/plain_only.eml b/tests/messages/plain_only.eml new file mode 100644 index 00000000..bbf9f3b3 --- /dev/null +++ b/tests/messages/plain_only.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file diff --git a/tests/messages/plain_text_attachment.eml b/tests/messages/plain_text_attachment.eml new file mode 100644 index 00000000..6ec6a6c5 --- /dev/null +++ b/tests/messages/plain_text_attachment.eml @@ -0,0 +1,21 @@ +To: to@here.com +From: from@there.com +Subject: Plain text attachment +Date: Tue, 21 Aug 2018 09:05:14 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------B832AF745285AEEC6D5AEE42" + +This is a multi-part message in MIME format. +--------------B832AF745285AEEC6D5AEE42 +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +Test +--------------B832AF745285AEEC6D5AEE42 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="a.txt" + +SGkh +--------------B832AF745285AEEC6D5AEE42-- diff --git a/tests/messages/references.eml b/tests/messages/references.eml new file mode 100644 index 00000000..478383cf --- /dev/null +++ b/tests/messages/references.eml @@ -0,0 +1,11 @@ +Message-ID: <123@example.com> +From: no_host +Cc: "This one: is \"right\"" , No-address +In-Reply-To: +References: <231d9ac57aec7d8c1a0eacfeab8af6f3@example.com> <08F04024-A5B3-4FDE-BF2C-6710DE97D8D9@example.com> +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi +How are you? diff --git a/tests/messages/simple_multipart.eml b/tests/messages/simple_multipart.eml new file mode 100644 index 00000000..60a6521c --- /dev/null +++ b/tests/messages/simple_multipart.eml @@ -0,0 +1,23 @@ +From: from@there.com +To: to@here.com +Date: Wed, 27 Sep 2017 12:48:51 +0200 +Subject: test +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0081_01D32C91.033E7020" + +This is a multipart message in MIME format. + +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit + +MyPlain +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/html; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +MyHtml +------=_NextPart_000_0081_01D32C91.033E7020-- + diff --git a/tests/messages/structured_with_attachment.eml b/tests/messages/structured_with_attachment.eml new file mode 100644 index 00000000..f795ec3a --- /dev/null +++ b/tests/messages/structured_with_attachment.eml @@ -0,0 +1,24 @@ +From: from@there.com +To: to@here.com +Subject: Test +Date: Fri, 29 Sep 2017 10:55:23 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------5B1F217006A67C28E756A62E" + +This is a multi-part message in MIME format. +--------------5B1F217006A67C28E756A62E +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +Test + +--------------5B1F217006A67C28E756A62E +Content-Type: text/plain; charset=UTF-8; + name="MyFile.txt" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="MyFile.txt" + +TXlGaWxlQ29udGVudA== +--------------5B1F217006A67C28E756A62E-- diff --git a/tests/messages/thread_my_topic.eml b/tests/messages/thread_my_topic.eml new file mode 100644 index 00000000..9a196351 --- /dev/null +++ b/tests/messages/thread_my_topic.eml @@ -0,0 +1,10 @@ +Message-ID: <123@example.com> +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/tests/messages/thread_re_my_topic.eml b/tests/messages/thread_re_my_topic.eml new file mode 100644 index 00000000..db4fdf41 --- /dev/null +++ b/tests/messages/thread_re_my_topic.eml @@ -0,0 +1,11 @@ +Message-ID: <456@example.com> +In-Reply-To: <123@example.com> +From: from@there.com +To: to@here.com +Subject: Re: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/tests/messages/thread_unrelated.eml b/tests/messages/thread_unrelated.eml new file mode 100644 index 00000000..e47faa81 --- /dev/null +++ b/tests/messages/thread_unrelated.eml @@ -0,0 +1,10 @@ +Message-ID: <999@example.com> +From: from@there.com +To: to@here.com +Subject: Wut +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/tests/messages/undefined_charset_header.eml b/tests/messages/undefined_charset_header.eml new file mode 100644 index 00000000..117a33e4 --- /dev/null +++ b/tests/messages/undefined_charset_header.eml @@ -0,0 +1,20 @@ +X-Real-To: +X-Stored-In: BlaBla +Return-Path: +Received: from + by bla.bla (CommuniGate Pro RULE 6.1.13) + with RULE id 14057804; Mon, 27 Feb 2017 13:21:44 +0930 +X-Autogenerated: Mirror +Resent-From: +Resent-Date: Mon, 27 Feb 2017 13:21:44 +0930 +Message-Id: <201702270351.BGF77614@bla.bla> +From: =?X-IAS-German?B?bXlHb3Y=?= +To: sales@bla.bla +Subject: =?X-IAS-German?B?U3VibWl0IHlvdXIgdGF4IHJlZnVuZCB8IEF1c3RyYWxpYW4gVGF4YXRpb24gT2ZmaWNlLg==?= +Date: 27 Feb 2017 04:51:29 +0100 +MIME-Version: 1.0 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +) \ No newline at end of file diff --git a/tests/messages/undisclosed_recipients_minus.eml b/tests/messages/undisclosed_recipients_minus.eml new file mode 100644 index 00000000..73f7b83f --- /dev/null +++ b/tests/messages/undisclosed_recipients_minus.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: undisclosed-recipients:; + +Hi! diff --git a/tests/messages/undisclosed_recipients_space.eml b/tests/messages/undisclosed_recipients_space.eml new file mode 100644 index 00000000..6c06cbd4 --- /dev/null +++ b/tests/messages/undisclosed_recipients_space.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: Undisclosed recipients:; + +Hi! diff --git a/tests/messages/unknown_encoding.eml b/tests/messages/unknown_encoding.eml new file mode 100644 index 00000000..271bc4e2 --- /dev/null +++ b/tests/messages/unknown_encoding.eml @@ -0,0 +1,24 @@ +From: from@there.com +To: to@here.com +Date: Wed, 27 Sep 2017 12:48:51 +0200 +Subject: test +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0081_01D32C91.033E7020" + +This is a multipart message in MIME format. + +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/plain; + charset= +Content-Transfer-Encoding: foobar + +MyPlain + +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/html; + charset="" +Content-Transfer-Encoding: quoted-printable + +MyHtml +------=_NextPart_000_0081_01D32C91.033E7020-- + diff --git a/tests/messages/without_charset_plain_only.eml b/tests/messages/without_charset_plain_only.eml new file mode 100644 index 00000000..bd4fe5de --- /dev/null +++ b/tests/messages/without_charset_plain_only.eml @@ -0,0 +1,8 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; charset= +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file diff --git a/tests/messages/without_charset_simple_multipart.eml b/tests/messages/without_charset_simple_multipart.eml new file mode 100644 index 00000000..d1a092d2 --- /dev/null +++ b/tests/messages/without_charset_simple_multipart.eml @@ -0,0 +1,23 @@ +From: from@there.com +To: to@here.com +Date: Wed, 27 Sep 2017 12:48:51 +0200 +Subject: test +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0081_01D32C91.033E7020" + +This is a multipart message in MIME format. + +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/plain; + charset= +Content-Transfer-Encoding: 7bit + +MyPlain +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/html; + charset="" +Content-Transfer-Encoding: quoted-printable + +MyHtml +------=_NextPart_000_0081_01D32C91.033E7020-- + From a9e5975c34ebadca0ea17e3dbbb81db737b9019c Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:49:51 +0100 Subject: [PATCH 077/203] Thread test added --- tests/live/MessageTest.php | 55 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/live/MessageTest.php b/tests/live/MessageTest.php index d7850917..31b15579 100644 --- a/tests/live/MessageTest.php +++ b/tests/live/MessageTest.php @@ -19,6 +19,7 @@ use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\EventNotFoundException; use Webklex\PHPIMAP\Exceptions\FolderFetchingException; +use Webklex\PHPIMAP\Exceptions\GetMessagesFailedException; use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; use Webklex\PHPIMAP\Exceptions\ImapServerErrorException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; @@ -102,6 +103,60 @@ public function testConvertEncoding(): void { self::assertTrue($message->delete()); } + /** + * Test Message::thread() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + * @throws GetMessagesFailedException + */ + public function testThread(): void { + $client = $this->getClient(); + + $delimiter = $this->getManager()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'thread']); + + $folder = $client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = $client->createFolder($folder_path, false); + + $message1 = $this->appendMessageTemplate($folder, "thread_my_topic.eml"); + $message2 = $this->appendMessageTemplate($folder, "thread_re_my_topic.eml"); + $message3 = $this->appendMessageTemplate($folder, "thread_unrelated.eml"); + + $thread = $message1->thread($folder); + self::assertCount(2, $thread); + + $thread = $message2->thread($folder); + self::assertCount(2, $thread); + + $thread = $message3->thread($folder); + self::assertCount(1, $thread); + + // Cleanup + self::assertTrue($message1->delete()); + self::assertTrue($message2->delete()); + self::assertTrue($message3->delete()); + $client->expunge(); + + self::assertTrue($this->deleteFolder($folder)); + } + /** * Test Message::hasAttachments() * From 6420569249bf40a122df5eff1a11d777560c91a0 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 01:50:39 +0100 Subject: [PATCH 078/203] Changelog updated --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82a5ab54..1af2da05 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,11 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Prevent message body parsing from adding empty lines - Don't parse regular inline message parts without name or filename as attachment - `Message::hasTextBody()` and `Message::hasHtmlBody()` should return `false` if the body is empty +- Imap-Protocol "empty response" detection extended to catch an empty response caused by a broken resource stream +- iconv_mime_decode() is now used with `ICONV_MIME_DECODE_CONTINUE_ON_ERROR` to prevent the decoding from failing +- Date decoding rules extended to support more date formats +- Unset the currently active folder if it gets deleted (prevent infinite loop) +- Attachment name and filename parsing fixed and improved to support more formats ### Added - Extended UTF-7 support added (RFC2060) #383 @@ -26,7 +31,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Message::getConfig()` method added to get the current message configuration - `Folder::select()` method added to select a folder - `Message::getAvailableFlags()` method added to get all available flags -- Live mailbox tests added +- Live mailbox and fixture tests added - `Attribute::map()` method added to map all attribute values - `Header::has()` method added to check if a header attribute / value exist - All part attributes are now accessible via linked attribute From 77026c23e0a17117b9fbde74d937383ab428b63a Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 04:32:12 +0100 Subject: [PATCH 079/203] Check if the next uid is available before fetching it #381 --- CHANGELOG.md | 1 + src/Message.php | 6 ++++ tests/MessageTest.php | 66 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1af2da05..68a9e83e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Date decoding rules extended to support more date formats - Unset the currently active folder if it gets deleted (prevent infinite loop) - Attachment name and filename parsing fixed and improved to support more formats +- Check if the next uid is available (after copying or moving a message) before fetching it #381 ### Added - Extended UTF-7 support added (RFC2060) #383 diff --git a/src/Message.php b/src/Message.php index 5cd9ff7a..47562cb7 100755 --- a/src/Message.php +++ b/src/Message.php @@ -1068,6 +1068,9 @@ public function copy(string $folder_path, bool $expunge = false): ?Message { if (isset($status["uidnext"])) { $next_uid = $status["uidnext"]; + if ((int)$next_uid <= 0) { + return null; + } /** @var Folder $folder */ $folder = $this->client->getFolderByPath($folder_path); @@ -1107,6 +1110,9 @@ public function move(string $folder_path, bool $expunge = false): ?Message { if (isset($status["uidnext"])) { $next_uid = $status["uidnext"]; + if ((int)$next_uid <= 0) { + return null; + } /** @var Folder $folder */ $folder = $this->client->getFolderByPath($folder_path); diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 88abd40c..3f854918 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -24,6 +24,7 @@ use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; use Webklex\PHPIMAP\Exceptions\MessageFlagException; use Webklex\PHPIMAP\Exceptions\MessageNotFoundException; +use Webklex\PHPIMAP\Exceptions\MessageSizeFetchingException; use Webklex\PHPIMAP\Exceptions\ResponseException; use Webklex\PHPIMAP\IMAP; use Webklex\PHPIMAP\Message; @@ -80,9 +81,10 @@ public function setUp(): void { * @throws MessageContentFetchingException * @throws MessageFlagException * @throws MessageNotFoundException + * @throws MessageSizeFetchingException * @throws ReflectionException - * @throws RuntimeException * @throws ResponseException + * @throws RuntimeException */ public function testMessage(): void { $this->createNewProtocolMockup(); @@ -123,16 +125,48 @@ public function testMessage(): void { } /** - * @throws RuntimeException - * @throws MessageContentFetchingException - * @throws ImapServerErrorException - * @throws ConnectionFailedException + * Test getMessageNumber + * + * @return void * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws MessageNotFoundException * @throws ResponseException + * @throws RuntimeException + */ + public function testGetMessageNumber(): void { + $this->createNewProtocolMockup(); + $this->protocol->expects($this->any())->method('getMessageNumber')->willReturn(Response::empty()->setResult("")); + + self::assertNotEmpty($this->client->openFolder("INBOX")); + + try { + $this->client->getConnection()->getMessageNumber(21)->validatedData(); + $this->fail("Message number should not exist"); + } catch (ResponseException $e) { + self::assertTrue(true); + } + + } + + /** + * Test loadMessageFromFile + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageNotFoundException * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException + * @throws MessageSizeFetchingException */ public function testLoadMessageFromFile(): void { $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", "1366671050@github.com.eml"]); @@ -189,6 +223,21 @@ public function testLoadMessageFromFile(): void { self::assertSame("text/plain", $attachment->getMimeType()); } + /** + * Test issue #348 + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException + */ public function testIssue348() { $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", "issue-348.eml"]); $message = Message::fromFile($filename); @@ -210,7 +259,12 @@ public function testIssue348() { self::assertSame("application/pdf", $attachment->getMimeType()); } - protected function createNewProtocolMockup() { + /** + * Create a new protocol mockup + * + * @return void + */ + protected function createNewProtocolMockup(): void { $this->protocol = $this->createMock(ImapProtocol::class); $this->protocol->expects($this->any())->method('createStream')->willReturn(true); From 79e370d8f3bca3c9a16d381e7e6db806cdbba32e Mon Sep 17 00:00:00 2001 From: hhniao <12420958+hhniao@users.noreply.github.com> Date: Sat, 11 Mar 2023 11:37:14 +0800 Subject: [PATCH 080/203] Update PaginatedCollection.php (#385) fix bug for high version php Typed property Webklex\PHPIMAP\Support\PaginatedCollection::$total must not be accessed before initialization --- src/Support/PaginatedCollection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/PaginatedCollection.php b/src/Support/PaginatedCollection.php index 27e7fd0d..3b3470e9 100644 --- a/src/Support/PaginatedCollection.php +++ b/src/Support/PaginatedCollection.php @@ -28,7 +28,7 @@ class PaginatedCollection extends Collection { * * @var int $total */ - protected int $total; + protected int $total = 0; /** * Paginate the current collection. From 70b9132c5c4717aca00d51afec6f7f1e2cd6e9bd Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 04:37:56 +0100 Subject: [PATCH 081/203] Changelog updated --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68a9e83e..c341729d 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Unset the currently active folder if it gets deleted (prevent infinite loop) - Attachment name and filename parsing fixed and improved to support more formats - Check if the next uid is available (after copying or moving a message) before fetching it #381 +- Default pagination $total attribute value set to 0 #385 (thanks @hhniao) ### Added - Extended UTF-7 support added (RFC2060) #383 From 7e792494a17bfa616162c34d605a6a4b71a59765 Mon Sep 17 00:00:00 2001 From: Adrien Brault Date: Sat, 11 Mar 2023 04:39:17 +0100 Subject: [PATCH 082/203] add missing @implements to MessageCollection (#359) --- src/Support/MessageCollection.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Support/MessageCollection.php b/src/Support/MessageCollection.php index 6d1249fa..1ca97a70 100644 --- a/src/Support/MessageCollection.php +++ b/src/Support/MessageCollection.php @@ -12,11 +12,15 @@ namespace Webklex\PHPIMAP\Support; +use Illuminate\Support\Collection; +use Webklex\PHPIMAP\Message; + /** * Class MessageCollection * * @package Webklex\PHPIMAP\Support + * @implements Collection */ class MessageCollection extends PaginatedCollection { -} \ No newline at end of file +} From 7595508dd0df657b3e37876fe56208b0be53d1b2 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 04:42:44 +0100 Subject: [PATCH 083/203] docs updated --- src/Support/AttachmentCollection.php | 4 ++++ src/Support/FlagCollection.php | 3 +++ src/Support/FolderCollection.php | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/src/Support/AttachmentCollection.php b/src/Support/AttachmentCollection.php index 8b3f9c32..9d2af20d 100644 --- a/src/Support/AttachmentCollection.php +++ b/src/Support/AttachmentCollection.php @@ -12,10 +12,14 @@ namespace Webklex\PHPIMAP\Support; +use Illuminate\Support\Collection; +use Webklex\PHPIMAP\Attachment; + /** * Class AttachmentCollection * * @package Webklex\PHPIMAP\Support + * @implements Collection */ class AttachmentCollection extends PaginatedCollection { diff --git a/src/Support/FlagCollection.php b/src/Support/FlagCollection.php index 929594ee..b8bf352c 100644 --- a/src/Support/FlagCollection.php +++ b/src/Support/FlagCollection.php @@ -12,10 +12,13 @@ namespace Webklex\PHPIMAP\Support; +use Illuminate\Support\Collection; + /** * Class FlagCollection * * @package Webklex\PHPIMAP\Support + * @implements Collection */ class FlagCollection extends PaginatedCollection { diff --git a/src/Support/FolderCollection.php b/src/Support/FolderCollection.php index f74ec4d4..212ddb0a 100644 --- a/src/Support/FolderCollection.php +++ b/src/Support/FolderCollection.php @@ -12,10 +12,14 @@ namespace Webklex\PHPIMAP\Support; +use Illuminate\Support\Collection; +use Webklex\PHPIMAP\Folder; + /** * Class FolderCollection * * @package Webklex\PHPIMAP\Support + * @implements Collection */ class FolderCollection extends PaginatedCollection { From 3ca7a9d2a0b780810d958fed288cbbd503118894 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 11 Mar 2023 05:00:21 +0100 Subject: [PATCH 084/203] Use attachment ID as fallback filename for saving an attachment --- CHANGELOG.md | 11 ++++++----- src/Attachment.php | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c341729d..6af555a1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,20 +11,21 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Partial fix for #362 (allow overview response to be empty) - `Message::setConfig()` config parameter type set to array - Reset the protocol uid cache if the session gets expunged -- Set the "seen" flag only if the flag isn't set and the fetch option isn't IMAP::FT_PEEK +- Set the "seen" flag only if the flag isn't set and the fetch option isn't `IMAP::FT_PEEK` - `Message::is()` date comparison fixed - `Message::$client` could not be set to null -- in_reply_to and references parsing fixed -- Prevent message body parsing from adding empty lines +- `in_reply_to` and `references` parsing fixed +- Prevent message body parser from injecting empty lines - Don't parse regular inline message parts without name or filename as attachment - `Message::hasTextBody()` and `Message::hasHtmlBody()` should return `false` if the body is empty - Imap-Protocol "empty response" detection extended to catch an empty response caused by a broken resource stream -- iconv_mime_decode() is now used with `ICONV_MIME_DECODE_CONTINUE_ON_ERROR` to prevent the decoding from failing +- `iconv_mime_decode()` is now used with `ICONV_MIME_DECODE_CONTINUE_ON_ERROR` to prevent the decoding from failing - Date decoding rules extended to support more date formats - Unset the currently active folder if it gets deleted (prevent infinite loop) - Attachment name and filename parsing fixed and improved to support more formats - Check if the next uid is available (after copying or moving a message) before fetching it #381 -- Default pagination $total attribute value set to 0 #385 (thanks @hhniao) +- Default pagination `$total` attribute value set to 0 #385 (thanks @hhniao) +- Use attachment ID as fallback filename for saving an attachment ### Added - Extended UTF-7 support added (RFC2060) #383 diff --git a/src/Attachment.php b/src/Attachment.php index 2c9c23cc..963f30eb 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -249,9 +249,9 @@ protected function fetch(): void { * @return boolean */ public function save(string $path, string $filename = null): bool { - $filename = $filename ?: $this->getName(); + $filename = $filename ?? $this->filename ?? $this->name ?? $this->id; - return file_put_contents($path.$filename, $this->getContent()) !== false; + return file_put_contents($path.DIRECTORY_SEPARATOR.$filename, $this->getContent()) !== false; } /** From 69dc33380a75f38ebb48775adf47643c4863fb7f Mon Sep 17 00:00:00 2001 From: webklex Date: Mon, 13 Mar 2023 15:23:29 +0100 Subject: [PATCH 085/203] Additional multipart message test added --- tests/fixtures/MultipartWithoutBodyTest.php | 63 +++++++++++ tests/messages/multipart_without_body.eml | 110 ++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 tests/fixtures/MultipartWithoutBodyTest.php create mode 100644 tests/messages/multipart_without_body.eml diff --git a/tests/fixtures/MultipartWithoutBodyTest.php b/tests/fixtures/MultipartWithoutBodyTest.php new file mode 100644 index 00000000..9989b3e7 --- /dev/null +++ b/tests/fixtures/MultipartWithoutBodyTest.php @@ -0,0 +1,63 @@ +getFixture("multipart_without_body.eml"); + + self::assertEquals("This mail will not contain a body", $message->subject); + self::assertEquals("This mail will not contain a body", $message->getTextBody()); + self::assertEquals("d76dfb1ff3231e3efe1675c971ce73f722b906cc049d328db0d255f8d3f65568", hash("sha256", $message->getHTMLBody())); + self::assertEquals("2023-03-11 08:24:31", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("Foo Bülow Bar ", $message->from); + self::assertEquals("some one ", $message->to); + self::assertEquals([ + 0 => 'from AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS; Sat, 11 Mar 2023 08:24:33 +0000', + 1 => 'from omef0ahNgeoJu.eurprd02.prod.outlook.com (2603:10a6:10:33c::12) by AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6178.19; Sat, 11 Mar 2023 08:24:31 +0000', + 2 => 'from omef0ahNgeoJu.eurprd02.prod.outlook.com ([fe80::38c0:9c40:7fc6:93a7]) by omef0ahNgeoJu.eurprd02.prod.outlook.com ([fe80::38c0:9c40:7fc6:93a7%7]) with mapi id 15.20.6178.019; Sat, 11 Mar 2023 08:24:31 +0000', + 3 => 'from AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS', + ], $message->received->all()); + self::assertEquals("This mail will not contain a body", $message->thread_topic); + self::assertEquals("AdlT8uVmpHPvImbCRM6E9LODIvAcQA==", $message->thread_index); + self::assertEquals("omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9@omef0ahNgeoJu.eurprd02.prod.outlook.com", $message->message_id); + self::assertEquals("da-DK, en-US", $message->accept_language); + self::assertEquals("en-US", $message->content_language); + self::assertEquals("Internal", $message->x_ms_exchange_organization_authAs); + self::assertEquals("04", $message->x_ms_exchange_organization_authMechanism); + self::assertEquals("omef0ahNgeoJu.eurprd02.prod.outlook.com", $message->x_ms_exchange_organization_authSource); + self::assertEquals("", $message->x_ms_Has_Attach); + self::assertEquals("aa546a02-2b7a-4fb1-7fd4-08db220a09f1", $message->x_ms_exchange_organization_Network_Message_Id); + self::assertEquals("-1", $message->x_ms_exchange_organization_SCL); + self::assertEquals("", $message->x_ms_TNEF_Correlator); + self::assertEquals("0", $message->x_ms_exchange_organization_RecordReviewCfmType); + self::assertEquals("Email", $message->x_ms_publictraffictype); + self::assertEquals("ucf:0;jmr:0;auth:0;dest:I;ENG:(910001)(944506478)(944626604)(920097)(425001)(930097);", $message->X_Microsoft_Antispam_Mailbox_Delivery->first()); + self::assertEquals("0712b5fe22cf6e75fa220501c1a6715a61098983df9e69bad4000c07531c1295", hash("sha256", $message->X_Microsoft_Antispam_Message_Info)); + self::assertEquals("multipart/alternative", $message->Content_Type->last()); + self::assertEquals("1.0", $message->mime_version); + + self::assertCount(0, $message->getAttachments()); + } +} \ No newline at end of file diff --git a/tests/messages/multipart_without_body.eml b/tests/messages/multipart_without_body.eml new file mode 100644 index 00000000..8f86799f --- /dev/null +++ b/tests/messages/multipart_without_body.eml @@ -0,0 +1,110 @@ +Received: from AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) + by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS; Sat, 11 Mar 2023 + 08:24:33 +0000 +Received: from omef0ahNgeoJu.eurprd02.prod.outlook.com (2603:10a6:10:33c::12) + by AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) with + Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6178.19; Sat, 11 Mar + 2023 08:24:31 +0000 +Received: from omef0ahNgeoJu.eurprd02.prod.outlook.com + ([fe80::38c0:9c40:7fc6:93a7]) by omef0ahNgeoJu.eurprd02.prod.outlook.com + ([fe80::38c0:9c40:7fc6:93a7%7]) with mapi id 15.20.6178.019; Sat, 11 Mar 2023 + 08:24:31 +0000 +From: =?iso-8859-1?Q?Foo_B=FClow_Bar?= +To: some one +Subject: This mail will not contain a body +Thread-Topic: This mail will not contain a body +Thread-Index: AdlT8uVmpHPvImbCRM6E9LODIvAcQA== +Date: Sat, 11 Mar 2023 08:24:31 +0000 +Message-ID: + +Accept-Language: da-DK, en-US +Content-Language: en-US +X-MS-Exchange-Organization-AuthAs: Internal +X-MS-Exchange-Organization-AuthMechanism: 04 +X-MS-Exchange-Organization-AuthSource: omef0ahNgeoJu.eurprd02.prod.outlook.com +X-MS-Has-Attach: +X-MS-Exchange-Organization-Network-Message-Id: + aa546a02-2b7a-4fb1-7fd4-08db220a09f1 +X-MS-Exchange-Organization-SCL: -1 +X-MS-TNEF-Correlator: +X-MS-Exchange-Organization-RecordReviewCfmType: 0 +x-ms-publictraffictype: Email +X-Microsoft-Antispam-Mailbox-Delivery: + ucf:0;jmr:0;auth:0;dest:I;ENG:(910001)(944506478)(944626604)(920097)(425001)(930097); +X-Microsoft-Antispam-Message-Info: + PeifKDwBVlQGeDu/c7oB3MKTffBwvlRIg5GJo1AiA4LroGAjwgwlg+oPfLetX9CgtbtKZy4gZnbjLCn3jJnod5ehn3Sv9gQaoH9PWkH/PIj6izaJzwlcEk81/MdprZFrORMwR7TGNqP/7ELAHw8rOH2Dmz9vGCE4cv0EwyYS3ShUhXABj4eJ17GNu1B4o53T2m9CzTgm647FRR5jpvk5xNIjQOwrhonVXMkCf2XKwF21sd/k4XLS/jX08tFJXBNyBALhsB4cG9gx2rxZYDn11SBejGFDw4eaqIQw6fYsmsyZvEoCnBkO+vK9lGIrQhRzGj7nJ41+uHVBcUYThce7P/ORpxl3GgThHyQpXQDV00JbP1aCBzm+4w8TyFiL0aOHXDhU9UKRHg3A01F+oH8IKAIaYazLCoOzbVcijSw0icNjBQsNLWa0FvfRT8y/rIvGkOB3rZpKf7ZR0g7cSlDAB3Ml2AaTbIB4ZL6QMukP/waDIObMZFmlVaAvmJzdTEdhGSLtUBFk4CNJhd6szcwaaPZxROumOGtz0sDke2iap8wRZqpdMWHVYi/trA+IESlF2G8TPzZiXs1lRDvYjNlam5r+1Ay1zlSmKTMnGbfNvsJgHkTlcgswKTlip4jGFPh6INTSDZtx9dQuDi4vbyNQiN1qVxoOPScTXzxUKVZ2PJ+8ipL2dqudLb3R2GSDHL10uQTuoIftA/Wjf67QZ629qQ== +Content-Type: multipart/alternative; + boundary="_000_omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9omef0ahNgeoJueurp_" +MIME-Version: 1.0 + +--_000_omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9omef0ahNgeoJueurp_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +This mail will not contain a body + + + +--_000_omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9omef0ahNgeoJueurp_ +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + + + +
+

This mail will not contain a bo= +dy

+

 

+
+ + + +--_000_omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9omef0ahNgeoJueurp_-- From fa73689dfd3e9e90dac31eba83db67d120f59c5d Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 16 Mar 2023 00:19:22 +0100 Subject: [PATCH 086/203] Query tests added --- tests/live/QueryTest.php | 283 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 tests/live/QueryTest.php diff --git a/tests/live/QueryTest.php b/tests/live/QueryTest.php new file mode 100644 index 00000000..af651ef4 --- /dev/null +++ b/tests/live/QueryTest.php @@ -0,0 +1,283 @@ +getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + self::assertInstanceOf(WhereQuery::class, $folder->query()); + self::assertInstanceOf(WhereQuery::class, $folder->search()); + self::assertInstanceOf(WhereQuery::class, $folder->messages()); + } + + /** + * Try to create a new query instance with a where clause + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + * @throws GetMessagesFailedException + * @throws InvalidWhereQueryCriteriaException + * @throws MessageSearchValidationException + */ + public function testQueryWhere(): void { + $client = $this->getClient(); + + $delimiter = $this->getManager()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'search']); + + $folder = $client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = $client->createFolder($folder_path, false); + + $messages = [ + $this->appendMessageTemplate($folder, '1366671050@github.com.eml'), + $this->appendMessageTemplate($folder, 'attachment_encoded_filename.eml'), + $this->appendMessageTemplate($folder, 'attachment_long_filename.eml'), + $this->appendMessageTemplate($folder, 'attachment_no_disposition.eml'), + $this->appendMessageTemplate($folder, 'bcc.eml'), + $this->appendMessageTemplate($folder, 'boolean_decoded_content.eml'), + $this->appendMessageTemplate($folder, 'email_address.eml'), + $this->appendMessageTemplate($folder, 'embedded_email.eml'), + $this->appendMessageTemplate($folder, 'embedded_email_without_content_disposition.eml'), + $this->appendMessageTemplate($folder, 'embedded_email_without_content_disposition-embedded.eml'), + $this->appendMessageTemplate($folder, 'example_attachment.eml'), + $this->appendMessageTemplate($folder, 'example_bounce.eml'), + $this->appendMessageTemplate($folder, 'four_nested_emails.eml'), + $this->appendMessageTemplate($folder, 'gbk_charset.eml'), + $this->appendMessageTemplate($folder, 'html_only.eml'), + $this->appendMessageTemplate($folder, 'imap_mime_header_decode_returns_false.eml'), + $this->appendMessageTemplate($folder, 'inline_attachment.eml'), + $this->appendMessageTemplate($folder, 'issue-275.eml'), + $this->appendMessageTemplate($folder, 'issue-275-2.eml'), + $this->appendMessageTemplate($folder, 'issue-348.eml'), + $this->appendMessageTemplate($folder, 'ks_c_5601-1987_headers.eml'), + $this->appendMessageTemplate($folder, 'mail_that_is_attachment.eml'), + $this->appendMessageTemplate($folder, 'missing_date.eml'), + $this->appendMessageTemplate($folder, 'missing_from.eml'), + $this->appendMessageTemplate($folder, 'mixed_filename.eml'), + $this->appendMessageTemplate($folder, 'multipart_without_body.eml'), + $this->appendMessageTemplate($folder, 'multiple_html_parts_and_attachments.eml'), + $this->appendMessageTemplate($folder, 'multiple_nested_attachments.eml'), + $this->appendMessageTemplate($folder, 'nestes_embedded_with_attachment.eml'), + $this->appendMessageTemplate($folder, 'null_content_charset.eml'), + $this->appendMessageTemplate($folder, 'pec.eml'), + $this->appendMessageTemplate($folder, 'plain.eml'), + $this->appendMessageTemplate($folder, 'plain_only.eml'), + $this->appendMessageTemplate($folder, 'plain_text_attachment.eml'), + $this->appendMessageTemplate($folder, 'references.eml'), + $this->appendMessageTemplate($folder, 'simple_multipart.eml'), + $this->appendMessageTemplate($folder, 'structured_with_attachment.eml'), + $this->appendMessageTemplate($folder, 'thread_my_topic.eml'), + $this->appendMessageTemplate($folder, 'thread_re_my_topic.eml'), + $this->appendMessageTemplate($folder, 'thread_unrelated.eml'), + $this->appendMessageTemplate($folder, 'undefined_charset_header.eml'), + $this->appendMessageTemplate($folder, 'undisclosed_recipients_minus.eml'), + $this->appendMessageTemplate($folder, 'undisclosed_recipients_space.eml'), + $this->appendMessageTemplate($folder, 'unknown_encoding.eml'), + $this->appendMessageTemplate($folder, 'without_charset_plain_only.eml'), + $this->appendMessageTemplate($folder, 'without_charset_simple_multipart.eml'), + ]; + + $folder->getClient()->expunge(); + + $query = $folder->query()->all(); + self::assertEquals(count($messages), $query->count()); + + $query = $folder->query()->whereSubject("test"); + self::assertEquals(11, $query->count()); + + $query = $folder->query()->whereOn(Carbon::now()); + self::assertEquals(count($messages), $query->count()); + + self::assertTrue($this->deleteFolder($folder)); + } + + /** + * Test query where criteria + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testQueryWhereCriteria(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $this->assertWhereSearchCriteria($folder, 'SUBJECT', 'Test'); + $this->assertWhereSearchCriteria($folder, 'BODY', 'Test'); + $this->assertWhereSearchCriteria($folder, 'TEXT', 'Test'); + $this->assertWhereSearchCriteria($folder, 'KEYWORD', 'Test'); + $this->assertWhereSearchCriteria($folder, 'UNKEYWORD', 'Test'); + $this->assertWhereSearchCriteria($folder, 'FLAGGED', 'Seen'); + $this->assertWhereSearchCriteria($folder, 'UNFLAGGED', 'Seen'); + $this->assertHeaderSearchCriteria($folder, 'Message-ID', 'Seen'); + $this->assertHeaderSearchCriteria($folder, 'In-Reply-To', 'Seen'); + $this->assertWhereSearchCriteria($folder, 'BCC', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'CC', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'FROM', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'TO', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'UID', '1'); + $this->assertWhereSearchCriteria($folder, 'UID', '1,2'); + $this->assertWhereSearchCriteria($folder, 'ALL'); + $this->assertWhereSearchCriteria($folder, 'NEW'); + $this->assertWhereSearchCriteria($folder, 'OLD'); + $this->assertWhereSearchCriteria($folder, 'SEEN'); + $this->assertWhereSearchCriteria($folder, 'UNSEEN'); + $this->assertWhereSearchCriteria($folder, 'RECENT'); + $this->assertWhereSearchCriteria($folder, 'ANSWERED'); + $this->assertWhereSearchCriteria($folder, 'UNANSWERED'); + $this->assertWhereSearchCriteria($folder, 'DELETED'); + $this->assertWhereSearchCriteria($folder, 'UNDELETED'); + $this->assertHeaderSearchCriteria($folder, 'Content-Language','en_US'); + $this->assertWhereSearchCriteria($folder, 'CUSTOM X-Spam-Flag NO'); + $this->assertWhereSearchCriteria($folder, 'CUSTOM X-Spam-Flag YES'); + $this->assertWhereSearchCriteria($folder, 'NOT'); + $this->assertWhereSearchCriteria($folder, 'OR'); + $this->assertWhereSearchCriteria($folder, 'AND'); + $this->assertWhereSearchCriteria($folder, 'BEFORE', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'BEFORE', Carbon::now()->subDays(1), true); + $this->assertWhereSearchCriteria($folder, 'ON', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'ON', Carbon::now()->subDays(1), true); + $this->assertWhereSearchCriteria($folder, 'SINCE', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'SINCE', Carbon::now()->subDays(1), true); + } + + /** + * Assert where search criteria + * @param Folder $folder + * @param string $criteria + * @param string|Carbon|null $value + * @param bool $date + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws ResponseException + * @throws RuntimeException + */ + protected function assertWhereSearchCriteria(Folder $folder, string $criteria, Carbon|string $value = null, bool $date = false): void { + $query = $folder->query()->where($criteria, $value); + self::assertInstanceOf(WhereQuery::class, $query); + + $item = $query->getQuery()->first(); + $criteria = str_replace("CUSTOM ", "", $criteria); + $expected = $value === null ? [$criteria] : [$criteria, $value]; + if ($date === true && $value instanceof Carbon) { + $date_format = ClientManager::get('date_format', 'd M y'); + $expected[1] = $value->format($date_format); + } + + self::assertIsArray($item); + self::assertIsString($item[0]); + if($value !== null) { + self::assertCount(2, $item); + self::assertIsString($item[1]); + }else{ + self::assertCount(1, $item); + } + self::assertSame($expected, $item); + } + + /** + * Assert header search criteria + * @param Folder $folder + * @param string $criteria + * @param mixed|null $value + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws ResponseException + * @throws RuntimeException + */ + protected function assertHeaderSearchCriteria(Folder $folder, string $criteria, mixed $value = null): void { + $query = $folder->query()->whereHeader($criteria, $value); + self::assertInstanceOf(WhereQuery::class, $query); + + $item = $query->getQuery()->first(); + + self::assertIsArray($item); + self::assertIsString($item[0]); + self::assertCount(1, $item); + self::assertSame(['HEADER '.$criteria.' '.$value], $item); + } +} \ No newline at end of file From f5033a5bcd72753bcb6737645b53246393eaafc3 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 16 Mar 2023 00:56:33 +0100 Subject: [PATCH 087/203] Additional value check added to prevent null values --- src/Address.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Address.php b/src/Address.php index ba186d1e..b45c72de 100644 --- a/src/Address.php +++ b/src/Address.php @@ -38,11 +38,11 @@ class Address { * @param object $object */ public function __construct(object $object) { - if (property_exists($object, "personal")){ $this->personal = $object->personal; } - if (property_exists($object, "mailbox")){ $this->mailbox = $object->mailbox; } - if (property_exists($object, "host")){ $this->host = $object->host; } - if (property_exists($object, "mail")){ $this->mail = $object->mail; } - if (property_exists($object, "full")){ $this->full = $object->full; } + if (property_exists($object, "personal")){ $this->personal = $object->personal ?? ''; } + if (property_exists($object, "mailbox")){ $this->mailbox = $object->mailbox ?? ''; } + if (property_exists($object, "host")){ $this->host = $object->host ?? ''; } + if (property_exists($object, "mail")){ $this->mail = $object->mail ?? ''; } + if (property_exists($object, "full")){ $this->full = $object->full ?? ''; } } From 63f26f6087e6f52bb9a5667ec617979c73e95f85 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 16 Mar 2023 00:58:32 +0100 Subject: [PATCH 088/203] Available criteria accessor added --- src/Query/WhereQuery.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index b9903aca..b218e545 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -543,4 +543,13 @@ public function unless(mixed $value, callable $callback, ?callable $default = nu return $this; } + + /** + * Get all available search criteria + * + * @return array|string[] + */ + public function getAvailableCriteria(): array { + return $this->available_criteria; + } } \ No newline at end of file From 88e65a8c9a10e23d6193dd8bac901a88995ff509 Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 16 Mar 2023 00:59:35 +0100 Subject: [PATCH 089/203] Address decoding error detection added #388 --- CHANGELOG.md | 1 + src/Header.php | 7 ++++ tests/fixtures/UndisclosedRecipientsTest.php | 42 ++++++++++++++++++++ tests/messages/undisclosed_recipients.eml | 8 ++++ 4 files changed, 58 insertions(+) create mode 100644 tests/fixtures/UndisclosedRecipientsTest.php create mode 100644 tests/messages/undisclosed_recipients.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af555a1..7507004a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Check if the next uid is available (after copying or moving a message) before fetching it #381 - Default pagination `$total` attribute value set to 0 #385 (thanks @hhniao) - Use attachment ID as fallback filename for saving an attachment +- Address decoding error detection added #388 ### Added - Extended UTF-7 support added (RFC2060) #383 diff --git a/src/Header.php b/src/Header.php index 49aa4397..4bd4390d 100644 --- a/src/Header.php +++ b/src/Header.php @@ -583,6 +583,13 @@ private function parseAddresses($list): array { } } + if ($address->host == ".SYNTAX-ERROR.") { + $address->host = ""; + } + if ($address->mailbox == "UNEXPECTED_DATA_AFTER_ADDRESS") { + $address->mailbox = ""; + } + $address->mail = ($address->mailbox && $address->host) ? $address->mailbox . '@' . $address->host : false; $address->full = ($address->personal) ? $address->personal . ' <' . $address->mail . '>' : $address->mail; diff --git a/tests/fixtures/UndisclosedRecipientsTest.php b/tests/fixtures/UndisclosedRecipientsTest.php new file mode 100644 index 00000000..f5f44164 --- /dev/null +++ b/tests/fixtures/UndisclosedRecipientsTest.php @@ -0,0 +1,42 @@ +getFixture("undisclosed_recipients.eml"); + + self::assertEquals("test", $message->subject); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals([ + "Undisclosed Recipients", + "" + ], $message->to->map(function ($item) { + return $item->mailbox; + })); + } +} \ No newline at end of file diff --git a/tests/messages/undisclosed_recipients.eml b/tests/messages/undisclosed_recipients.eml new file mode 100644 index 00000000..5d33ecff --- /dev/null +++ b/tests/messages/undisclosed_recipients.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: "Undisclosed Recipients" <> + +Hi! From abf080eb745e21df7a3070ca356248dda021d4ad Mon Sep 17 00:00:00 2001 From: webklex Date: Thu, 16 Mar 2023 01:04:11 +0100 Subject: [PATCH 090/203] Release information added --- CHANGELOG.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7507004a..c454a212 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Breaking changes +- NaN + + +## [5.1.0] - 2023-03-16 +### Fixed - IMAP Quota root command fixed - Prevent line-breaks in folder path caused by special chars - Partial fix for #362 (allow overview response to be empty) @@ -41,9 +52,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - All part attributes are now accessible via linked attribute - Restore a message from string `Message::fromString()` -### Breaking changes -- NaN - ## [5.0.1] - 2023-03-01 ### Fixed From db3b997ce5f718ccf460f6ca8975e8067f94d4c7 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Mar 2023 03:49:05 +0100 Subject: [PATCH 091/203] Use all available methods to detect the attachment extension instead of just one --- CHANGELOG.md | 2 +- src/Attachment.php | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c454a212..0f00dbb6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Use all available methods to detect the attachment extension instead of just one ### Added - NaN diff --git a/src/Attachment.php b/src/Attachment.php index 963f30eb..efae50ca 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -302,21 +302,25 @@ public function getMimeType(): ?string { * @return string|null */ public function getExtension(): ?string { + $extension = null; $guesser = "\Symfony\Component\Mime\MimeTypes"; if (class_exists($guesser) !== false) { /** @var Symfony\Component\Mime\MimeTypes $guesser */ $extensions = $guesser::getDefault()->getExtensions($this->getMimeType()); - return $extensions[0] ?? null; + $extension = $extensions[0] ?? null; } - - $deprecated_guesser = "\Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser"; - if (class_exists($deprecated_guesser) !== false){ - /** @var \Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser $deprecated_guesser */ - return $deprecated_guesser::getInstance()->guess($this->getMimeType()); + if ($extension === null) { + $deprecated_guesser = "\Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser"; + if (class_exists($deprecated_guesser) !== false){ + /** @var \Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser $deprecated_guesser */ + $extension = $deprecated_guesser::getInstance()->guess($this->getMimeType()); + } } - - $extensions = explode(".", $this->part->filename ?: $this->part->name); - return end($extensions); + if ($extension === null) { + $extensions = explode(".", $this->filename); + $extension = end($extensions); + } + return $extension; } /** From 75c988c3c7cef118e97108166fb8ff75355d3b4c Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Mar 2023 03:49:23 +0100 Subject: [PATCH 092/203] Extension test added --- tests/fixtures/AttachmentEncodedFilenameTest.php | 1 + tests/fixtures/AttachmentLongFilenameTest.php | 3 +++ tests/fixtures/AttachmentNoDispositionTest.php | 1 + tests/fixtures/BooleanDecodedContentTest.php | 1 + tests/fixtures/EmbeddedEmailTest.php | 1 + .../EmbeddedEmailWithoutContentDispositionEmbeddedTest.php | 2 ++ tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php | 4 ++++ tests/fixtures/ExampleBounceTest.php | 2 ++ tests/fixtures/FourNestedEmailsTest.php | 1 + tests/fixtures/InlineAttachmentTest.php | 1 + tests/fixtures/MailThatIsAttachmentTest.php | 1 + tests/fixtures/MixedFilenameTest.php | 1 + tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php | 2 ++ tests/fixtures/MultipleNestedAttachmentsTest.php | 2 ++ tests/fixtures/NestesEmbeddedWithAttachmentTest.php | 2 ++ tests/fixtures/PecTest.php | 3 +++ tests/fixtures/PlainTextAttachmentTest.php | 1 + tests/fixtures/StructuredWithAttachmentTest.php | 1 + 18 files changed, 30 insertions(+) diff --git a/tests/fixtures/AttachmentEncodedFilenameTest.php b/tests/fixtures/AttachmentEncodedFilenameTest.php index 33d97fde..b5da3597 100644 --- a/tests/fixtures/AttachmentEncodedFilenameTest.php +++ b/tests/fixtures/AttachmentEncodedFilenameTest.php @@ -40,6 +40,7 @@ public function testFixture() : void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->filename); self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); + self::assertEquals('xls', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/vnd.ms-excel", $attachment->content_type); self::assertEquals("a0ef7cfbc05b73dbcb298fe0bc224b41900cdaf60f9904e3fea5ba6c7670013c", hash("sha256", $attachment->content)); diff --git a/tests/fixtures/AttachmentLongFilenameTest.php b/tests/fixtures/AttachmentLongFilenameTest.php index b3c3e903..c9e67d15 100644 --- a/tests/fixtures/AttachmentLongFilenameTest.php +++ b/tests/fixtures/AttachmentLongFilenameTest.php @@ -42,6 +42,7 @@ public function testFixture() : void { self::assertEquals("Buchungsbestätigung- Rechnung-Geschäftsbedingungen-Nr.B123-45 - XXXX xxxxxxxxxxxxxxxxx XxxX, Lüdxxxxxxxx - VM Klaus XXXXXX - xxxxxxxx.pdf", $attachment->name); self::assertEquals("Buchungsbestätigung- Rechnung-Geschäftsbedingungen-Nr.B123-45 - XXXXX xxxxxxxxxxxxxxxxx XxxX, Lüxxxxxxxxxx - VM Klaus XXXXXX - xxxxxxxx.pdf", $attachment->filename); self::assertEquals('text', $attachment->type); + self::assertEquals('pdf', $attachment->getExtension()); self::assertEquals("text/plain", $attachment->content_type); self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); self::assertEquals(4, $attachment->size); @@ -54,6 +55,7 @@ public function testFixture() : void { self::assertEquals('01_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); self::assertEquals("f7b5181985862431bfc443d26e3af2371e20a0afd676eeb9b9595a26d42e0b73", hash("sha256", $attachment->filename)); self::assertEquals('text', $attachment->type); + self::assertEquals('txt', $attachment->getExtension()); self::assertEquals("text/plain", $attachment->content_type); self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); self::assertEquals(4, $attachment->size); @@ -67,6 +69,7 @@ public function testFixture() : void { self::assertEquals('02_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->filename); self::assertEquals('text', $attachment->type); self::assertEquals("text/plain", $attachment->content_type); + self::assertEquals('txt', $attachment->getExtension()); self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); self::assertEquals(4, $attachment->size); self::assertEquals(2, $attachment->part_number); diff --git a/tests/fixtures/AttachmentNoDispositionTest.php b/tests/fixtures/AttachmentNoDispositionTest.php index 5a3fadd8..cb037053 100644 --- a/tests/fixtures/AttachmentNoDispositionTest.php +++ b/tests/fixtures/AttachmentNoDispositionTest.php @@ -41,6 +41,7 @@ public function testFixture() : void { self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->filename); self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); self::assertEquals('text', $attachment->type); + self::assertEquals('xls', $attachment->getExtension()); self::assertEquals("application/vnd.ms-excel", $attachment->content_type); self::assertEquals("a0ef7cfbc05b73dbcb298fe0bc224b41900cdaf60f9904e3fea5ba6c7670013c", hash("sha256", $attachment->content)); self::assertEquals(146, $attachment->size); diff --git a/tests/fixtures/BooleanDecodedContentTest.php b/tests/fixtures/BooleanDecodedContentTest.php index ae9b797f..e49229a4 100644 --- a/tests/fixtures/BooleanDecodedContentTest.php +++ b/tests/fixtures/BooleanDecodedContentTest.php @@ -44,6 +44,7 @@ public function testFixture() : void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("Example Domain.pdf", $attachment->name); self::assertEquals('text', $attachment->type); + self::assertEquals('pdf', $attachment->getExtension()); self::assertEquals("application/pdf", $attachment->content_type); self::assertEquals("1c449aaab4f509012fa5eaa180fd017eb7724ccacabdffc1c6066d3756dcde5c", hash("sha256", $attachment->content)); self::assertEquals(53, $attachment->size); diff --git a/tests/fixtures/EmbeddedEmailTest.php b/tests/fixtures/EmbeddedEmailTest.php index 4d8baf77..fb6e2492 100644 --- a/tests/fixtures/EmbeddedEmailTest.php +++ b/tests/fixtures/EmbeddedEmailTest.php @@ -53,6 +53,7 @@ public function testFixture() : void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("demo.eml", $attachment->name); self::assertEquals('text', $attachment->type); + self::assertEquals('eml', $attachment->getExtension()); self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("a1f965f10a9872e902a82dde039a237e863f522d238a1cb1968fe3396dbcac65", hash("sha256", $attachment->content)); self::assertEquals(893, $attachment->size); diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php index e7629bc3..7d3fb2d0 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php @@ -52,6 +52,7 @@ public function testFixture() : void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("file1.xlsx", $attachment->name); self::assertEquals('text', $attachment->type); + self::assertEquals('xlsx', $attachment->getExtension()); self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); self::assertEquals(40, $attachment->size); @@ -62,6 +63,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("file2.xlsx", $attachment->name); + self::assertEquals('xlsx', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php index c62e743c..0c7e53cb 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -50,6 +50,7 @@ public function testFixture() : void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("file.jpg", $attachment->name); + self::assertEquals('jpg', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("image/jpeg", $attachment->content_type); self::assertEquals("6b7fa434f92a8b80aab02d9bf1a12e49ffcae424e4013a1c4f68b67e3d2bbcd0", hash("sha256", $attachment->content)); @@ -62,6 +63,7 @@ public function testFixture() : void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("", $attachment->name); self::assertEquals('text', $attachment->type); + self::assertEquals('', $attachment->getExtension()); self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("2476c8b91a93c6b2fe1bfff593cb55956c2fe8e7ca6de9ad2dc9d101efe7a867", hash("sha256", $attachment->content)); self::assertEquals(2073, $attachment->size); @@ -72,6 +74,7 @@ public function testFixture() : void { $attachment = $attachments[2]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("file3.xlsx", $attachment->name); + self::assertEquals('xlsx', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); @@ -83,6 +86,7 @@ public function testFixture() : void { $attachment = $attachments[3]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("file4.zip", $attachment->name); + self::assertEquals('zip', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/x-zip-compressed", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); diff --git a/tests/fixtures/ExampleBounceTest.php b/tests/fixtures/ExampleBounceTest.php index d032f316..89810fc0 100644 --- a/tests/fixtures/ExampleBounceTest.php +++ b/tests/fixtures/ExampleBounceTest.php @@ -74,6 +74,7 @@ public function testFixture(): void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("", $attachment->filename); self::assertEquals("", $attachment->name); + self::assertEquals('', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("message/delivery-status", $attachment->content_type); self::assertEquals("85ac09d1d74b2d85853084dc22abcad205a6bfde62d6056e3a933ffe7e82e45c", hash("sha256", $attachment->content)); @@ -86,6 +87,7 @@ public function testFixture(): void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("", $attachment->filename); self::assertEquals("", $attachment->name); + self::assertEquals('', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("7525331f5fab23ea77f595b995336aca7b8dad12db00ada14abebe7fe5b96e10", hash("sha256", $attachment->content)); diff --git a/tests/fixtures/FourNestedEmailsTest.php b/tests/fixtures/FourNestedEmailsTest.php index 5511c05e..e6c37ccd 100644 --- a/tests/fixtures/FourNestedEmailsTest.php +++ b/tests/fixtures/FourNestedEmailsTest.php @@ -43,6 +43,7 @@ public function testFixture() : void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("2-second-email.eml", $attachment->name); self::assertEquals('text', $attachment->type); + self::assertEquals('eml', $attachment->getExtension()); self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("85012e6a26d064a0288ee62618b3192687385adb4a4e27e48a28f738a325ca46", hash("sha256", $attachment->content)); self::assertEquals(1376, $attachment->size); diff --git a/tests/fixtures/InlineAttachmentTest.php b/tests/fixtures/InlineAttachmentTest.php index ae538b37..c20ac8d5 100644 --- a/tests/fixtures/InlineAttachmentTest.php +++ b/tests/fixtures/InlineAttachmentTest.php @@ -47,6 +47,7 @@ public function testFixture() : void { self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("", $attachment->name); self::assertEquals('text', $attachment->type); + self::assertEquals('', $attachment->getExtension()); self::assertEquals("image/png", $attachment->content_type); self::assertEquals("6568c9e9c35a7fa06f236e89f704d8c9b47183a24f2c978dba6c92e2747e3a13", hash("sha256", $attachment->content)); self::assertEquals(1486, $attachment->size); diff --git a/tests/fixtures/MailThatIsAttachmentTest.php b/tests/fixtures/MailThatIsAttachmentTest.php index e28dc938..0f4164c9 100644 --- a/tests/fixtures/MailThatIsAttachmentTest.php +++ b/tests/fixtures/MailThatIsAttachmentTest.php @@ -51,6 +51,7 @@ public function testFixture() : void { $attachment = $message->attachments()->first(); self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("google.com!yyy.cz!1423872000!1423958399.zip", $attachment->name); + self::assertEquals('zip', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/zip", $attachment->content_type); self::assertEquals("c0d4f47b6fde124cea7460c3e509440d1a062705f550b0502b8ba0cbf621c97a", hash("sha256", $attachment->content)); diff --git a/tests/fixtures/MixedFilenameTest.php b/tests/fixtures/MixedFilenameTest.php index fc1ffa88..b2be0e32 100644 --- a/tests/fixtures/MixedFilenameTest.php +++ b/tests/fixtures/MixedFilenameTest.php @@ -49,6 +49,7 @@ public function testFixture() : void { $attachment = $message->attachments()->first(); self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("Price4VladDaKar.xlsx", $attachment->name); + self::assertEquals('xlsx', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/octet-stream", $attachment->content_type); self::assertEquals("b832983842b0ad65db69e4c7096444c540a2393e2d43f70c2c9b8b9fceeedbb1", hash('sha256', $attachment->content)); diff --git a/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php b/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php index c8f72bb8..c08c03e0 100644 --- a/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php +++ b/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php @@ -52,6 +52,7 @@ public function testFixture() : void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("attachment1.pdf", $attachment->name); + self::assertEquals('pdf', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/pdf", $attachment->content_type); self::assertEquals("c162adf19e0f67e26ef0b7f791b33a60b2c23b175560a505dc7f9ec490206e49", hash("sha256", $attachment->content)); @@ -63,6 +64,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("attachment2.pdf", $attachment->name); + self::assertEquals('pdf', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/pdf", $attachment->content_type); self::assertEquals("a337b37e9d3edb172a249639919f0eee3d344db352046d15f8f9887e55855a25", hash("sha256", $attachment->content)); diff --git a/tests/fixtures/MultipleNestedAttachmentsTest.php b/tests/fixtures/MultipleNestedAttachmentsTest.php index e7b9cbdf..543c0e21 100644 --- a/tests/fixtures/MultipleNestedAttachmentsTest.php +++ b/tests/fixtures/MultipleNestedAttachmentsTest.php @@ -45,6 +45,7 @@ public function testFixture() : void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("mleokdgdlgkkecep.png", $attachment->name); + self::assertEquals('png', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("image/png", $attachment->content_type); self::assertEquals("e0e99b0bd6d5ea3ced99add53cc98b6f8eea6eae8ddd773fd06f3489289385fb", hash("sha256", $attachment->content)); @@ -56,6 +57,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("FF4D00-1.png", $attachment->name); + self::assertEquals('png', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("image/png", $attachment->content_type); self::assertEquals("e0e99b0bd6d5ea3ced99add53cc98b6f8eea6eae8ddd773fd06f3489289385fb", hash("sha256", $attachment->content)); diff --git a/tests/fixtures/NestesEmbeddedWithAttachmentTest.php b/tests/fixtures/NestesEmbeddedWithAttachmentTest.php index a1db7378..c5c8c561 100644 --- a/tests/fixtures/NestesEmbeddedWithAttachmentTest.php +++ b/tests/fixtures/NestesEmbeddedWithAttachmentTest.php @@ -45,6 +45,7 @@ public function testFixture() : void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("first.eml", $attachment->name); + self::assertEquals('eml', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("From: from@there.com\r\nTo: to@here.com\r\nSubject: FIRST\r\nDate: Sat, 28 Apr 2018 14:37:16 -0400\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"----=_NextPart_000_222_000\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextPart_000_222_000\r\nContent-Type: multipart/alternative;\r\n boundary=\"----=_NextPart_000_222_111\"\r\n\r\n\r\n------=_NextPart_000_222_111\r\nContent-Type: text/plain;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nPlease respond directly to this email to update your RMA\r\n\r\n\r\n2018-04-17T11:04:03-04:00\r\n------=_NextPart_000_222_111\r\nContent-Type: text/html;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n\r\n\r\n
Please respond directly to this =\r\nemail to=20\r\nupdate your RMA
\r\n\r\n------=_NextPart_000_222_111--\r\n\r\n------=_NextPart_000_222_000\r\nContent-Type: image/png;\r\n name=\"chrome.png\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment;\r\n filename=\"chrome.png\"\r\n\r\niVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0Xk\r\nNTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8\r\nkkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwalju\r\nZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklH\r\npEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZF\r\no0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzv\r\nvhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGd\r\nyKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWf\r\nwtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+u\r\nSi6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm\r\n5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUA\r\nLabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxh\r\neD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T\r\n5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7\r\ns7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3\r\na/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU\r\n9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RH\r\niTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZN\r\nJ3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSu\r\nQmCC\r\n\r\n------=_NextPart_000_222_000--", $attachment->content); @@ -56,6 +57,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("second.eml", $attachment->name); + self::assertEquals('eml', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("From: from@there.com\r\nTo: to@here.com\r\nSubject: SECOND\r\nDate: Sat, 28 Apr 2018 13:37:30 -0400\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative;\r\n boundary=\"----=_NextPart_000_333_000\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextPart_000_333_000\r\nContent-Type: text/plain;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nT whom it may concern:\r\n------=_NextPart_000_333_000\r\nContent-Type: text/html;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n\r\n\r\n
T whom it may concern:
\r\n\r\n\r\n------=_NextPart_000_333_000--", $attachment->content); diff --git a/tests/fixtures/PecTest.php b/tests/fixtures/PecTest.php index 01fe17c5..28ecb27b 100644 --- a/tests/fixtures/PecTest.php +++ b/tests/fixtures/PecTest.php @@ -46,6 +46,7 @@ public function testFixture() : void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("data.xml", $attachment->name); + self::assertEquals('xml', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/xml", $attachment->content_type); self::assertEquals("", $attachment->content); @@ -57,6 +58,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("postacert.eml", $attachment->name); + self::assertEquals('eml', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("To: test@example.com\r\nFrom: test@example.com\r\nSubject: test-subject\r\nDate: Mon, 2 Oct 2017 12:13:50 +0200\r\nContent-Type: text/plain; charset=iso-8859-15; format=flowed\r\nContent-Transfer-Encoding: 7bit\r\n\r\ntest-content", $attachment->content); @@ -68,6 +70,7 @@ public function testFixture() : void { $attachment = $attachments[2]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("smime.p7s", $attachment->name); + self::assertEquals('p7s', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("application/x-pkcs7-signature", $attachment->content_type); self::assertEquals("1", $attachment->content); diff --git a/tests/fixtures/PlainTextAttachmentTest.php b/tests/fixtures/PlainTextAttachmentTest.php index 6106b558..2e9993cc 100644 --- a/tests/fixtures/PlainTextAttachmentTest.php +++ b/tests/fixtures/PlainTextAttachmentTest.php @@ -42,6 +42,7 @@ public function testFixture() : void { $attachment = $message->attachments()->first(); self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("a.txt", $attachment->name); + self::assertEquals('txt', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertNull($attachment->content_type); self::assertEquals("Hi!", $attachment->content); diff --git a/tests/fixtures/StructuredWithAttachmentTest.php b/tests/fixtures/StructuredWithAttachmentTest.php index a49d3609..3fc591c3 100644 --- a/tests/fixtures/StructuredWithAttachmentTest.php +++ b/tests/fixtures/StructuredWithAttachmentTest.php @@ -43,6 +43,7 @@ public function testFixture() : void { $attachment = $message->attachments()->first(); self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals("MyFile.txt", $attachment->name); + self::assertEquals('txt', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("text/plain", $attachment->content_type); self::assertEquals("MyFileContent", $attachment->content); From 7fa61f6467a1fbfe7627ea79b668e6a187f8b17b Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 21 Mar 2023 21:20:19 +0100 Subject: [PATCH 093/203] Allow the `LIST` command response to be empty #393 --- src/Connection/Protocols/ImapProtocol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 55297af0..914347b6 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -838,7 +838,7 @@ public function getMessageNumber(string $id): Response { * @throws RuntimeException */ public function folders(string $reference = '', string $folder = '*'): Response { - $response = $this->requestAndResponse('LIST', $this->escapeString($reference, $folder)); + $response = $this->requestAndResponse('LIST', $this->escapeString($reference, $folder))->setCanBeEmpty(true); $list = $response->data(); $result = []; From 4bad1705753c1f345e12e9ccf76cd393d6155357 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 21 Mar 2023 21:21:19 +0100 Subject: [PATCH 094/203] Test for issue #393 added --- tests/issues/Issue393Test.php | 62 +++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/issues/Issue393Test.php diff --git a/tests/issues/Issue393Test.php b/tests/issues/Issue393Test.php new file mode 100644 index 00000000..73099e07 --- /dev/null +++ b/tests/issues/Issue393Test.php @@ -0,0 +1,62 @@ +getClient(); + $client->connect(); + + $delimiter = $this->getManager()->get("options.delimiter"); + $pattern = implode($delimiter, ['doesnt_exist', '%']); + + $folder = $client->getFolder('doesnt_exist'); + $this->deleteFolder($folder); + + $folders = $client->getFolders(true, $pattern, true); + self::assertCount(0, $folders); + + try { + $client->getFolders(true, $pattern, false); + $this->fail('Expected FolderFetchingException::class exception not thrown'); + } catch (FolderFetchingException $e) { + self::assertInstanceOf(FolderFetchingException::class, $e); + } + } +} \ No newline at end of file From 4913ec6e8da53ff69fdf1244ea29db3478a78730 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 21 Mar 2023 21:24:32 +0100 Subject: [PATCH 095/203] Soft fail option added to all folder fetching methods --- src/Client.php | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/Client.php b/src/Client.php index a4e004ba..8027dc53 100755 --- a/src/Client.php +++ b/src/Client.php @@ -519,6 +519,7 @@ public function getFolder(string $folder_name, ?string $delimiter = null, bool $ /** * Get a folder instance by a folder name * @param $folder_name + * @param bool $soft_fail If true, it will return null instead of throwing an exception * * @return Folder|null * @throws FolderFetchingException @@ -529,14 +530,16 @@ public function getFolder(string $folder_name, ?string $delimiter = null, bool $ * @throws RuntimeException * @throws ResponseException */ - public function getFolderByName($folder_name): ?Folder { - return $this->getFolders(false)->where("name", $folder_name)->first(); + public function getFolderByName($folder_name, bool $soft_fail = false): ?Folder { + return $this->getFolders(false, null, $soft_fail)->where("name", $folder_name)->first(); } /** * Get a folder instance by a folder path * @param $folder_path * @param bool $utf7 + * @param bool $soft_fail If true, it will return null instead of throwing an exception + * * @return Folder|null * @throws AuthFailedException * @throws ConnectionFailedException @@ -546,9 +549,9 @@ public function getFolderByName($folder_name): ?Folder { * @throws ResponseException * @throws RuntimeException */ - public function getFolderByPath($folder_path, bool $utf7 = false): ?Folder { + public function getFolderByPath($folder_path, bool $utf7 = false, bool $soft_fail = false): ?Folder { if (!$utf7) $folder_path = EncodingAliases::convert($folder_path, "utf-8", "utf7-imap"); - return $this->getFolders(false)->where("path", $folder_path)->first(); + return $this->getFolders(false, null, $soft_fail)->where("path", $folder_path)->first(); } /** @@ -557,17 +560,18 @@ public function getFolderByPath($folder_path, bool $utf7 = false): ?Folder { * * @param boolean $hierarchical * @param string|null $parent_folder + * @param bool $soft_fail If true, it will return an empty collection instead of throwing an exception * * @return FolderCollection + * @throws AuthFailedException * @throws ConnectionFailedException * @throws FolderFetchingException - * @throws AuthFailedException * @throws ImapBadRequestException * @throws ImapServerErrorException - * @throws RuntimeException * @throws ResponseException + * @throws RuntimeException */ - public function getFolders(bool $hierarchical = true, string $parent_folder = null): FolderCollection { + public function getFolders(bool $hierarchical = true, string $parent_folder = null, bool $soft_fail = false): FolderCollection { $this->checkConnection(); $folders = FolderCollection::make([]); @@ -581,7 +585,7 @@ public function getFolders(bool $hierarchical = true, string $parent_folder = nu if ($hierarchical && $folder->hasChildren()) { $pattern = $folder->full_name.$folder->delimiter.'%'; - $children = $this->getFolders(true, $pattern); + $children = $this->getFolders(true, $pattern, $soft_fail); $folder->setChildren($children); } @@ -589,9 +593,11 @@ public function getFolders(bool $hierarchical = true, string $parent_folder = nu } return $folders; - }else{ + }else if (!$soft_fail){ throw new FolderFetchingException("failed to fetch any folders"); } + + return $folders; } /** @@ -600,6 +606,7 @@ public function getFolders(bool $hierarchical = true, string $parent_folder = nu * * @param boolean $hierarchical * @param string|null $parent_folder + * @param bool $soft_fail If true, it will return an empty collection instead of throwing an exception * * @return FolderCollection * @throws FolderFetchingException @@ -610,7 +617,7 @@ public function getFolders(bool $hierarchical = true, string $parent_folder = nu * @throws RuntimeException * @throws ResponseException */ - public function getFoldersWithStatus(bool $hierarchical = true, string $parent_folder = null): FolderCollection { + public function getFoldersWithStatus(bool $hierarchical = true, string $parent_folder = null, bool $soft_fail = false): FolderCollection { $this->checkConnection(); $folders = FolderCollection::make([]); @@ -624,7 +631,7 @@ public function getFoldersWithStatus(bool $hierarchical = true, string $parent_f if ($hierarchical && $folder->hasChildren()) { $pattern = $folder->full_name.$folder->delimiter.'%'; - $children = $this->getFoldersWithStatus(true, $pattern); + $children = $this->getFoldersWithStatus(true, $pattern, $soft_fail); $folder->setChildren($children); } @@ -633,9 +640,11 @@ public function getFoldersWithStatus(bool $hierarchical = true, string $parent_f } return $folders; - }else{ + }else if (!$soft_fail){ throw new FolderFetchingException("failed to fetch any folders"); } + + return $folders; } /** From d8174e6e976f573c3a0588b11747c551a4e7e730 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 21 Mar 2023 21:25:36 +0100 Subject: [PATCH 096/203] Initialize folder children attributes on class initialization --- src/Folder.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Folder.php b/src/Folder.php index 0089dc05..c9ca395d 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -138,6 +138,8 @@ public function __construct(Client $client, string $folder_name, string $delimit $this->path = $folder_name; $this->full_name = $this->decodeName($folder_name); $this->name = $this->getSimpleName($this->delimiter, $this->full_name); + $this->children = new FolderCollection(); + $this->has_children = false; $this->parseAttributes($attributes); } From b2d127ea3996c36fe901732445c1948d47f03678 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 21 Mar 2023 21:25:49 +0100 Subject: [PATCH 097/203] Changelog updated --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f00dbb6..431cbf1f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Use all available methods to detect the attachment extension instead of just one +- Allow the `LIST` command response to be empty #393 +- Initialize folder children attributes on class initialization ### Added -- NaN +- Soft fail option added to all folder fetching methods. If soft fail is enabled, the method will return an empty collection instead of throwing an exception if the folder doesn't exist ### Breaking changes - NaN From 3b3bd567839826b061233687cddcc0fb4f06d122 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 11 Apr 2023 00:18:21 +0200 Subject: [PATCH 098/203] Sponsor link added --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 2fb9c13a..fa7febde 100755 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Discord: [discord.gg/rd4cN9h6][link-discord] - [Documentations](#documentations) - [Compatibility](#compatibility) - [Basic usage example](#basic-usage-example) +- [Sponsors](#sponsors) - [Testing](#testing) - [Known issues](#known-issues) - [Support](#support) @@ -97,6 +98,9 @@ foreach($folders as $folder){ } ``` +## Sponsors +[![Feline][ico-sponsor-feline]][link-sponsor-feline] + ## Testing To run the tests, please execute the following command: @@ -232,3 +236,7 @@ The MIT License (MIT). Please see [License File][link-license] for more informat [link-hits]: https://hits.webklex.com [link-snyk]: https://snyk.io/vuln/composer:webklex%2Fphp-imap [link-discord]: https://discord.gg/rd4cN9h6 + + +[ico-sponsor-feline]: https://cdn.feline.dk/public/feline.png +[link-sponsor-feline]: https://www.feline.dk \ No newline at end of file From 62362cd527f9ba95fc4a35a0b5b0503b5d817d2e Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 11 Apr 2023 00:19:29 +0200 Subject: [PATCH 099/203] Release information added --- CHANGELOG.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 431cbf1f..4934fae8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Breaking changes +- NaN + + +## [5.2.0] - 2023-04-11 +### Fixed - Use all available methods to detect the attachment extension instead of just one - Allow the `LIST` command response to be empty #393 - Initialize folder children attributes on class initialization @@ -13,9 +24,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Soft fail option added to all folder fetching methods. If soft fail is enabled, the method will return an empty collection instead of throwing an exception if the folder doesn't exist -### Breaking changes -- NaN - ## [5.1.0] - 2023-03-16 ### Fixed From 6ea5a94f2f8936ac5c2c40a46fbbd88f2bdeb16b Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 11 Apr 2023 00:21:40 +0200 Subject: [PATCH 100/203] Old links removed --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index fa7febde..c110d48f 100755 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ [![Latest release on Packagist][ico-release]][link-packagist] [![Latest prerelease on Packagist][ico-prerelease]][link-packagist] [![Software License][ico-license]][link-license] -[![Build Status][ico-travis]][link-scrutinizer] [![Total Downloads][ico-downloads]][link-downloads] [![Hits][ico-hits]][link-hits] [![Discord][ico-discord]][link-discord] @@ -213,26 +212,17 @@ The MIT License (MIT). Please see [License File][link-license] for more informat [ico-release]: https://img.shields.io/packagist/v/Webklex/php-imap.svg?style=flat-square&label=version [ico-prerelease]: https://img.shields.io/github/v/release/webklex/php-imap?include_prereleases&style=flat-square&label=pre-release [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square -[ico-travis]: https://img.shields.io/travis/Webklex/php-imap/master.svg?style=flat-square -[ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/Webklex/php-imap.svg?style=flat-square -[ico-code-quality]: https://img.shields.io/scrutinizer/g/Webklex/php-imap.svg?style=flat-square [ico-downloads]: https://img.shields.io/packagist/dt/Webklex/php-imap.svg?style=flat-square -[ico-build]: https://img.shields.io/scrutinizer/build/g/Webklex/php-imap/master?style=flat-square -[ico-quality]: https://img.shields.io/scrutinizer/quality/g/Webklex/php-imap/master?style=flat-square [ico-hits]: https://hits.webklex.com/svg/webklex/php-imap [ico-snyk]: https://snyk-widget.herokuapp.com/badge/composer/webklex/php-imap/badge.svg [ico-discord]: https://img.shields.io/static/v1?label=discord&message=open&color=5865f2&style=flat-square [link-packagist]: https://packagist.org/packages/Webklex/php-imap -[link-travis]: https://travis-ci.org/Webklex/php-imap -[link-scrutinizer]: https://scrutinizer-ci.com/g/Webklex/php-imap/code-structure -[link-code-quality]: https://scrutinizer-ci.com/g/Webklex/php-imap [link-downloads]: https://packagist.org/packages/Webklex/php-imap [link-author]: https://github.com/webklex [link-contributors]: https://github.com/Webklex/php-imap/graphs/contributors [link-license]: https://github.com/Webklex/php-imap/blob/master/LICENSE [link-changelog]: https://github.com/Webklex/php-imap/blob/master/CHANGELOG.md -[link-jetbrains]: https://www.jetbrains.com [link-hits]: https://hits.webklex.com [link-snyk]: https://snyk.io/vuln/composer:webklex%2Fphp-imap [link-discord]: https://discord.gg/rd4cN9h6 From 88aaafbd881f09c3ec174f7417d36a8dedbd1b37 Mon Sep 17 00:00:00 2001 From: Jeremy Angele <131715596+angelej@users.noreply.github.com> Date: Tue, 20 Jun 2023 13:07:45 +0200 Subject: [PATCH 101/203] Improve security (#414) * Improve security * Update phpunit tests --- src/Attachment.php | 26 +++++++++++-------- .../fixtures/AttachmentNoDispositionTest.php | 2 +- ...ddedEmailWithoutContentDispositionTest.php | 2 +- tests/fixtures/ExampleBounceTest.php | 2 +- tests/fixtures/InlineAttachmentTest.php | 2 +- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index efae50ca..2fbde843 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -216,6 +216,10 @@ protected function fetch(): void { $this->filename = $this->decodeName($filename); } + if(!$this->filename){ + $this->filename = bin2hex(random_bytes(10)); + } + if (($name = $this->part->name) !== null) { $this->name = $this->decodeName($name); } @@ -233,11 +237,6 @@ protected function fetch(): void { $this->name = $this->part->subtype; } } - - if (!$this->filename) { - $this->filename = $this->name; - } - $this->attributes = array_merge($this->part->getHeader()->getAttributes(), $this->attributes); } @@ -248,19 +247,19 @@ protected function fetch(): void { * * @return boolean */ - public function save(string $path, string $filename = null): bool { - $filename = $filename ?? $this->filename ?? $this->name ?? $this->id; + public function save(string $path, ?string $filename = null): bool { + $filename = $filename ? $this->decodeName($filename) : $this->filename; return file_put_contents($path.DIRECTORY_SEPARATOR.$filename, $this->getContent()) !== false; } /** * Decode a given name - * @param $name + * @param string|null $name * * @return string */ - public function decodeName($name): string { + public function decodeName(?string $name): string { if ($name !== null) { if (str_contains($name, "''")) { $parts = explode("''", $name); @@ -282,6 +281,11 @@ public function decodeName($name): string { if (preg_match('/%[0-9A-F]{2}/i', $name)) { $name = urldecode($name); } + + // sanitize $name + // order of '..' is important + $name = str_replace(['\\', '/', chr(0), ':', '..'], '', $name); + return $name; } return ""; @@ -317,8 +321,8 @@ public function getExtension(): ?string { } } if ($extension === null) { - $extensions = explode(".", $this->filename); - $extension = end($extensions); + $parts = explode(".", $this->filename); + $extension = count($parts) > 1 ? end($parts) : null; } return $extension; } diff --git a/tests/fixtures/AttachmentNoDispositionTest.php b/tests/fixtures/AttachmentNoDispositionTest.php index cb037053..1eee597b 100644 --- a/tests/fixtures/AttachmentNoDispositionTest.php +++ b/tests/fixtures/AttachmentNoDispositionTest.php @@ -38,7 +38,7 @@ public function testFixture() : void { $attachment = $message->attachments()->first(); self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->filename); + self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); self::assertEquals('text', $attachment->type); self::assertEquals('xls', $attachment->getExtension()); diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php index 0c7e53cb..0f6a8a3f 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -61,7 +61,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals("", $attachment->name); + self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->name); self::assertEquals('text', $attachment->type); self::assertEquals('', $attachment->getExtension()); self::assertEquals("message/rfc822", $attachment->content_type); diff --git a/tests/fixtures/ExampleBounceTest.php b/tests/fixtures/ExampleBounceTest.php index 89810fc0..685a72cf 100644 --- a/tests/fixtures/ExampleBounceTest.php +++ b/tests/fixtures/ExampleBounceTest.php @@ -72,7 +72,7 @@ public function testFixture(): void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals("", $attachment->filename); + self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); self::assertEquals("", $attachment->name); self::assertEquals('', $attachment->getExtension()); self::assertEquals('text', $attachment->type); diff --git a/tests/fixtures/InlineAttachmentTest.php b/tests/fixtures/InlineAttachmentTest.php index c20ac8d5..b66976ef 100644 --- a/tests/fixtures/InlineAttachmentTest.php +++ b/tests/fixtures/InlineAttachmentTest.php @@ -45,7 +45,7 @@ public function testFixture() : void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals("", $attachment->name); + self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->name); self::assertEquals('text', $attachment->type); self::assertEquals('', $attachment->getExtension()); self::assertEquals("image/png", $attachment->content_type); From a7770066936b2e8f6952f8598ec18f13bc1a58e7 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 20 Jun 2023 13:33:12 +0200 Subject: [PATCH 102/203] Additional "extension" location added --- src/Attachment.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index 2fbde843..fcbc54e6 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -216,10 +216,6 @@ protected function fetch(): void { $this->filename = $this->decodeName($filename); } - if(!$this->filename){ - $this->filename = bin2hex(random_bytes(10)); - } - if (($name = $this->part->name) !== null) { $this->name = $this->decodeName($name); } @@ -238,6 +234,10 @@ protected function fetch(): void { } } $this->attributes = array_merge($this->part->getHeader()->getAttributes(), $this->attributes); + + if(!$this->filename){ + $this->filename = bin2hex(random_bytes(10)); + } } /** @@ -324,6 +324,10 @@ public function getExtension(): ?string { $parts = explode(".", $this->filename); $extension = count($parts) > 1 ? end($parts) : null; } + if ($extension === null) { + $parts = explode(".", $this->name); + $extension = count($parts) > 1 ? end($parts) : null; + } return $extension; } From 495a3fb44e03775d3f58c7cbd23ae80ab04322b8 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 20 Jun 2023 13:33:55 +0200 Subject: [PATCH 103/203] tests updated --- tests/fixtures/AttachmentNoDispositionTest.php | 1 + tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php | 2 +- tests/fixtures/ExampleBounceTest.php | 2 +- tests/fixtures/InlineAttachmentTest.php | 3 ++- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/fixtures/AttachmentNoDispositionTest.php b/tests/fixtures/AttachmentNoDispositionTest.php index 1eee597b..76b3c12b 100644 --- a/tests/fixtures/AttachmentNoDispositionTest.php +++ b/tests/fixtures/AttachmentNoDispositionTest.php @@ -37,6 +37,7 @@ public function testFixture() : void { self::assertCount(1, $message->attachments()); $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php index 0f6a8a3f..67ad1cb0 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -61,7 +61,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->name); + self::assertEquals('', $attachment->name); self::assertEquals('text', $attachment->type); self::assertEquals('', $attachment->getExtension()); self::assertEquals("message/rfc822", $attachment->content_type); diff --git a/tests/fixtures/ExampleBounceTest.php b/tests/fixtures/ExampleBounceTest.php index 685a72cf..20288206 100644 --- a/tests/fixtures/ExampleBounceTest.php +++ b/tests/fixtures/ExampleBounceTest.php @@ -85,7 +85,7 @@ public function testFixture(): void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals("", $attachment->filename); + self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); self::assertEquals("", $attachment->name); self::assertEquals('', $attachment->getExtension()); self::assertEquals('text', $attachment->type); diff --git a/tests/fixtures/InlineAttachmentTest.php b/tests/fixtures/InlineAttachmentTest.php index b66976ef..a482f25b 100644 --- a/tests/fixtures/InlineAttachmentTest.php +++ b/tests/fixtures/InlineAttachmentTest.php @@ -45,7 +45,8 @@ public function testFixture() : void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->name); + self::assertEquals('', $attachment->name); + self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); self::assertEquals('text', $attachment->type); self::assertEquals('', $attachment->getExtension()); self::assertEquals("image/png", $attachment->content_type); From d1aeb4ed6a142f55f90a1bad2a5c797990a15294 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 20 Jun 2023 13:34:27 +0200 Subject: [PATCH 104/203] Security release --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4934fae8..d967e5c1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,25 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN +## [5.3.0] - Security patch - 2023-06-20 +### Fixed +- Potential RCE through path traversal fixed #414 (special thanks @angelej) + +### Security Impact and Mitigation +Impacted are all versions below v5.3.0. +If possible, update to >= v5.3.0 as soon as possible. Impacted was the `Attachment::save` +method which could be used to write files to the local filesystem. The path was not +properly sanitized and could be used to write files to arbitrary locations. + +However, the `Attachment::save` method is not used by default and has to be called +manually. If you are using this method without providing a sanitized path, you are +affected by this vulnerability. +If you are not using this method or are providing a sanitized path, you are not affected +by this vulnerability and no immediate action is required. + +If you have any questions, please feel to join this issue: https://github.com/Webklex/php-imap/issues/416 + + ## [5.2.0] - 2023-04-11 ### Fixed - Use all available methods to detect the attachment extension instead of just one From 4f99ebcc1edb371ca5698ad5e425701ef1f2cc5c Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 20 Jun 2023 13:40:42 +0200 Subject: [PATCH 105/203] Timeline added --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d967e5c1..e9f1df28 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,12 @@ by this vulnerability and no immediate action is required. If you have any questions, please feel to join this issue: https://github.com/Webklex/php-imap/issues/416 +#### Timeline +- 17.06.23 21:30: Vulnerability reported +- 18.06.23 19:14: Vulnerability confirmed +- 19.06.23 18:41: Vulnerability fixed via PR #414 +- 20.06.23 13:45: Security patch released + ## [5.2.0] - 2023-04-11 ### Fixed From 5c45dc2fad444dd3a21abcadbc686f3e815e03c6 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 20 Jun 2023 13:43:16 +0200 Subject: [PATCH 106/203] spelling fixed --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9f1df28..3a4bb3f2 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,8 +31,7 @@ affected by this vulnerability. If you are not using this method or are providing a sanitized path, you are not affected by this vulnerability and no immediate action is required. -If you have any questions, please feel to join this issue: https://github.com/Webklex/php-imap/issues/416 - +If you have any questions, please feel welcome to join this issue: https://github.com/Webklex/php-imap/issues/416 #### Timeline - 17.06.23 21:30: Vulnerability reported - 18.06.23 19:14: Vulnerability confirmed From d3b9eaf1498d8dc1fee7cdca1c4a10f497a40cad Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 19:01:34 +0200 Subject: [PATCH 107/203] Legacy protocol support fixed #411 --- CHANGELOG.md | 4 +- src/Connection/Protocols/LegacyProtocol.php | 129 +++--- tests/live/LegacyTest.php | 475 ++++++++++++++++++++ 3 files changed, 542 insertions(+), 66 deletions(-) create mode 100644 tests/live/LegacyTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a4bb3f2..f451f40c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Legacy protocol support fixed (object to array conversion) #411 ### Added - NaN @@ -37,6 +37,8 @@ If you have any questions, please feel welcome to join this issue: https://githu - 18.06.23 19:14: Vulnerability confirmed - 19.06.23 18:41: Vulnerability fixed via PR #414 - 20.06.23 13:45: Security patch released +- 21.06.23 20:48: CVE-2023-35169 got assigned +- 21.06.23 20:58: Advisory released https://github.com/Webklex/php-imap/security/advisories/GHSA-47p7-xfcc-4pv9 ## [5.2.0] - 2023-04-11 diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 6eedddef..10bc9d9f 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -72,7 +72,7 @@ public function connect(string $host, int $port = null) { * @return Response */ public function login(string $user, string $password): Response { - return $this->response()->wrap(function($response) use($user, $password){ + return $this->response()->wrap(function($response) use ($user, $password) { /** @var Response $response */ try { $this->stream = \imap_open( @@ -108,7 +108,7 @@ public function login(string $user, string $password): Response { } if ($this->stream !== false) { - return ["TAG".$response->Noun()." OK [] Logged in\r\n"]; + return ["TAG" . $response->Noun() . " OK [] Logged in\r\n"]; } $response->addError("failed to login"); @@ -156,7 +156,7 @@ protected function getAddress(): string { * @return Response */ public function logout(): Response { - return $this->response()->wrap(function($response){ + return $this->response()->wrap(function($response) { /** @var Response $response */ if ($this->stream) { $this->uid_cache = []; @@ -165,7 +165,7 @@ public function logout(): Response { $this->stream = false; return [ 0 => "BYE Logging out\r\n", - 1 => "TAG".$response->Noun()." OK Logout completed (0.001 + 0.000 secs).\r\n", + 1 => "TAG" . $response->Noun() . " OK Logout completed (0.001 + 0.000 secs).\r\n", ]; } $this->stream = false; @@ -199,7 +199,7 @@ public function selectFolder(string $folder = 'INBOX'): Response { throw new RuntimeException("failed to reopen stream."); } - return $this->response("imap_reopen")->wrap(function($response)use($folder, $flags){ + return $this->response("imap_reopen")->wrap(function($response) use ($folder, $flags) { /** @var Response $response */ \imap_reopen($this->stream, $this->getAddress() . $folder, $flags, 3); $this->uid_cache = []; @@ -222,10 +222,9 @@ public function examineFolder(string $folder = 'INBOX'): Response { if (str_starts_with($folder, ".")) { throw new RuntimeException("Segmentation fault prevented. Folders starts with an illegal char '.'."); } - $folder = $this->getAddress() . $folder; - return $this->response("imap_status")->wrap(function($response)use($folder){ + return $this->response("imap_status")->wrap(function($response) use ($folder) { /** @var Response $response */ - $status = \imap_status($this->stream, $folder, IMAP::SA_ALL); + $status = \imap_status($this->stream, $this->getAddress() . $folder, IMAP::SA_ALL); return $status ? [ "flags" => [], @@ -246,7 +245,7 @@ public function examineFolder(string $folder = 'INBOX'): Response { * @return Response */ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->response()->wrap(function($response)use($uids, $uid){ + return $this->response()->wrap(function($response) use ($uids, $uid) { /** @var Response $response */ $result = []; @@ -269,7 +268,7 @@ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid * @return Response */ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->response()->wrap(function($response)use($uids, $uid){ + return $this->response()->wrap(function($response) use ($uids, $uid) { /** @var Response $response */ $result = []; @@ -291,7 +290,7 @@ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid * @return Response */ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response { - return $this->response()->wrap(function($response)use($uids, $uid){ + return $this->response()->wrap(function($response) use ($uids, $uid) { /** @var Response $response */ $result = []; @@ -314,7 +313,7 @@ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response return $result; }); } - + /** * Fetch message sizes * @param int|array $uids @@ -323,21 +322,22 @@ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response * @return Response */ public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response { - return $this->response()->wrap(function($response)use($uids, $uid){ + return $this->response()->wrap(function($response) use ($uids, $uid) { /** @var Response $response */ $result = []; $uids = is_array($uids) ? $uids : [$uids]; - $uid_text = implode("','",$uids); + $uid_text = implode("','", $uids); $response->addCommand("imap_fetch_overview"); - $raw_overview = false; - if ($uid == IMAP::ST_UID) - $raw_overview = \imap_fetch_overview($this->stream, $uid_text, IMAP::FT_UID); - else - $raw_overview = \imap_fetch_overview($this->stream, $uid_text); + if ($uid == IMAP::ST_UID) { + $raw_overview = \imap_fetch_overview($this->stream, $uid_text, IMAP::FT_UID); + } else { + $raw_overview = \imap_fetch_overview($this->stream, $uid_text); + } if ($raw_overview !== false) { - foreach ($raw_overview as $overview_element) { - $result[$overview_element[$uid == IMAP::ST_UID ? 'uid': 'msgno']] = $overview_element['size']; - } + foreach ($raw_overview as $overview_element) { + $overview_element = (array)$overview_element; + $result[$overview_element[$uid == IMAP::ST_UID ? 'uid' : 'msgno']] = $overview_element['size']; + } } return $result; }); @@ -350,7 +350,7 @@ public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response * @return Response message number for given message or all messages as array */ public function getUid(int $id = null): Response { - return $this->response()->wrap(function($response)use($id){ + return $this->response()->wrap(function($response) use ($id) { /** @var Response $response */ if ($id === null) { if ($this->enable_uid_cache && $this->uid_cache) { @@ -370,7 +370,7 @@ public function getUid(int $id = null): Response { $response->addCommand("imap_uid"); $uid = \imap_uid($this->stream, $id); - if($uid) { + if ($uid) { return $uid; } @@ -385,7 +385,7 @@ public function getUid(int $id = null): Response { * @return Response message number */ public function getMessageNumber(string $id): Response { - return $this->response("imap_msgno")->wrap(function($response)use($id){ + return $this->response("imap_msgno")->wrap(function($response) use ($id) { /** @var Response $response */ return \imap_msgno($this->stream, $id); }); @@ -399,7 +399,7 @@ public function getMessageNumber(string $id): Response { * @return Response */ public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Response { - return $this->response("imap_fetch_overview")->wrap(function($response)use($sequence, $uid){ + return $this->response("imap_fetch_overview")->wrap(function($response) use ($sequence, $uid) { /** @var Response $response */ return \imap_fetch_overview($this->stream, $sequence, $uid ? IMAP::ST_UID : IMAP::NIL) ?: []; }); @@ -413,7 +413,7 @@ public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Resp * @return Response folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..)) */ public function folders(string $reference = '', string $folder = '*'): Response { - return $this->response("imap_getmailboxes")->wrap(function($response)use($reference, $folder){ + return $this->response("imap_getmailboxes")->wrap(function($response) use ($reference, $folder) { /** @var Response $response */ $result = []; @@ -447,7 +447,7 @@ public function folders(string $reference = '', string $folder = '*'): Response public function store(array|string $flags, int $from, int $to = null, string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, string $item = null): Response { $flag = trim(is_array($flags) ? implode(" ", $flags) : $flags); - return $this->response()->wrap(function($response)use($mode, $from, $flag, $uid, $silent){ + return $this->response()->wrap(function($response) use ($mode, $from, $flag, $uid, $silent) { /** @var Response $response */ if ($mode == "+") { @@ -459,9 +459,9 @@ public function store(array|string $flags, int $from, int $to = null, string $mo } if ($silent === true) { - if ($status){ + if ($status) { return [ - "TAG".$response->Noun()." OK Store completed (0.001 + 0.000 secs).\r\n" + "TAG" . $response->Noun() . " OK Store completed (0.001 + 0.000 secs).\r\n" ]; } return []; @@ -481,21 +481,20 @@ public function store(array|string $flags, int $from, int $to = null, string $mo * @return Response */ public function appendMessage(string $folder, string $message, array $flags = null, mixed $date = null): Response { - return $this->response("imap_append")->wrap(function($response)use($folder, $message, $flags, $date){ + return $this->response("imap_append")->wrap(function($response) use ($folder, $message, $flags, $date) { /** @var Response $response */ - if ($date != null) { if ($date instanceof \Carbon\Carbon) { $date = $date->format('d-M-Y H:i:s O'); } - if(\imap_append($this->stream, $folder, $message, $flags, $date)) { + if (\imap_append($this->stream, $this->getAddress() . $folder, $message, $flags, $date)) { return [ - "TAG".$response->Noun()." OK Append completed (0.001 + 0.000 secs).\r\n" + "OK Append completed (0.001 + 0.000 secs).\r\n" ]; } - } else if (\imap_append($this->stream, $folder, $message, $flags)){ + } else if (\imap_append($this->stream, $this->getAddress() . $folder, $message, $flags)) { return [ - "TAG".$response->Noun()." OK Append completed (0.001 + 0.000 secs).\r\n" + "OK Append completed (0.001 + 0.000 secs).\r\n" ]; } return []; @@ -513,12 +512,12 @@ public function appendMessage(string $folder, string $message, array $flags = nu * @return Response */ public function copyMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { - return $this->response("imap_mail_copy")->wrap(function($response)use($from, $folder, $uid){ + return $this->response("imap_mail_copy")->wrap(function($response) use ($from, $folder, $uid) { /** @var Response $response */ - if (\imap_mail_copy($this->stream, $from, $folder, $uid ? IMAP::ST_UID : IMAP::NIL)) { + if (\imap_mail_copy($this->stream, $from, $this->getAddress() . $folder, $uid ? IMAP::ST_UID : IMAP::NIL)) { return [ - "TAG".$response->Noun()." OK Copy completed (0.001 + 0.000 secs).\r\n" + "TAG" . $response->Noun() . " OK Copy completed (0.001 + 0.000 secs).\r\n" ]; } throw new ImapBadRequestException("Invalid ID $from"); @@ -534,20 +533,20 @@ public function copyMessage(string $folder, $from, int $to = null, int|string $u * @return Response Tokens if operation successful, false if an error occurred */ public function copyManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response { - return $this->response()->wrap(function($response)use($messages, $folder, $uid){ + return $this->response()->wrap(function($response) use ($messages, $folder, $uid) { /** @var Response $response */ foreach ($messages as $msg) { $copy_response = $this->copyMessage($folder, $msg, null, $uid); $response->stack($copy_response); if (empty($copy_response->data())) { return [ - "TAG".$response->Noun()." BAD Copy failed (0.001 + 0.000 secs).\r\n", + "TAG" . $response->Noun() . " BAD Copy failed (0.001 + 0.000 secs).\r\n", "Invalid ID $msg\r\n" ]; } } return [ - "TAG".$response->Noun()." OK Copy completed (0.001 + 0.000 secs).\r\n" + "TAG" . $response->Noun() . " OK Copy completed (0.001 + 0.000 secs).\r\n" ]; }); } @@ -563,10 +562,10 @@ public function copyManyMessages(array $messages, string $folder, int|string $ui * @return Response success */ public function moveMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { - return $this->response("imap_mail_move")->wrap(function($response)use($from, $folder, $uid){ - if (\imap_mail_move($this->stream, $from, $folder, $uid ? IMAP::ST_UID : IMAP::NIL)) { + return $this->response("imap_mail_move")->wrap(function($response) use ($from, $folder, $uid) { + if (\imap_mail_move($this->stream, $from, $this->getAddress() . $folder, $uid ? IMAP::ST_UID : IMAP::NIL)) { return [ - "TAG".$response->Noun()." OK Move completed (0.001 + 0.000 secs).\r\n" + "TAG" . $response->Noun() . " OK Move completed (0.001 + 0.000 secs).\r\n" ]; } throw new ImapBadRequestException("Invalid ID $from"); @@ -583,19 +582,19 @@ public function moveMessage(string $folder, $from, int $to = null, int|string $u * @throws ImapBadRequestException */ public function moveManyMessages(array $messages, string $folder, int|string $uid = IMAP::ST_UID): Response { - return $this->response()->wrap(function($response)use($messages, $folder, $uid){ + return $this->response()->wrap(function($response) use ($messages, $folder, $uid) { foreach ($messages as $msg) { $move_response = $this->moveMessage($folder, $msg, null, $uid); $response = $response->include($response); if (empty($move_response->data())) { return [ - "TAG".$response->Noun()." BAD Move failed (0.001 + 0.000 secs).\r\n", - "Invalid ID $msg\r\n" - ]; + "TAG" . $response->Noun() . " BAD Move failed (0.001 + 0.000 secs).\r\n", + "Invalid ID $msg\r\n" + ]; } } return [ - "TAG".$response->Noun()." OK Move completed (0.001 + 0.000 secs).\r\n" + "TAG" . $response->Noun() . " OK Move completed (0.001 + 0.000 secs).\r\n" ]; }); } @@ -620,9 +619,9 @@ public function ID($ids = null): Response { * @return Response */ public function createFolder(string $folder): Response { - return $this->response("imap_createmailbox")->wrap(function($response)use($folder){ - return \imap_createmailbox($this->stream, $folder) ? [ - 0 => "TAG".$response->Noun()." OK Create completed (0.004 + 0.000 + 0.003 secs).\r\n", + return $this->response("imap_createmailbox")->wrap(function($response) use ($folder) { + return \imap_createmailbox($this->stream, $this->getAddress() . $folder) ? [ + 0 => "TAG" . $response->Noun() . " OK Create completed (0.004 + 0.000 + 0.003 secs).\r\n", ] : []; }); } @@ -635,9 +634,9 @@ public function createFolder(string $folder): Response { * @return Response */ public function renameFolder(string $old, string $new): Response { - return $this->response("imap_renamemailbox")->wrap(function($response)use($old, $new){ - return \imap_renamemailbox($this->stream, $old, $new) ? [ - 0 => "TAG".$response->Noun()." OK Move completed (0.004 + 0.000 + 0.003 secs).\r\n", + return $this->response("imap_renamemailbox")->wrap(function($response) use ($old, $new) { + return \imap_renamemailbox($this->stream, $this->getAddress() . $old, $this->getAddress() . $new) ? [ + 0 => "TAG" . $response->Noun() . " OK Move completed (0.004 + 0.000 + 0.003 secs).\r\n", ] : []; }); } @@ -649,9 +648,9 @@ public function renameFolder(string $old, string $new): Response { * @return Response */ public function deleteFolder(string $folder): Response { - return $this->response("imap_deletemailbox")->wrap(function($response)use($folder){ - return \imap_deletemailbox($this->stream, $folder) ? [ - 0 => "TAG".$response->Noun()." OK Delete completed (0.004 + 0.000 + 0.003 secs).\r\n", + return $this->response("imap_deletemailbox")->wrap(function($response) use ($folder) { + return \imap_deletemailbox($this->stream, $this->getAddress() . $folder) ? [ + 0 => "OK Delete completed (0.004 + 0.000 + 0.003 secs).\r\n", ] : []; }); } @@ -682,9 +681,9 @@ public function unsubscribeFolder(string $folder): Response { * @return Response */ public function expunge(): Response { - return $this->response("imap_expunge")->wrap(function($response){ + return $this->response("imap_expunge")->wrap(function($response) { return \imap_expunge($this->stream) ? [ - 0 => "TAG".$response->Noun()." OK Expunge completed (0.001 + 0.000 secs).\r\n", + 0 => "TAG" . $response->Noun() . " OK Expunge completed (0.001 + 0.000 secs).\r\n", ] : []; }); } @@ -724,7 +723,7 @@ public function done() { * @return Response message ids */ public function search(array $params, int|string $uid = IMAP::ST_UID): Response { - return $this->response("imap_search")->wrap(function($response)use($params, $uid){ + return $this->response("imap_search")->wrap(function($response) use ($params, $uid) { $response->setCanBeEmpty(true); $result = \imap_search($this->stream, $params[0], $uid ? IMAP::ST_UID : IMAP::NIL); return $result ?: []; @@ -772,7 +771,7 @@ public function getProtocol(): string { * @return Response */ public function getQuota($username): Response { - return $this->response("imap_get_quota")->wrap(function($response)use($username){ + return $this->response("imap_get_quota")->wrap(function($response) use ($username) { $result = \imap_get_quota($this->stream, 'user.' . $username); return $result ?: []; }); @@ -785,8 +784,8 @@ public function getQuota($username): Response { * @return Response */ public function getQuotaRoot(string $quota_root = 'INBOX'): Response { - return $this->response("imap_get_quotaroot")->wrap(function($response)use($quota_root){ - $result = \imap_get_quotaroot($this->stream, $quota_root); + return $this->response("imap_get_quotaroot")->wrap(function($response) use ($quota_root) { + $result = \imap_get_quotaroot($this->stream, $this->getAddress() . $quota_root); return $result ?: []; }); } diff --git a/tests/live/LegacyTest.php b/tests/live/LegacyTest.php new file mode 100644 index 00000000..85fa4448 --- /dev/null +++ b/tests/live/LegacyTest.php @@ -0,0 +1,475 @@ +markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test."); + } + + parent::__construct($name, $data, $dataName); + $manager = new ClientManager([ + 'options' => [ + "debug" => $_ENV["LIVE_MAILBOX_DEBUG"] ?? false, + ], + 'accounts' => [ + 'legacy' => [ + 'host' => getenv("LIVE_MAILBOX_HOST"), + 'port' => getenv("LIVE_MAILBOX_PORT"), + 'encryption' => getenv("LIVE_MAILBOX_ENCRYPTION"), + 'validate_cert' => getenv("LIVE_MAILBOX_VALIDATE_CERT"), + 'username' => getenv("LIVE_MAILBOX_USERNAME"), + 'password' => getenv("LIVE_MAILBOX_PASSWORD"), + 'protocol' => 'legacy-imap', + ], + ], + ]); + self::$client = $manager->account('legacy'); + self::$client->connect(); + self::assertInstanceOf(Client::class, self::$client->connect()); + } + + /** + * @throws RuntimeException + * @throws MessageFlagException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ConnectionFailedException + * @throws InvalidMessageDateException + * @throws AuthFailedException + * @throws MessageHeaderFetchingException + */ + public function testSizes(): void { + + $delimiter = ClientManager::get("options.delimiter"); + $child_path = implode($delimiter, ['INBOX', 'test']); + if (self::$client->getFolder($child_path) === null) { + self::$client->createFolder($child_path, false); + } + $folder = $this->getFolder($child_path); + + self::assertInstanceOf(Folder::class, $folder); + + $message = $this->appendMessageTemplate($folder, "plain.eml"); + self::assertInstanceOf(Message::class, $message); + + self::assertEquals(214, $message->size); + self::assertEquals(214, self::$client->getConnection()->sizes($message->uid)->array()[$message->uid]); + } + + /** + * Try to create a new query instance + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testQuery(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + self::assertInstanceOf(WhereQuery::class, $folder->query()); + self::assertInstanceOf(WhereQuery::class, $folder->search()); + self::assertInstanceOf(WhereQuery::class, $folder->messages()); + } + + /** + * Get a folder + * @param string $folder_path + * + * @return Folder + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + * @throws FolderFetchingException + */ + final protected function getFolder(string $folder_path = "INDEX"): Folder { + $folder = self::$client->getFolderByPath($folder_path); + self::assertInstanceOf(Folder::class, $folder); + + return $folder; + } + + + /** + * Append a message to a folder + * @param Folder $folder + * @param string $message + * + * @return Message + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + */ + final protected function appendMessage(Folder $folder, string $message): Message { + $status = $folder->select(); + if (!isset($status['uidnext'])) { + $this->fail("No UIDNEXT returned"); + } + + $response = $folder->appendMessage($message); + $valid_response = false; + foreach ($response as $line) { + if (str_starts_with($line, 'OK')) { + $valid_response = true; + break; + } + } + if (!$valid_response) { + $this->fail("Failed to append message: ".implode("\n", $response)); + } + + $message = $folder->messages()->getMessageByUid($status['uidnext']); + self::assertInstanceOf(Message::class, $message); + + return $message; + } + + /** + * Append a message template to a folder + * @param Folder $folder + * @param string $template + * + * @return Message + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + */ + final protected function appendMessageTemplate(Folder $folder, string $template): Message { + $content = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", $template])); + return $this->appendMessage($folder, $content); + } + + /** + * Delete a folder if it is given + * @param Folder|null $folder + * + * @return bool + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws ResponseException + * @throws RuntimeException + */ + final protected function deleteFolder(Folder $folder = null): bool { + $response = $folder?->delete(false); + if (is_array($response)) { + $valid_response = false; + foreach ($response as $line) { + if (str_starts_with($line, 'OK')) { + $valid_response = true; + break; + } + } + if (!$valid_response) { + $this->fail("Failed to delete mailbox: ".implode("\n", $response)); + } + return $valid_response; + } + return false; + } + + /** + * Try to create a new query instance with a where clause + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + * @throws GetMessagesFailedException + * @throws InvalidWhereQueryCriteriaException + * @throws MessageSearchValidationException + */ + public function testQueryWhere(): void { + $delimiter = ClientManager::get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'search']); + + $folder = self::$client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = self::$client->createFolder($folder_path, false); + + $messages = [ + $this->appendMessageTemplate($folder, '1366671050@github.com.eml'), + $this->appendMessageTemplate($folder, 'attachment_encoded_filename.eml'), + $this->appendMessageTemplate($folder, 'attachment_long_filename.eml'), + $this->appendMessageTemplate($folder, 'attachment_no_disposition.eml'), + $this->appendMessageTemplate($folder, 'bcc.eml'), + $this->appendMessageTemplate($folder, 'boolean_decoded_content.eml'), + $this->appendMessageTemplate($folder, 'email_address.eml'), + $this->appendMessageTemplate($folder, 'embedded_email.eml'), + $this->appendMessageTemplate($folder, 'embedded_email_without_content_disposition.eml'), + $this->appendMessageTemplate($folder, 'embedded_email_without_content_disposition-embedded.eml'), + $this->appendMessageTemplate($folder, 'example_attachment.eml'), + $this->appendMessageTemplate($folder, 'example_bounce.eml'), + $this->appendMessageTemplate($folder, 'four_nested_emails.eml'), + $this->appendMessageTemplate($folder, 'gbk_charset.eml'), + $this->appendMessageTemplate($folder, 'html_only.eml'), + $this->appendMessageTemplate($folder, 'imap_mime_header_decode_returns_false.eml'), + $this->appendMessageTemplate($folder, 'inline_attachment.eml'), + $this->appendMessageTemplate($folder, 'issue-275.eml'), + $this->appendMessageTemplate($folder, 'issue-275-2.eml'), + $this->appendMessageTemplate($folder, 'issue-348.eml'), + $this->appendMessageTemplate($folder, 'ks_c_5601-1987_headers.eml'), + $this->appendMessageTemplate($folder, 'mail_that_is_attachment.eml'), + $this->appendMessageTemplate($folder, 'missing_date.eml'), + $this->appendMessageTemplate($folder, 'missing_from.eml'), + $this->appendMessageTemplate($folder, 'mixed_filename.eml'), + $this->appendMessageTemplate($folder, 'multipart_without_body.eml'), + $this->appendMessageTemplate($folder, 'multiple_html_parts_and_attachments.eml'), + $this->appendMessageTemplate($folder, 'multiple_nested_attachments.eml'), + $this->appendMessageTemplate($folder, 'nestes_embedded_with_attachment.eml'), + $this->appendMessageTemplate($folder, 'null_content_charset.eml'), + $this->appendMessageTemplate($folder, 'pec.eml'), + $this->appendMessageTemplate($folder, 'plain.eml'), + $this->appendMessageTemplate($folder, 'plain_only.eml'), + $this->appendMessageTemplate($folder, 'plain_text_attachment.eml'), + $this->appendMessageTemplate($folder, 'references.eml'), + $this->appendMessageTemplate($folder, 'simple_multipart.eml'), + $this->appendMessageTemplate($folder, 'structured_with_attachment.eml'), + $this->appendMessageTemplate($folder, 'thread_my_topic.eml'), + $this->appendMessageTemplate($folder, 'thread_re_my_topic.eml'), + $this->appendMessageTemplate($folder, 'thread_unrelated.eml'), + $this->appendMessageTemplate($folder, 'undefined_charset_header.eml'), + $this->appendMessageTemplate($folder, 'undisclosed_recipients_minus.eml'), + $this->appendMessageTemplate($folder, 'undisclosed_recipients_space.eml'), + $this->appendMessageTemplate($folder, 'unknown_encoding.eml'), + $this->appendMessageTemplate($folder, 'without_charset_plain_only.eml'), + $this->appendMessageTemplate($folder, 'without_charset_simple_multipart.eml'), + ]; + + $folder->getClient()->expunge(); + + $query = $folder->query()->all(); + self::assertEquals(count($messages), $query->count()); + + $query = $folder->query()->whereSubject("test"); + self::assertEquals(11, $query->count()); + + $query = $folder->query()->whereOn(Carbon::now()); + self::assertEquals(count($messages), $query->count()); + + self::assertTrue($this->deleteFolder($folder)); + } + + /** + * Test query where criteria + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testQueryWhereCriteria(): void { + self::$client->reconnect(); + + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $this->assertWhereSearchCriteria($folder, 'SUBJECT', 'Test'); + $this->assertWhereSearchCriteria($folder, 'BODY', 'Test'); + $this->assertWhereSearchCriteria($folder, 'TEXT', 'Test'); + $this->assertWhereSearchCriteria($folder, 'KEYWORD', 'Test'); + $this->assertWhereSearchCriteria($folder, 'UNKEYWORD', 'Test'); + $this->assertWhereSearchCriteria($folder, 'FLAGGED', 'Seen'); + $this->assertWhereSearchCriteria($folder, 'UNFLAGGED', 'Seen'); + $this->assertHeaderSearchCriteria($folder, 'Message-ID', 'Seen'); + $this->assertHeaderSearchCriteria($folder, 'In-Reply-To', 'Seen'); + $this->assertWhereSearchCriteria($folder, 'BCC', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'CC', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'FROM', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'TO', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'UID', '1'); + $this->assertWhereSearchCriteria($folder, 'UID', '1,2'); + $this->assertWhereSearchCriteria($folder, 'ALL'); + $this->assertWhereSearchCriteria($folder, 'NEW'); + $this->assertWhereSearchCriteria($folder, 'OLD'); + $this->assertWhereSearchCriteria($folder, 'SEEN'); + $this->assertWhereSearchCriteria($folder, 'UNSEEN'); + $this->assertWhereSearchCriteria($folder, 'RECENT'); + $this->assertWhereSearchCriteria($folder, 'ANSWERED'); + $this->assertWhereSearchCriteria($folder, 'UNANSWERED'); + $this->assertWhereSearchCriteria($folder, 'DELETED'); + $this->assertWhereSearchCriteria($folder, 'UNDELETED'); + $this->assertHeaderSearchCriteria($folder, 'Content-Language','en_US'); + $this->assertWhereSearchCriteria($folder, 'CUSTOM X-Spam-Flag NO'); + $this->assertWhereSearchCriteria($folder, 'CUSTOM X-Spam-Flag YES'); + $this->assertWhereSearchCriteria($folder, 'NOT'); + $this->assertWhereSearchCriteria($folder, 'OR'); + $this->assertWhereSearchCriteria($folder, 'AND'); + $this->assertWhereSearchCriteria($folder, 'BEFORE', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'BEFORE', Carbon::now()->subDays(1), true); + $this->assertWhereSearchCriteria($folder, 'ON', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'ON', Carbon::now()->subDays(1), true); + $this->assertWhereSearchCriteria($folder, 'SINCE', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'SINCE', Carbon::now()->subDays(1), true); + } + + /** + * Assert where search criteria + * @param Folder $folder + * @param string $criteria + * @param string|Carbon|null $value + * @param bool $date + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws ResponseException + * @throws RuntimeException + */ + protected function assertWhereSearchCriteria(Folder $folder, string $criteria, Carbon|string $value = null, bool $date = false): void { + $query = $folder->query()->where($criteria, $value); + self::assertInstanceOf(WhereQuery::class, $query); + + $item = $query->getQuery()->first(); + $criteria = str_replace("CUSTOM ", "", $criteria); + $expected = $value === null ? [$criteria] : [$criteria, $value]; + if ($date === true && $value instanceof Carbon) { + $date_format = ClientManager::get('date_format', 'd M y'); + $expected[1] = $value->format($date_format); + } + + self::assertIsArray($item); + self::assertIsString($item[0]); + if($value !== null) { + self::assertCount(2, $item); + self::assertIsString($item[1]); + }else{ + self::assertCount(1, $item); + } + self::assertSame($expected, $item); + } + + /** + * Assert header search criteria + * @param Folder $folder + * @param string $criteria + * @param mixed|null $value + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws ResponseException + * @throws RuntimeException + */ + protected function assertHeaderSearchCriteria(Folder $folder, string $criteria, mixed $value = null): void { + $query = $folder->query()->whereHeader($criteria, $value); + self::assertInstanceOf(WhereQuery::class, $query); + + $item = $query->getQuery()->first(); + + self::assertIsArray($item); + self::assertIsString($item[0]); + self::assertCount(1, $item); + self::assertSame(['HEADER '.$criteria.' '.$value], $item); + } +} \ No newline at end of file From 9d6a2d5190555af1bff122cb86e64bfc92569319 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 21:05:27 +0200 Subject: [PATCH 108/203] Header value decoding improved #410 --- CHANGELOG.md | 1 + src/Header.php | 14 +++++----- .../ImapMimeHeaderDecodeReturnsFalseTest.php | 2 +- tests/issues/Issue410Test.php | 27 +++++++++++++++++++ tests/messages/issue-410.eml | 9 +++++++ 5 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 tests/issues/Issue410Test.php create mode 100644 tests/messages/issue-410.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index f451f40c..a1a0f0b1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Legacy protocol support fixed (object to array conversion) #411 +- Header value decoding improved #410 ### Added - NaN diff --git a/src/Header.php b/src/Header.php index 4bd4390d..e9ff162b 100644 --- a/src/Header.php +++ b/src/Header.php @@ -421,7 +421,7 @@ public function decode(mixed $value): mixed { $decoder = $this->config['decoder']['message']; if ($value !== null) { - if ($decoder === 'utf-8' && extension_loaded('imap')) { + if ($decoder === 'utf-8') { $decoded_values = $this->mime_header_decode($value); $tempValue = ""; foreach ($decoded_values as $decoded_value) { @@ -429,14 +429,16 @@ public function decode(mixed $value): mixed { } if ($tempValue) { $value = $tempValue; - } else { + } else if (extension_loaded('imap')) { $value = \imap_utf8($value); + }else if (function_exists('iconv_mime_decode')){ + $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); + }else{ + $value = mb_decode_mimeheader($value); } - } elseif ($decoder === 'iconv' && $this->is_uft8($value)) { + }elseif ($decoder === 'iconv') { $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); - } - - if ($this->is_uft8($value)) { + }else if ($this->is_uft8($value)) { $value = mb_decode_mimeheader($value); } diff --git a/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php b/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php index 6f2629ff..7f90a4ec 100644 --- a/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php +++ b/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php @@ -27,7 +27,7 @@ class ImapMimeHeaderDecodeReturnsFalseTest extends FixtureTestCase { public function testFixture() : void { $message = $this->getFixture("imap_mime_header_decode_returns_false.eml"); - self::assertEquals("?p?#]ݰ?[??W̌ N? ?LL?̍L??NL˜", $message->subject->first()); + self::assertEquals("=?UTF-8?B?nnDusSNdG92w6Fuw61fMjAxOF8wMy0xMzMyNTMzMTkzLnBkZg==?=", $message->subject->first()); self::assertEquals("Hi", $message->getTextBody()); self::assertFalse($message->hasHTMLBody()); self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); diff --git a/tests/issues/Issue410Test.php b/tests/issues/Issue410Test.php new file mode 100644 index 00000000..7c3e6dc0 --- /dev/null +++ b/tests/issues/Issue410Test.php @@ -0,0 +1,27 @@ +subject); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-410.eml b/tests/messages/issue-410.eml new file mode 100644 index 00000000..05ddeff7 --- /dev/null +++ b/tests/messages/issue-410.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: =?ISO-2022-JP?B?GyRCIXlCaBsoQjEzMhskQjlmISEhViUsITwlRyVzGyhCJhskQiUoJS8lOSVGJWolIiFXQGxMZ0U5JE4kPyRhJE4jURsoQiYbJEIjQSU1JW0lcyEhIVo3bjQpJSglLyU5JUYlaiUiISYlbyE8JS8hWxsoQg==?= +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file From a4fe003f1135b61edef0141b5d2668439f361bff Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 21:13:39 +0200 Subject: [PATCH 109/203] issue #413 test case added --- tests/issues/Issue413Test.php | 28 ++++++++++++++++++++++++++++ tests/messages/issue-413.eml | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/issues/Issue413Test.php create mode 100644 tests/messages/issue-413.eml diff --git a/tests/issues/Issue413Test.php b/tests/issues/Issue413Test.php new file mode 100644 index 00000000..22cc881b --- /dev/null +++ b/tests/issues/Issue413Test.php @@ -0,0 +1,28 @@ +subject); + self::assertSame("This is just a test, so ignore it (if you can!)\r\n\r\nTony Marston", $message->getTextBody()); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-413.eml b/tests/messages/issue-413.eml new file mode 100644 index 00000000..a01412b3 --- /dev/null +++ b/tests/messages/issue-413.eml @@ -0,0 +1,32 @@ +Return-Path: +Delivered-To: gmx@tonymarston.co.uk +Received: from ion.dnsprotect.com + by ion.dnsprotect.com with LMTP + id oPy8IzIke2Rr4gIAzEkvSQ + (envelope-from ) + for ; Sat, 03 Jun 2023 07:29:54 -0400 +Return-path: +Envelope-to: gmx@tonymarston.net +Delivery-date: Sat, 03 Jun 2023 07:29:54 -0400 +Received: from [::1] (port=48740 helo=ion.dnsprotect.com) + by ion.dnsprotect.com with esmtpa (Exim 4.96) + (envelope-from ) + id 1q5PSF-000nPQ-1F + for gmx@tonymarston.net; + Sat, 03 Jun 2023 07:29:54 -0400 +MIME-Version: 1.0 +Date: Sat, 03 Jun 2023 07:29:54 -0400 +From: radicore +To: gmx@tonymarston.net +Subject: Test Message +User-Agent: Roundcube Webmail/1.6.0 +Message-ID: +X-Sender: radicore@radicore.org +Content-Type: text/plain; charset=US-ASCII; + format=flowed +Content-Transfer-Encoding: 7bit +X-From-Rewrite: unmodified, already matched + +This is just a test, so ignore it (if you can!) + +Tony Marston \ No newline at end of file From df4f3c3d60a5fd6a2c0f952a2b1609dd2e253405 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 21:25:52 +0200 Subject: [PATCH 110/203] issue #412 test case added --- tests/issues/Issue412Test.php | 30 +++++ tests/messages/issue-412.eml | 216 ++++++++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 tests/issues/Issue412Test.php create mode 100644 tests/messages/issue-412.eml diff --git a/tests/issues/Issue412Test.php b/tests/issues/Issue412Test.php new file mode 100644 index 00000000..5d105541 --- /dev/null +++ b/tests/issues/Issue412Test.php @@ -0,0 +1,30 @@ +subject); + self::assertSame("64254d63e92a36ee02c760676351e60a", md5($message->getTextBody())); + self::assertSame("2e4de288f6a1ed658548ed11fcdb1d79", md5($message->getHTMLBody())); + self::assertSame(0, $message->attachments()->count()); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-412.eml b/tests/messages/issue-412.eml new file mode 100644 index 00000000..f28b81a7 --- /dev/null +++ b/tests/messages/issue-412.eml @@ -0,0 +1,216 @@ +Return-Path: +Delivered-To: gmx@tonymarston.co.uk +Received: from ion.dnsprotect.com + by ion.dnsprotect.com with LMTP + id YFDRGeZ6d2SlCRUAzEkvSQ + (envelope-from ) + for ; Wed, 31 May 2023 12:50:46 -0400 +Return-path: +Envelope-to: gmx@tonymarston.co.uk +Delivery-date: Wed, 31 May 2023 12:50:46 -0400 +Received: from mail-vi1eur04olkn2050.outbound.protection.outlook.com ([40.92.75.50]:23637 helo=EUR04-VI1-obe.outbound.protection.outlook.com) + by ion.dnsprotect.com with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + (Exim 4.96) + (envelope-from ) + id 1q4P28-005pTm-0E + for gmx@tonymarston.co.uk; + Wed, 31 May 2023 12:50:46 -0400 +ARC-Seal: i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none; + b=SYBG6BEOiWsmVfcgXUQJ0moS1SuG/IdTK6DIT4H3g7CQ+hbWIbItTxOhFzJiHP+q0uz+XzR1FzX2Daso+4iKotX7x2ViHIA0Hs65xUZVFtvflMsUrB+5RLlf3Pr7DiNKguQQtC+R2IBLvedc+IqElnMrTHcxLVS2gWl89MZx5Q0bXGWW/9wBVq6yvc6C69ynppcEdD0QZsoUQlp2DgzDpg8iG3y6dYGxFiTvLzw08nTQiCuqi8qQ+nmHyeeItIiAmyKynvyiL+kh4frcSDS67r6PU/CBxvto/nP3RCAxHuzJEGOpS7LJPqoJAlRSrUp2zpeEMpmDFJUE/Jo0K+EgcQ== +ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; + s=arcselector9901; + h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; + bh=ISg5C/1AgVASEPkGp/cgowfc/y9qywGvXMTv5yjJffs=; + b=eVYGErUWMxOeFGLg2nPuB0E/ngKet0hEVcN8Ay4ujGFY4k7i+1hrmnOMD6XiaIk3gVrqPalsmDjmEHpS0zV3+fPPTSktlSvkLrUr5ViVI1kMVBbBQsowLD5x3FpX7fnP2q17WPQ2P6R8Ibudkdnei8uq7gZhc3CSDLv4PfNma45H0FmdaB40mF2dCYzj5hEzr6jmMliANHJjznuDEFEUH3CfS1/iIA9rzhBKPKtahipTNeYiLqvZpKo1fO/XkZ57T44fqHkocfCyEK3Y1rehWudmkU8a9eEZlU5nBC6xoGO3P5Q1XIUNEHFmx2HH7eO8IgGzq/vbLMhcvbc3Ysdb2A== +ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none; + dkim=none; arc=none +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=hotmail.com; + s=selector1; + h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; + bh=ISg5C/1AgVASEPkGp/cgowfc/y9qywGvXMTv5yjJffs=; + b=Qv71Bx+LsM9Lo0uen0vxQzWI3ju4Q095ZMkKPXDaKsd5Y8dA3QteMpAPhy3/mAdP1+EV8NviDBhamtTG4qMO+zEqu/pyRpBGZOtjyiJGKh7aFh2bbodOkJkiGqH3jPwYBnE7T1XAqDVFmvRpuqIkqBe9FXeCZKRrF/Na5Y+zuklH7ebuGQVzIK+xol6q7BDgb/oLul7Wa3r3Lw40cPW5leUgwxngRFMucUVVO5aJ4MWlk76CmcN8XqgwVcFaACY80HLWRqRZfM8n24/KzV9nKSZIQFCgJi2CiqnEWVRSZZtZ9SudJJv4S3C/gU4OYoiFKr7GkEQibyqE2QkGHCBA1g== +Received: from PAXP193MB2364.EURP193.PROD.OUTLOOK.COM (2603:10a6:102:22b::9) + by DB8P193MB0773.EURP193.PROD.OUTLOOK.COM (2603:10a6:10:15a::10) with + Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6455.22; Wed, 31 May + 2023 16:50:03 +0000 +Received: from PAXP193MB2364.EURP193.PROD.OUTLOOK.COM + ([fe80::b962:59b:2d33:85c2]) by PAXP193MB2364.EURP193.PROD.OUTLOOK.COM + ([fe80::b962:59b:2d33:85c2%5]) with mapi id 15.20.6455.020; Wed, 31 May 2023 + 16:50:03 +0000 +From: Tony Marston +To: "gmx@tonymarston.co.uk" +Subject: RE: TEST MESSAGE +Thread-Topic: TEST MESSAGE +Thread-Index: AQHZjysmbQn8p2cYOEawNV6ywnFmN690oIyAgAAA5rw= +Date: Wed, 31 May 2023 16:50:03 +0000 +Message-ID: + +References: + + <944a7d16fcb607628f358cdd0f5ba8c0@tonymarston.co.uk> +In-Reply-To: <944a7d16fcb607628f358cdd0f5ba8c0@tonymarston.co.uk> +Accept-Language: en-GB, en-US +Content-Language: en-GB +X-MS-Has-Attach: +X-MS-TNEF-Correlator: +x-ms-exchange-messagesentrepresentingtype: 1 +x-tmn: [ABOzTq1ytyWYuvBD5A4I78Eqna1hBLeM] +x-ms-publictraffictype: Email +x-ms-traffictypediagnostic: PAXP193MB2364:EE_|DB8P193MB0773:EE_ +x-ms-office365-filtering-correlation-id: 7fbd6771-9a0d-495d-c4d3-08db61f714a6 +x-microsoft-antispam: BCL:0; +x-microsoft-antispam-message-info: + fwWKs8Qs/JyQ+pSnCNC804PQ86JYn/R63U7P6WCeXLC/fVSGqcAnslOc2vbxC2cUeTPPfow1WAz3f73/7Uk+byKxiE6L48WkgL6BFKVkYCwPKcQ3ps6KyEdGL6uUvPqYKwf5gFgUiTt+fKDiR/GJljj6qmN51jd5lUBvLf1g59q3LryUC+lEy6iLQ8cjCivzmBcR+0C2+uaa/xsjJxokbIEQoMicjjUiVWFkxRFBDr6FO8kEQyzzs70pNivK6mcXpVeJSOiLQfYa/2Q5QDYhE8kznG1EYUzHBQD/sLp/maUgmpKj1b8ObeI13QXed1qih+CtdLYAmPs5GPaoz8aY7pmaxroKLjBAqfOAC2IeQ3grxdQ8eRXlrnZ29cQnvD9tDryUvE9nyQLinaM2Dft4MueHvBTL5+WOTNnVQB98zVjnop8iVkwNrBKzPYiox9ufs9XFXNl0+2fFn66647ET7y/DkBKcszTKYF5RRp5o59QAh8rUTsaKeGJkPkyowZd+i6R12NIavL+eOOgnKvziTTbNl1lGgP+3zTKbgbb6K+DxfbKNl1zaTNvonOHwzI3jfdkDRtg5BvZVKstyl9AfZqo6OZ7ii3JKgVquRVWAtEQ6J6tx1Va/nTVrn1478+Dj +x-ms-exchange-antispam-messagedata-chunkcount: 1 +x-ms-exchange-antispam-messagedata-0: + =?us-ascii?Q?EMaSeqWhMBslXR7KptI34/opub+lBzTReVt3ACHDu/w7NvVUPpdp4YhwogpL?= + =?us-ascii?Q?8V9o/Zj+8vv6zZ7YL0n2NpODi8fzrPScgRiZGtJmfS58OqfgW2yygp1BmawZ?= + =?us-ascii?Q?y99Omv39THENDmCAba4d8zYLikOc+HJUfhpWeb/L9yqut7T2QkPFJYoeN5vU?= + =?us-ascii?Q?LeJ1hoPlguhNnDwMtE3HTgAl0Hbesms0Rb7wGaTDsgAc7XOO+8XpqLamnU0m?= + =?us-ascii?Q?zPpkQXV8+V5dFU2HxgIcYPYSaTX1CLPCdNcCAr1uHPnN81Ntm9Jb7fpYs1oA?= + =?us-ascii?Q?tgzPMwt37Ks9eQucJWZ5LQnNmZl/kYtVnvkylqYYMWiAg3y2XtjVkW/Ut/V2?= + =?us-ascii?Q?6vMfyCB5fEGJrn/uCp+KwL1s7s/4B332Bn4zvwpYd5TioMSGf9Rdp157eAfg?= + =?us-ascii?Q?LiNlLjDFdRc5SSEkUEl1TeX0FOQLsaCsQQqC/hzb10boa49GuxpoKwRmwhAO?= + =?us-ascii?Q?LZ2+veS5Jr1qWngBGo4MTkEq7nD6vBRIXQmKiLMpJc+Gk3/PCADL+H0IRH0+?= + =?us-ascii?Q?4uHnvcVr6CrDDZ2BEwcaWOa/ct8yUI5G9SC5gFP53TaS2llCnYwHAX1PkMAo?= + =?us-ascii?Q?w2LHFHE5I5dtQQJHaWNGMJGYdPmb9dDrksggLWXN+IxsxvFcFNSK2GPaqOMJ?= + =?us-ascii?Q?H4ht1rpqHTlU0uzgjb19gKCCcBdZfIzv118RTjFYG+EX/rsHlNRei/OWdTBF?= + =?us-ascii?Q?xf5cdnap836rQmre5ZoubNsSw2qKSRJhmZH1pCHoCFLtreM1fk7kkVJfkUz5?= + =?us-ascii?Q?ApMa03dEfFvzVv5wvPdWBLiCqzI6z6z8fUmwg2XfvK9Nyxb1AOZoT7JUXnp2?= + =?us-ascii?Q?Me6dTKqGKsBx87Dtny+fANHqgOm+Eo/pBZqyXwN93udbltxPmtNJ84MJJjyx?= + =?us-ascii?Q?vbEiDnMVb4knBO+sBqlKAVUv1F9ZJA2oUTrOD7t1xx6nBmQTgoYN6zsi+dgw?= + =?us-ascii?Q?nPebsl1/6fUy73FWLUKkeA64PeSa00Zi/q53ylXmUZV4Pc+11blKdL8o+p32?= + =?us-ascii?Q?dTD0ndul0WvvpQf8RdYNtGJ/BMurqNfvHq9wJo7Iu4fgTElR50ngwEsr28Bc?= + =?us-ascii?Q?G81pAb2fNhID6ewyOGfj87kqybxUhv1E+4pquh770UagjD1J3rKAUiw1sxWp?= + =?us-ascii?Q?u+FSmd7HKgd2cKJsmMErnQelF3DNozw5d0qdNELXZNO03xlMiADVvhqEJuqc?= + =?us-ascii?Q?uArdsSo2hyApUaiB+dM4Fp+oeiGienEQl64NJ7QFxRb/h96J0iTL3Vp8+8Y?= + =?us-ascii?Q?=3D?= +Content-Type: multipart/alternative; + boundary="_000_PAXP193MB2364F1160B9E7C559D7D897ABB489PAXP193MB2364EURP_" +MIME-Version: 1.0 +X-OriginatorOrg: sct-15-20-4755-11-msonline-outlook-80ceb.templateTenant +X-MS-Exchange-CrossTenant-AuthAs: Internal +X-MS-Exchange-CrossTenant-AuthSource: PAXP193MB2364.EURP193.PROD.OUTLOOK.COM +X-MS-Exchange-CrossTenant-RMS-PersistedConsumerOrg: 00000000-0000-0000-0000-000000000000 +X-MS-Exchange-CrossTenant-Network-Message-Id: 7fbd6771-9a0d-495d-c4d3-08db61f714a6 +X-MS-Exchange-CrossTenant-originalarrivaltime: 31 May 2023 16:50:03.3982 + (UTC) +X-MS-Exchange-CrossTenant-fromentityheader: Hosted +X-MS-Exchange-CrossTenant-id: 84df9e7f-e9f6-40af-b435-aaaaaaaaaaaa +X-MS-Exchange-CrossTenant-rms-persistedconsumerorg: 00000000-0000-0000-0000-000000000000 +X-MS-Exchange-Transport-CrossTenantHeadersStamped: DB8P193MB0773 +X-Spam-Status: No, score=-1.6 +X-Spam-Score: -15 +X-Spam-Bar: - +X-Ham-Report: Spam detection software, running on the system "ion.dnsprotect.com", + has NOT identified this incoming email as spam. The original + message has been attached to this so you can view it or label + similar future email. If you have any questions, see + root\@localhost for details. + Content preview: Here is my reply to your reply. Tony Marston From: gmx@tonymarston.co.uk + Sent: 31 May 2023 17:46 To: Tony Marston + Subject: Re: TEST MESSAGE + Content analysis details: (-1.6 points, 8.0 required) + pts rule name description + ---- ---------------------- -------------------------------------------------- + -1.9 BAYES_00 BODY: Bayes spam probability is 0 to 1% + [score: 0.0005] + 0.0 FREEMAIL_FROM Sender email is commonly abused enduser mail + provider + [tonymarston[at]hotmail.com] + -0.0 SPF_PASS SPF: sender matches SPF record + 0.5 SUBJ_ALL_CAPS Subject is all capitals + -0.0 SPF_HELO_PASS SPF: HELO matches SPF record + 0.0 HTML_MESSAGE BODY: HTML included in message + -0.1 DKIM_VALID_AU Message has a valid DKIM or DK signature from + author's domain + 0.1 DKIM_SIGNED Message has a DKIM or DK signature, not necessarily + valid + -0.1 DKIM_VALID_EF Message has a valid DKIM or DK signature from + envelope-from domain + -0.1 DKIM_VALID Message has at least one valid DKIM or DK signature + -0.0 T_SCC_BODY_TEXT_LINE No description available. +X-Spam-Flag: NO +X-From-Rewrite: unmodified, no actual sender determined from check mail permissions + +--_000_PAXP193MB2364F1160B9E7C559D7D897ABB489PAXP193MB2364EURP_ +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Here is my reply to your reply. + +Tony Marston + +From: gmx@tonymarston.co.uk +Sent: 31 May 2023 17:46 +To: Tony Marston +Subject: Re: TEST MESSAGE + +On 2023-05-25 13:06, Tony Marston wrote: +Here is my reply to your message +> Tony Marston + + +--_000_PAXP193MB2364F1160B9E7C559D7D897ABB489PAXP193MB2364EURP_ +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + + + + + + + + +
+

Here is my reply to your reply.

+

 

+

Tony Marston

+

 

+
+

From: gmx@tonymarston.co.uk
+Sent: 31 May 2023 17:46
+To: Tony Marston
+Subject: Re: TEST MESSAGE

+
+

 

+

On 2023-05-25 13:06, Tony Marston wrote:
+Here is my reply to your message
+> Tony Marston

+

 

+
+ + + +--_000_PAXP193MB2364F1160B9E7C559D7D897ABB489PAXP193MB2364EURP_-- \ No newline at end of file From 50b4ea1e090f3718ff615d0ec5a1a0296f36c3b2 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 21:34:54 +0200 Subject: [PATCH 111/203] Protocol exception handling improved (bad response message added) #408 --- CHANGELOG.md | 1 + src/Connection/Protocols/ImapProtocol.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1a0f0b1..e50b541e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Legacy protocol support fixed (object to array conversion) #411 - Header value decoding improved #410 +- Protocol exception handling improved (bad response message added) #408 ### Added - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 914347b6..b5d84b52 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -303,10 +303,10 @@ public function readResponse(Response $response, string $tag, bool $dontParse = if ($tokens[0] == 'OK') { return $lines ?: [true]; } elseif ($tokens[0] == 'NO' || $tokens[0] == 'BAD' || $tokens[0] == 'BYE') { - throw new ImapServerErrorException(); + throw new ImapServerErrorException(implode("\n", $tokens)); } - throw new ImapBadRequestException(); + throw new ImapBadRequestException(implode("\n", $tokens)); } /** From 9f02c8417c638b3200a407c70a1bf219f4647515 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 22:18:54 +0200 Subject: [PATCH 112/203] Prevent fetching singular rfc partials from running indefinitely #407 --- CHANGELOG.md | 1 + src/Connection/Protocols/ImapProtocol.php | 15 +++--- tests/issues/Issue407Test.php | 57 +++++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 tests/issues/Issue407Test.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e50b541e..6e4dfd5e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Legacy protocol support fixed (object to array conversion) #411 - Header value decoding improved #410 - Protocol exception handling improved (bad response message added) #408 +- Prevent fetching singular rfc partials from running indefinitely #407 ### Added - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index b5d84b52..3270fa97 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -294,19 +294,22 @@ public function readResponse(Response $response, string $tag, bool $dontParse = $lines[] = $tokens; } while (!$readAll); + $original = $tokens; if ($dontParse) { // First two chars are still needed for the response code $tokens = [substr($tokens, 0, 2)]; } + $original = is_array($original)?$original : [$original]; + // last line has response code if ($tokens[0] == 'OK') { return $lines ?: [true]; } elseif ($tokens[0] == 'NO' || $tokens[0] == 'BAD' || $tokens[0] == 'BYE') { - throw new ImapServerErrorException(implode("\n", $tokens)); + throw new ImapServerErrorException(implode("\n", $original)); } - throw new ImapBadRequestException(implode("\n", $tokens)); + throw new ImapBadRequestException(implode("\n", $original)); } /** @@ -730,7 +733,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in * @throws RuntimeException */ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["$rfc.TEXT"], $uids, null, $uid); + return $this->fetch(["$rfc.TEXT"], is_array($uids)?$uids:[$uids], null, $uid); } /** @@ -744,7 +747,7 @@ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid * @throws RuntimeException */ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["$rfc.HEADER"], $uids, null, $uid); + return $this->fetch(["$rfc.HEADER"], is_array($uids)?$uids:[$uids], null, $uid); } /** @@ -757,7 +760,7 @@ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid * @throws RuntimeException */ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["FLAGS"], $uids, null, $uid); + return $this->fetch(["FLAGS"], is_array($uids)?$uids:[$uids], null, $uid); } /** @@ -770,7 +773,7 @@ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response * @throws RuntimeException */ public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["RFC822.SIZE"], $uids, null, $uid); + return $this->fetch(["RFC822.SIZE"], is_array($uids)?$uids:[$uids], null, $uid); } /** diff --git a/tests/issues/Issue407Test.php b/tests/issues/Issue407Test.php new file mode 100644 index 00000000..4adcbb39 --- /dev/null +++ b/tests/issues/Issue407Test.php @@ -0,0 +1,57 @@ +getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $message = $this->appendMessageTemplate($folder, "plain.eml"); + self::assertInstanceOf(Message::class, $message); + + $message->setFlag("Seen"); + + $flags = $this->getClient()->getConnection()->flags($message->uid, IMAP::ST_UID)->validatedData(); + + self::assertIsArray($flags); + self::assertSame(1, count($flags)); + self::assertSame("\\Seen", $flags[$message->uid][0]); + + $message->delete(); + } + +} \ No newline at end of file From f1d82d9f69cfd3bcf326467a966e214e05f07a74 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 22:51:09 +0200 Subject: [PATCH 113/203] Subject with colon ";" is truncated #401 --- CHANGELOG.md | 1 + src/Header.php | 2 +- tests/issues/Issue401Test.php | 27 +++++++++++++++++++++++++++ tests/messages/issue-401.eml | 9 +++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 tests/issues/Issue401Test.php create mode 100644 tests/messages/issue-401.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e4dfd5e..84a52d29 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Header value decoding improved #410 - Protocol exception handling improved (bad response message added) #408 - Prevent fetching singular rfc partials from running indefinitely #407 +- Subject with colon ";" is truncated #401 ### Added - NaN diff --git a/src/Header.php b/src/Header.php index e9ff162b..6a80412a 100644 --- a/src/Header.php +++ b/src/Header.php @@ -612,7 +612,7 @@ private function extractHeaderExtensions(): void { $value = (string)$value; } // Only parse strings and don't parse any attributes like the user-agent - if (($key == "user_agent") === false) { + if (!in_array($key, ["user-agent", "subject"])) { if (($pos = strpos($value, ";")) !== false) { $original = substr($value, 0, $pos); $this->set($key, trim(rtrim($original))); diff --git a/tests/issues/Issue401Test.php b/tests/issues/Issue401Test.php new file mode 100644 index 00000000..e250a550 --- /dev/null +++ b/tests/issues/Issue401Test.php @@ -0,0 +1,27 @@ +subject); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-401.eml b/tests/messages/issue-401.eml new file mode 100644 index 00000000..386fdad8 --- /dev/null +++ b/tests/messages/issue-401.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: 1;00pm Client running few minutes late +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file From 8ff2782565cc6f115fce568c01fe4664b3a8ad19 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 23 Jun 2023 23:04:02 +0200 Subject: [PATCH 114/203] Catching and handling iconv decoding exception #397 --- CHANGELOG.md | 1 + src/Message.php | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84a52d29..da9697a5 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Protocol exception handling improved (bad response message added) #408 - Prevent fetching singular rfc partials from running indefinitely #407 - Subject with colon ";" is truncated #401 +- Catching and handling iconv decoding exception #397 ### Added - NaN diff --git a/src/Message.php b/src/Message.php index 47562cb7..2a60c641 100755 --- a/src/Message.php +++ b/src/Message.php @@ -903,7 +903,11 @@ public function convertEncoding($str, string $from = "ISO-8859-2", string $to = } if (function_exists('iconv') && !EncodingAliases::isUtf7($from) && !EncodingAliases::isUtf7($to)) { - return @iconv($from, $to . '//IGNORE', $str); + try { + return iconv($from, $to.'//IGNORE', $str); + } catch (\Exception $e) { + return @iconv($from, $to, $str); + } } else { if (!$from) { return mb_convert_encoding($str, $to); From 4bbcb4ba0f1c0b89e9660300175e5b94b20e760f Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 24 Jun 2023 00:38:16 +0200 Subject: [PATCH 115/203] Additional timestamp format added #392 --- CHANGELOG.md | 2 +- src/Header.php | 5 +++++ tests/fixtures/DateTemplateTest.php | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da9697a5..50233901 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Catching and handling iconv decoding exception #397 ### Added -- NaN +- Additional timestamp format added #392 (thanks @esk-ap) ### Breaking changes - NaN diff --git a/src/Header.php b/src/Header.php index 6a80412a..c87a8d09 100644 --- a/src/Header.php +++ b/src/Header.php @@ -716,6 +716,11 @@ private function parseDate(object $header): void { array_splice($parts, -2); $date = implode(' ', $parts); break; + case preg_match('/([A-Z]{2,4}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0: + $array = explode(',', $date); + array_shift($array); + $date = Carbon::createFromFormat("d M Y H:i:s O", trim(implode(',', $array))); + break; case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: $date .= 'C'; diff --git a/tests/fixtures/DateTemplateTest.php b/tests/fixtures/DateTemplateTest.php index 4bf8f114..aaa338aa 100644 --- a/tests/fixtures/DateTemplateTest.php +++ b/tests/fixtures/DateTemplateTest.php @@ -48,6 +48,7 @@ class DateTemplateTest extends FixtureTestCase { "14 Sep 2019 00:10:08 UT +0200" => "2019-09-14 00:10:08", "Tue, 08 Nov 2022 18:47:20 +0000 14:03:33 +0000" => "2022-11-08 18:47:20", "Sat, 10, Dec 2022 09:35:19 +0100" => "2022-12-10 08:35:19", + "Thur, 16 Mar 2023 15:33:07 +0400" => "2023-03-16 11:33:07", ]; /** From dc237efb208d8892b9b119a70bf4d87a01e7e77a Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 24 Jun 2023 00:46:08 +0200 Subject: [PATCH 116/203] Test case for #382 added --- tests/issues/Issue382Test.php | 33 +++++++++++++++++++++++++++++++++ tests/messages/issue-382.eml | 9 +++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/issues/Issue382Test.php create mode 100644 tests/messages/issue-382.eml diff --git a/tests/issues/Issue382Test.php b/tests/issues/Issue382Test.php new file mode 100644 index 00000000..4e638827 --- /dev/null +++ b/tests/issues/Issue382Test.php @@ -0,0 +1,33 @@ +from->first(); + + self::assertSame("Mail Delivery System", $from->personal); + self::assertSame("MAILER-DAEMON", $from->mailbox); + self::assertSame("mta-09.someserver.com", $from->host); + self::assertSame("MAILER-DAEMON@mta-09.someserver.com", $from->mail); + self::assertSame("Mail Delivery System ", $from->full); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-382.eml b/tests/messages/issue-382.eml new file mode 100644 index 00000000..f34b89fc --- /dev/null +++ b/tests/messages/issue-382.eml @@ -0,0 +1,9 @@ +From: MAILER-DAEMON@mta-09.someserver.com (Mail Delivery System) +To: to@here.com +Subject: Test +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file From d8880dff606ea7451ca88b6e0539ff6bc3d59320 Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 24 Jun 2023 01:36:12 +0200 Subject: [PATCH 117/203] Additional timestamp formats added #198 --- CHANGELOG.md | 2 +- src/Header.php | 21 +++++++++++++++++++++ tests/fixtures/DateTemplateTest.php | 2 ++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50233901..8ebab514 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Catching and handling iconv decoding exception #397 ### Added -- Additional timestamp format added #392 (thanks @esk-ap) +- Additional timestamp formats added #198 #392 (thanks @esk-ap) ### Breaking changes - NaN diff --git a/src/Header.php b/src/Header.php index c87a8d09..9c8ae046 100644 --- a/src/Header.php +++ b/src/Header.php @@ -728,6 +728,27 @@ private function parseDate(object $header): void { case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}[\,]\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0: $date = str_replace(',', '', $date); break; + // match case for: Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)/Di., 15 Feb. 2022 06:52:44 +0100 (MEZ) + case preg_match('/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \([A-Z]{3,4}\))\/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \([A-Z]{3,4}\))+$/i', $date) > 0: + $dates = explode('/', $date); + $date = array_shift($dates); + $array = explode(',', $date); + array_shift($array); + $date = trim(implode(',', $array)); + $array = explode(' ', $date); + array_pop($array); + $date = trim(implode(' ', $array)); + $date = Carbon::createFromFormat("d M. Y H:i:s O", $date); + break; + // match case for: fr., 25 nov. 2022 06:27:14 +0100/fr., 25 nov. 2022 06:27:14 +0100 + case preg_match('/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})\/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0: + $dates = explode('/', $date); + $date = array_shift($dates); + $array = explode(',', $date); + array_shift($array); + $date = trim(implode(',', $array)); + $date = Carbon::createFromFormat("d M. Y H:i:s O", $date); + break; case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ \+[0-9]{2,4}\ \(\+[0-9]{1,2}\))+$/i', $date) > 0: case preg_match('/([A-Z]{2,3}[\,|\ \,]\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}.*)+$/i', $date) > 0: case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \(.*)\)+$/i', $date) > 0: diff --git a/tests/fixtures/DateTemplateTest.php b/tests/fixtures/DateTemplateTest.php index aaa338aa..79f42dfe 100644 --- a/tests/fixtures/DateTemplateTest.php +++ b/tests/fixtures/DateTemplateTest.php @@ -49,6 +49,8 @@ class DateTemplateTest extends FixtureTestCase { "Tue, 08 Nov 2022 18:47:20 +0000 14:03:33 +0000" => "2022-11-08 18:47:20", "Sat, 10, Dec 2022 09:35:19 +0100" => "2022-12-10 08:35:19", "Thur, 16 Mar 2023 15:33:07 +0400" => "2023-03-16 11:33:07", + "fr., 25 nov. 2022 06:27:14 +0100/fr., 25 nov. 2022 06:27:14 +0100" => "2022-11-25 05:27:14", + "Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)/Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)" => "2022-02-15 05:52:44", ]; /** From 829a6e6fe84b6c713858d227c24a5d01a3ccefca Mon Sep 17 00:00:00 2001 From: webklex Date: Sat, 24 Jun 2023 03:46:07 +0200 Subject: [PATCH 118/203] Changelog updated --- CHANGELOG.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ebab514..3edc4efb 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Breaking changes +- NaN + + +## [5.4.0] - 2023-06-24 +### Fixed - Legacy protocol support fixed (object to array conversion) #411 - Header value decoding improved #410 - Protocol exception handling improved (bad response message added) #408 @@ -16,9 +27,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Additional timestamp formats added #198 #392 (thanks @esk-ap) -### Breaking changes -- NaN - ## [5.3.0] - Security patch - 2023-06-20 ### Fixed From 29245c8c99b58af71b7c5cc3e92714cd10690378 Mon Sep 17 00:00:00 2001 From: webklex Date: Sun, 25 Jun 2023 22:07:09 +0200 Subject: [PATCH 119/203] #413 live test added & sample readded --- tests/issues/Issue413Test.php | 54 ++++++++++++++++++++++++++++++++++- tests/messages/issue-413.eml | 2 +- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/tests/issues/Issue413Test.php b/tests/issues/Issue413Test.php index 22cc881b..f3bd2212 100644 --- a/tests/issues/Issue413Test.php +++ b/tests/issues/Issue413Test.php @@ -13,10 +13,62 @@ namespace Tests\issues; use PHPUnit\Framework\TestCase; +use Tests\live\LiveMailboxTestCase; +use Webklex\PHPIMAP\Folder; use Webklex\PHPIMAP\Message; -class Issue413Test extends TestCase { +class Issue413Test extends LiveMailboxTestCase { + /** + * Live server test + * + * @return void + * @throws \Webklex\PHPIMAP\Exceptions\AuthFailedException + * @throws \Webklex\PHPIMAP\Exceptions\ConnectionFailedException + * @throws \Webklex\PHPIMAP\Exceptions\EventNotFoundException + * @throws \Webklex\PHPIMAP\Exceptions\FolderFetchingException + * @throws \Webklex\PHPIMAP\Exceptions\GetMessagesFailedException + * @throws \Webklex\PHPIMAP\Exceptions\ImapBadRequestException + * @throws \Webklex\PHPIMAP\Exceptions\ImapServerErrorException + * @throws \Webklex\PHPIMAP\Exceptions\InvalidMessageDateException + * @throws \Webklex\PHPIMAP\Exceptions\MaskNotFoundException + * @throws \Webklex\PHPIMAP\Exceptions\MessageContentFetchingException + * @throws \Webklex\PHPIMAP\Exceptions\MessageFlagException + * @throws \Webklex\PHPIMAP\Exceptions\MessageHeaderFetchingException + * @throws \Webklex\PHPIMAP\Exceptions\MessageNotFoundException + * @throws \Webklex\PHPIMAP\Exceptions\ResponseException + * @throws \Webklex\PHPIMAP\Exceptions\RuntimeException + */ + public function testLiveIssueEmail() { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $_message = $this->appendMessageTemplate($folder, 'issue-413.eml'); + + $message = $folder->messages()->all()->get()->last(); + self::assertEquals($message->uid, $_message->uid); + + self::assertSame("Test Message", (string)$message->subject); + self::assertSame("This is just a test, so ignore it (if you can!)\r\n\r\nTony Marston", $message->getTextBody()); + + $message->delete(); + } + + /** + * Static parsing test + * + * @return void + * @throws \ReflectionException + * @throws \Webklex\PHPIMAP\Exceptions\AuthFailedException + * @throws \Webklex\PHPIMAP\Exceptions\ConnectionFailedException + * @throws \Webklex\PHPIMAP\Exceptions\ImapBadRequestException + * @throws \Webklex\PHPIMAP\Exceptions\ImapServerErrorException + * @throws \Webklex\PHPIMAP\Exceptions\InvalidMessageDateException + * @throws \Webklex\PHPIMAP\Exceptions\MaskNotFoundException + * @throws \Webklex\PHPIMAP\Exceptions\MessageContentFetchingException + * @throws \Webklex\PHPIMAP\Exceptions\ResponseException + * @throws \Webklex\PHPIMAP\Exceptions\RuntimeException + */ public function testIssueEmail() { $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-413.eml"]); $message = Message::fromFile($filename); diff --git a/tests/messages/issue-413.eml b/tests/messages/issue-413.eml index a01412b3..9f0cbfa2 100644 --- a/tests/messages/issue-413.eml +++ b/tests/messages/issue-413.eml @@ -29,4 +29,4 @@ X-From-Rewrite: unmodified, already matched This is just a test, so ignore it (if you can!) -Tony Marston \ No newline at end of file +Tony Marston From f8f17f69693d57138888fb0ca8fdc430ed21d018 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 27 Jun 2023 01:50:27 +0200 Subject: [PATCH 120/203] #413 live test adjusted to fit the provided example code --- tests/issues/Issue413Test.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/issues/Issue413Test.php b/tests/issues/Issue413Test.php index f3bd2212..cfdae548 100644 --- a/tests/issues/Issue413Test.php +++ b/tests/issues/Issue413Test.php @@ -43,9 +43,10 @@ public function testLiveIssueEmail() { $folder = $this->getFolder('INBOX'); self::assertInstanceOf(Folder::class, $folder); + /** @var Message $message */ $_message = $this->appendMessageTemplate($folder, 'issue-413.eml'); - $message = $folder->messages()->all()->get()->last(); + $message = $folder->messages()->getMessageByMsgn($_message->msgn); self::assertEquals($message->uid, $_message->uid); self::assertSame("Test Message", (string)$message->subject); From 510c4eaf1074140e92931d77285be890de02d1f0 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 27 Jun 2023 01:51:59 +0200 Subject: [PATCH 121/203] Test case extended to include attachment filename decoding #410 --- tests/issues/Issue410Test.php | 8 ++++++++ tests/messages/issue-410.eml | 15 +++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/tests/issues/Issue410Test.php b/tests/issues/Issue410Test.php index 7c3e6dc0..068cb7df 100644 --- a/tests/issues/Issue410Test.php +++ b/tests/issues/Issue410Test.php @@ -22,6 +22,14 @@ public function testIssueEmail() { $message = Message::fromFile($filename); self::assertSame("☆第132号 「ガーデン&エクステリア」専門店のためのQ&Aサロン 【月刊エクステリア・ワーク】", (string)$message->subject); + + $attachments = $message->getAttachments(); + + self::assertSame(1, $attachments->count()); + + $attachment = $attachments->first(); + self::assertSame("☆第132号 「ガーデン&エクステリア」専門店のためのQ&Aサロン 【月刊エクステリア・ワーク】", $attachment->filename); + self::assertSame("☆第132号 「ガーデン&エクステリア」専門店のためのQ&Aサロン 【月刊エクステリア・ワーク】", $attachment->name); } } \ No newline at end of file diff --git a/tests/messages/issue-410.eml b/tests/messages/issue-410.eml index 05ddeff7..e5fe0c62 100644 --- a/tests/messages/issue-410.eml +++ b/tests/messages/issue-410.eml @@ -2,8 +2,15 @@ From: from@there.com To: to@here.com Subject: =?ISO-2022-JP?B?GyRCIXlCaBsoQjEzMhskQjlmISEhViUsITwlRyVzGyhCJhskQiUoJS8lOSVGJWolIiFXQGxMZ0U5JE4kPyRhJE4jURsoQiYbJEIjQSU1JW0lcyEhIVo3bjQpJSglLyU5JUYlaiUiISYlbyE8JS8hWxsoQg==?= Date: Wed, 13 Sep 2017 13:05:45 +0200 -Content-Type: text/plain; - charset="us-ascii" -Content-Transfer-Encoding: quoted-printable +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------B832AF745285AEEC6D5AEE42" -Hi \ No newline at end of file +Hi +--------------B832AF745285AEEC6D5AEE42 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="=?ISO-2022-JP?B?GyRCIXlCaBsoQjEzMhskQjlmISEhViUsITwlRyVzGyhCJhskQiUoJS8lOSVGJWolIiFXQGxMZ0U5JE4kPyRhJE4jURsoQiYbJEIjQSU1JW0lcyEhIVo3bjQpJSglLyU5JUYlaiUiISYlbyE8JS8hWxsoQg==?=" + +SGkh +--------------B832AF745285AEEC6D5AEE42-- \ No newline at end of file From 5ee4990435ea92dcfb780c57d7b2a12fe8b8433f Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 27 Jun 2023 02:32:07 +0200 Subject: [PATCH 122/203] Error token length mismatch in readResponse #400 --- CHANGELOG.md | 2 +- src/Connection/Protocols/ImapProtocol.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3edc4efb..74d573ab 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Error token length mismatch in `ImapProtocol::readResponse` #400 ### Added - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 3270fa97..4d54579f 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -297,7 +297,7 @@ public function readResponse(Response $response, string $tag, bool $dontParse = $original = $tokens; if ($dontParse) { // First two chars are still needed for the response code - $tokens = [substr($tokens, 0, 2)]; + $tokens = [trim(substr($tokens, 0, 3))]; } $original = is_array($original)?$original : [$original]; From 57e7d287ab4239295b50138eea2c2edeabc59539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Wed, 28 Jun 2023 03:42:20 +0200 Subject: [PATCH 123/203] add failing test (#421) --- tests/issues/Issue410Test.php | 16 ++++++++++++++++ tests/messages/issue-410b.eml | 22 ++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/messages/issue-410b.eml diff --git a/tests/issues/Issue410Test.php b/tests/issues/Issue410Test.php index 068cb7df..b300ec7a 100644 --- a/tests/issues/Issue410Test.php +++ b/tests/issues/Issue410Test.php @@ -13,6 +13,7 @@ namespace Tests\issues; use PHPUnit\Framework\TestCase; +use Webklex\PHPIMAP\ClientManager; use Webklex\PHPIMAP\Message; class Issue410Test extends TestCase { @@ -32,4 +33,19 @@ public function testIssueEmail() { self::assertSame("☆第132号 「ガーデン&エクステリア」専門店のためのQ&Aサロン 【月刊エクステリア・ワーク】", $attachment->name); } + public function testIssueEmailB() { + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-410b.eml"]); + $message = Message::fromFile($filename); + + self::assertSame("386 - 400021804 - 19., Heiligenstädter Straße 80 - 0819306 - Anfrage Vergabevorschlag", (string)$message->subject); + + $attachments = $message->getAttachments(); + + self::assertSame(1, $attachments->count()); + + $attachment = $attachments->first(); + self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->filename); + self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->name); + } + } \ No newline at end of file diff --git a/tests/messages/issue-410b.eml b/tests/messages/issue-410b.eml new file mode 100644 index 00000000..b260a1e0 --- /dev/null +++ b/tests/messages/issue-410b.eml @@ -0,0 +1,22 @@ +From: from@there.com +To: to@here.com +Subject: =?iso-8859-1?Q?386_-_400021804_-_19.,_Heiligenst=E4dter_Stra=DFe_80_-_081?= + =?iso-8859-1?Q?9306_-_Anfrage_Vergabevorschlag?= +Date: Wed, 13 Sep 2017 13:05:45 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------B832AF745285AEEC6D5AEE42" + +Hi +--------------B832AF745285AEEC6D5AEE42 +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="=?iso-8859-1?Q?2021=5FM=E4ngelliste=5F0819306.xlsx?=" +Content-Description: =?iso-8859-1?Q?2021=5FM=E4ngelliste=5F0819306.xlsx?= +Content-Disposition: attachment; + filename="=?iso-8859-1?Q?2021=5FM=E4ngelliste=5F0819306.xlsx?="; size=11641; + creation-date="Mon, 10 Jan 2022 09:01:00 GMT"; + modification-date="Mon, 10 Jan 2022 09:01:00 GMT" +Content-Transfer-Encoding: base64 + +SGkh +--------------B832AF745285AEEC6D5AEE42-- \ No newline at end of file From 25fa1a067c670010553799873bab75ad0b866155 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:46:21 +0200 Subject: [PATCH 124/203] Attachment name parsing fixed #410 #421 --- src/Attachment.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index fcbc54e6..85ea62fb 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -269,12 +269,10 @@ public function decodeName(?string $name): string { } $decoder = $this->config['decoder']['message']; - if($decoder === 'utf-8' && extension_loaded('imap')) { - $name = \imap_utf8($name); - } - if (preg_match('/=\?([^?]+)\?(Q|B)\?(.+)\?=/i', $name, $matches)) { $name = $this->part->getHeader()->decode($name); + } elseif ($decoder === 'utf-8' && extension_loaded('imap')) { + $name = \imap_utf8($name); } // check if $name is url encoded @@ -284,9 +282,7 @@ public function decodeName(?string $name): string { // sanitize $name // order of '..' is important - $name = str_replace(['\\', '/', chr(0), ':', '..'], '', $name); - - return $name; + return str_replace(['\\', '/', chr(0), ':', '..'], '', $name); } return ""; } From 9d0377e4cdf584ad68a1027b3a237dc79231339e Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:47:53 +0200 Subject: [PATCH 125/203] Attachment content hash added --- src/Attachment.php | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index 85ea62fb..5a918b35 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -28,6 +28,7 @@ * @property string type * @property string content_type * @property string id + * @property string hash * @property string name * @property string description * @property string filename @@ -44,6 +45,8 @@ * @method string setContentType(string $content_type) * @method string getId() * @method string setId(string $id) + * @method string getHash() + * @method string setHash(string $hash) * @method string getSize() * @method string setSize(integer $size) * @method string getName() @@ -74,17 +77,18 @@ class Attachment { * @var array $attributes */ protected array $attributes = [ - 'content' => null, - 'type' => null, - 'part_number' => 0, + 'content' => null, + 'hash' => null, + 'type' => null, + 'part_number' => 0, 'content_type' => null, - 'id' => null, - 'name' => null, - 'filename' => null, - 'description' => null, - 'disposition' => null, - 'img_src' => null, - 'size' => null, + 'id' => null, + 'name' => null, + 'filename' => null, + 'description' => null, + 'disposition' => null, + 'img_src' => null, + 'size' => null, ]; /** @@ -203,10 +207,26 @@ protected function fetch(): void { $this->content_type = $this->part->content_type; $this->content = $this->oMessage->decodeString($content, $this->part->encoding); + // Create a hash of the raw part - this can be used to identify the attachment in the message context. However, + // it is not guaranteed to be unique and collisions are possible. + // Some additional online resources: + // - https://en.wikipedia.org/wiki/Hash_collision + // - https://www.php.net/manual/en/function.hash.php + // - https://php.watch/articles/php-hash-benchmark + // Benchmark speeds: + // -xxh3 ~15.19(GB/s) (requires php-xxhash extension or >= php8.1) + // -crc32c ~14.12(GB/s) + // -sha256 ~0.25(GB/s) + // xxh3 would be nice to use, because of its extra speed and 32 instead of 8 bytes, but it is not compatible with + // php < 8.1. crc32c is the next fastest and is compatible with php >= 5.1. sha256 is the slowest, but is compatible + // with php >= 5.1 and is the most likely to be unique. crc32c is the best compromise between speed and uniqueness. + // Unique enough for our purposes, but not so slow that it could be a bottleneck. + $this->hash = hash("crc32c", $this->part->getHeader()->raw."\r\n\r\n".$this->part->content); + if (($id = $this->part->id) !== null) { $this->id = str_replace(['<', '>'], '', $id); - }else{ - $this->id = hash("sha256", uniqid((string) rand(10000, 99999), true)); + }else { + $this->id = $this->hash; } $this->size = $this->part->bytes; From 141dc105dc1272e1ce0cb0aa8fa13a3be46d2faa Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:48:36 +0200 Subject: [PATCH 126/203] Always parse the attachment description if it is available --- src/Attachment.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Attachment.php b/src/Attachment.php index 5a918b35..8c217186 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -236,6 +236,10 @@ protected function fetch(): void { $this->filename = $this->decodeName($filename); } + if (($description = $this->part->description) !== null) { + $this->description = $this->part->getHeader()->decode($description); + } + if (($name = $this->part->name) !== null) { $this->name = $this->decodeName($name); } @@ -248,7 +252,6 @@ protected function fetch(): void { if (!$this->name) { $this->name = $this->part->description; } - $this->description = $this->part->description; } else if (!$this->name) { $this->name = $this->part->subtype; } From b8484ba199315ca92580ebd0807e173ad80c7a4d Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:49:56 +0200 Subject: [PATCH 127/203] Additional Attachment name fallback added to prevent missing attachments --- src/Attachment.php | 11 ++++++----- src/Message.php | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index 8c217186..899b4e69 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -243,9 +243,6 @@ protected function fetch(): void { if (($name = $this->part->name) !== null) { $this->name = $this->decodeName($name); } - if (!$this->name && $this->filename != "") { - $this->name = $this->filename; - } if (IMAP::ATTACHMENT_TYPE_MESSAGE == $this->part->type) { if ($this->part->ifdescription) { @@ -258,8 +255,12 @@ protected function fetch(): void { } $this->attributes = array_merge($this->part->getHeader()->getAttributes(), $this->attributes); - if(!$this->filename){ - $this->filename = bin2hex(random_bytes(10)); + if (!$this->filename) { + $this->filename = $this->hash; + } + + if (!$this->name && $this->filename != "") { + $this->name = $this->filename; } } diff --git a/src/Message.php b/src/Message.php index 2a60c641..09a534f2 100755 --- a/src/Message.php +++ b/src/Message.php @@ -767,7 +767,7 @@ protected function addBody(string $subtype, string $content): void { protected function fetchAttachment(Part $part): void { $oAttachment = new Attachment($this, $part); - if ($oAttachment->getName() !== null && $oAttachment->getSize() > 0) { + if ($oAttachment->getSize() > 0) { if ($oAttachment->getId() !== null && $this->attachments->offsetExists($oAttachment->getId())) { $this->attachments->put($oAttachment->getId(), $oAttachment); } else { From 8e69ee69fe0652de3b6da3c6f63f68719f3d7ded Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:50:22 +0200 Subject: [PATCH 128/203] Test case for #414 added --- tests/issues/Issue414Test.php | 43 +++++++++++++++++++++++++++++++++++ tests/messages/issue-414.eml | 27 ++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 tests/issues/Issue414Test.php create mode 100644 tests/messages/issue-414.eml diff --git a/tests/issues/Issue414Test.php b/tests/issues/Issue414Test.php new file mode 100644 index 00000000..36a1d300 --- /dev/null +++ b/tests/issues/Issue414Test.php @@ -0,0 +1,43 @@ +subject); + + $attachments = $message->getAttachments(); + + self::assertSame(2, $attachments->count()); + + $attachment = $attachments->first(); + self::assertEmpty($attachment->description); + self::assertSame("exampleMyFile.txt", $attachment->filename); + self::assertSame("exampleMyFile.txt", $attachment->name); + self::assertSame("be62f7e6", $attachment->id); + + $attachment = $attachments->last(); + self::assertEmpty($attachment->description); + self::assertSame("phpfoo", $attachment->filename); + self::assertSame("phpfoo", $attachment->name); + self::assertSame("12e1d38b", $attachment->hash); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-414.eml b/tests/messages/issue-414.eml new file mode 100644 index 00000000..302e4492 --- /dev/null +++ b/tests/messages/issue-414.eml @@ -0,0 +1,27 @@ +From: from@there.com +To: to@here.com +Subject: Test +Date: Fri, 29 Sep 2017 10:55:23 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------5B1F217006A67C28E756A62E" + +This is a multi-part message in MIME format. + +--------------5B1F217006A67C28E756A62E +Content-Type: text/plain; charset=UTF-8; + name="../../example/MyFile.txt" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="../../example/MyFile.txt" + +TXlGaWxlQ29udGVudA== +--------------5B1F217006A67C28E756A62E +Content-Type: text/plain; charset=UTF-8; + name="php://foo" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="php://foo" + +TXlGaWxlQ29udGVudA== +--------------5B1F217006A67C28E756A62E-- From abc9cb3367a3a7b76d31a94c880538942d6193dd Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:51:00 +0200 Subject: [PATCH 129/203] test updated and extended --- tests/fixtures/AttachmentLongFilenameTest.php | 4 ++-- tests/fixtures/AttachmentNoDispositionTest.php | 4 +++- .../EmbeddedEmailWithoutContentDispositionTest.php | 2 +- tests/fixtures/ExampleBounceTest.php | 8 ++++---- tests/fixtures/InlineAttachmentTest.php | 6 ++++-- tests/issues/Issue410Test.php | 1 + 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/fixtures/AttachmentLongFilenameTest.php b/tests/fixtures/AttachmentLongFilenameTest.php index c9e67d15..650a2dcb 100644 --- a/tests/fixtures/AttachmentLongFilenameTest.php +++ b/tests/fixtures/AttachmentLongFilenameTest.php @@ -52,7 +52,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals('01_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); + self::assertEquals('01_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); self::assertEquals("f7b5181985862431bfc443d26e3af2371e20a0afd676eeb9b9595a26d42e0b73", hash("sha256", $attachment->filename)); self::assertEquals('text', $attachment->type); self::assertEquals('txt', $attachment->getExtension()); @@ -65,7 +65,7 @@ public function testFixture() : void { $attachment = $attachments[2]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals('02_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); + self::assertEquals('02_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); self::assertEquals('02_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->filename); self::assertEquals('text', $attachment->type); self::assertEquals("text/plain", $attachment->content_type); diff --git a/tests/fixtures/AttachmentNoDispositionTest.php b/tests/fixtures/AttachmentNoDispositionTest.php index 76b3c12b..aa1ef946 100644 --- a/tests/fixtures/AttachmentNoDispositionTest.php +++ b/tests/fixtures/AttachmentNoDispositionTest.php @@ -39,7 +39,8 @@ public function testFixture() : void { $attachment = $message->attachments()->first(); self::assertInstanceOf(Attachment::class, $attachment); - self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); + self::assertEquals('26ed3dd2', $attachment->filename); + self::assertEquals('26ed3dd2', $attachment->id); self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); self::assertEquals('text', $attachment->type); self::assertEquals('xls', $attachment->getExtension()); @@ -49,5 +50,6 @@ public function testFixture() : void { self::assertEquals(0, $attachment->part_number); self::assertNull($attachment->disposition); self::assertNotEmpty($attachment->id); + self::assertEmpty($attachment->content_id); } } \ No newline at end of file diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php index 67ad1cb0..603a956d 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -61,7 +61,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals('', $attachment->name); + self::assertEquals('a1abc19a', $attachment->name); self::assertEquals('text', $attachment->type); self::assertEquals('', $attachment->getExtension()); self::assertEquals("message/rfc822", $attachment->content_type); diff --git a/tests/fixtures/ExampleBounceTest.php b/tests/fixtures/ExampleBounceTest.php index 20288206..d2f418a9 100644 --- a/tests/fixtures/ExampleBounceTest.php +++ b/tests/fixtures/ExampleBounceTest.php @@ -72,8 +72,8 @@ public function testFixture(): void { $attachment = $attachments[0]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); - self::assertEquals("", $attachment->name); + self::assertEquals('c541a506', $attachment->filename); + self::assertEquals("c541a506", $attachment->name); self::assertEquals('', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("message/delivery-status", $attachment->content_type); @@ -85,8 +85,8 @@ public function testFixture(): void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); - self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); - self::assertEquals("", $attachment->name); + self::assertEquals('da786518', $attachment->filename); + self::assertEquals("da786518", $attachment->name); self::assertEquals('', $attachment->getExtension()); self::assertEquals('text', $attachment->type); self::assertEquals("message/rfc822", $attachment->content_type); diff --git a/tests/fixtures/InlineAttachmentTest.php b/tests/fixtures/InlineAttachmentTest.php index a482f25b..edb380cd 100644 --- a/tests/fixtures/InlineAttachmentTest.php +++ b/tests/fixtures/InlineAttachmentTest.php @@ -44,9 +44,11 @@ public function testFixture() : void { self::assertCount(1, $attachments); $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); - self::assertEquals('', $attachment->name); - self::assertMatchesRegularExpression('/^[a-z0-9]{20}$/', $attachment->filename); + self::assertEquals('d2913999', $attachment->name); + self::assertEquals('d2913999', $attachment->filename); + self::assertEquals('ii_15f0aad691bb745f', $attachment->id); self::assertEquals('text', $attachment->type); self::assertEquals('', $attachment->getExtension()); self::assertEquals("image/png", $attachment->content_type); diff --git a/tests/issues/Issue410Test.php b/tests/issues/Issue410Test.php index b300ec7a..d02724ca 100644 --- a/tests/issues/Issue410Test.php +++ b/tests/issues/Issue410Test.php @@ -44,6 +44,7 @@ public function testIssueEmailB() { self::assertSame(1, $attachments->count()); $attachment = $attachments->first(); + self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->description); self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->filename); self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->name); } From 7d7b897ea5219c34d7972cb99e878893b0a2c5f1 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:51:18 +0200 Subject: [PATCH 130/203] file formatted --- src/Attachment.php | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index 899b4e69..15b83f63 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -100,8 +100,8 @@ class Attachment { /** * Attachment constructor. - * @param Message $oMessage - * @param Part $part + * @param Message $oMessage + * @param Part $part */ public function __construct(Message $oMessage, Part $part) { $this->config = ClientManager::get('options'); @@ -112,13 +112,13 @@ public function __construct(Message $oMessage, Part $part) { if ($this->oMessage->getClient()) { $default_mask = $this->oMessage->getClient()?->getDefaultAttachmentMask(); - if($default_mask != null) { + if ($default_mask != null) { $this->mask = $default_mask; } - }else{ - $default_mask = ClientManager::getMask("attachment"); - if($default_mask != ""){ - $this->mask =$default_mask; + } else { + $default_mask = ClientManager::getMask("attachment"); + if ($default_mask != "") { + $this->mask = $default_mask; } } @@ -135,15 +135,15 @@ public function __construct(Message $oMessage, Part $part) { * @throws MethodNotFoundException */ public function __call(string $method, array $arguments) { - if(strtolower(substr($method, 0, 3)) === 'get') { + if (strtolower(substr($method, 0, 3)) === 'get') { $name = Str::snake(substr($method, 3)); - if(isset($this->attributes[$name])) { + if (isset($this->attributes[$name])) { return $this->attributes[$name]; } return null; - }elseif (strtolower(substr($method, 0, 3)) === 'set') { + } elseif (strtolower(substr($method, 0, 3)) === 'set') { $name = Str::snake(substr($method, 3)); $this->attributes[$name] = array_pop($arguments); @@ -151,7 +151,7 @@ public function __call(string $method, array $arguments) { return $this->attributes[$name]; } - throw new MethodNotFoundException("Method ".self::class.'::'.$method.'() is not supported'); + throw new MethodNotFoundException("Method " . self::class . '::' . $method . '() is not supported'); } /** @@ -174,7 +174,7 @@ public function __set($name, $value) { * @return mixed|null */ public function __get($name) { - if(isset($this->attributes[$name])) { + if (isset($this->attributes[$name])) { return $this->attributes[$name]; } @@ -274,7 +274,7 @@ protected function fetch(): void { public function save(string $path, ?string $filename = null): bool { $filename = $filename ? $this->decodeName($filename) : $this->filename; - return file_put_contents($path.DIRECTORY_SEPARATOR.$filename, $this->getContent()) !== false; + return file_put_contents($path . DIRECTORY_SEPARATOR . $filename, $this->getContent()) !== false; } /** @@ -335,7 +335,7 @@ public function getExtension(): ?string { } if ($extension === null) { $deprecated_guesser = "\Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser"; - if (class_exists($deprecated_guesser) !== false){ + if (class_exists($deprecated_guesser) !== false) { /** @var \Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser $deprecated_guesser */ $extension = $deprecated_guesser::getInstance()->guess($this->getMimeType()); } @@ -374,7 +374,7 @@ public function getMessage(): Message { * @return $this */ public function setMask($mask): Attachment { - if(class_exists($mask)){ + if (class_exists($mask)) { $this->mask = $mask; } @@ -399,10 +399,10 @@ public function getMask(): string { */ public function mask(string $mask = null): mixed { $mask = $mask !== null ? $mask : $this->mask; - if(class_exists($mask)){ + if (class_exists($mask)) { return new $mask($this); } - throw new MaskNotFoundException("Unknown mask provided: ".$mask); + throw new MaskNotFoundException("Unknown mask provided: " . $mask); } } From d286ead5b83052ed7417578cd492d0ba7f237b72 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:51:39 +0200 Subject: [PATCH 131/203] changelog updated --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74d573ab..56e5e211 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Error token length mismatch in `ImapProtocol::readResponse` #400 +- Attachment name parsing fixed #410 #421 (thanks @nuernbergerA) +- Additional Attachment name fallback added to prevent missing attachments +- Attachment id is now static (based on the raw part content) and now longer random +- Always parse the attachment description if it is available ### Added -- NaN +- Attachment content hash added ### Breaking changes - NaN From 3c23c8f66b772ce8597772816e068326559e7e4b Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 28 Jun 2023 03:57:03 +0200 Subject: [PATCH 132/203] v5.5.0 release information added --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56e5e211..6e775710 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,18 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN +## [5.5.0] - 2023-06-28 +### Fixed +- Error token length mismatch in `ImapProtocol::readResponse` #400 +- Attachment name parsing fixed #410 #421 (thanks @nuernbergerA) +- Additional Attachment name fallback added to prevent missing attachments +- Attachment id is now static (based on the raw part content) and now longer random +- Always parse the attachment description if it is available + +### Added +- Attachment content hash added + + ## [5.4.0] - 2023-06-24 ### Fixed - Legacy protocol support fixed (object to array conversion) #411 From cf32a9dc0213869f4ce2b7c86c863afbd01d806b Mon Sep 17 00:00:00 2001 From: InterLinked1 <24227567+InterLinked1@users.noreply.github.com> Date: Fri, 30 Jun 2023 06:20:26 -0400 Subject: [PATCH 133/203] ImapProtocol.php: fix documentation typo (#423) Fixes a copy/paste typo in the documentation. --- src/Connection/Protocols/ImapProtocol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 4d54579f..15859372 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -723,7 +723,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in } /** - * Fetch message headers + * Fetch message body (without headers) * @param int|array $uids * @param string $rfc * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use From 27d911ca2ec0a825a846909dce73d8decf4f4b4d Mon Sep 17 00:00:00 2001 From: InterLinked1 <24227567+InterLinked1@users.noreply.github.com> Date: Fri, 30 Jun 2023 08:50:57 -0400 Subject: [PATCH 134/203] ImapProtocol: Add STATUS command support. (#424) This library currently contains some functions that purport to get the status of a folder, but these are misleading because they issue the EXAMINE command, not the STATUS command. This library actually currently lacks support for the STATUS command, and this command may be necessary to get some information that the EXAMINE command will not provide. This adds support for STATUS at the protocol level and the folder level, and clarifies the difference in the documentation. --- src/Connection/Protocols/ImapProtocol.php | 19 +++++++++++++++++ src/Folder.php | 26 +++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 15859372..cece4303 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -536,6 +536,25 @@ public function getCapabilities(): Response { return $response->setResult($response->validatedData()[0]); } + /** + * Get an array of available STATUS items + * + * @return Response list of STATUS items + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws ResponseException + */ + public function getStatus(): Response { + $s = implode(" ", $properties); + $response = $this->requestAndResponse('STATUS', array($this->escapeString($folder), "(" . $s . ")")); + + if (!$response->getResponse()) return $response; + + return $response->setResult($response->validatedData()[0]); + } + /** * Examine and select have the same response. * @param string $command can be 'EXAMINE' or 'SELECT' diff --git a/src/Folder.php b/src/Folder.php index c9ca395d..8b3da4db 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -491,8 +491,30 @@ public function idle(callable $callback, int $timeout = 300): void { } } - /** - * Get folder status information + /** + * Get the standard folder status items from STATUS command + * + * @return array + * @throws ConnectionFailedException + * @throws ImapbadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + * @throws AuthFailedException + * @throws ResponseException + */ + public function getFolderStatus(string $folder, array $properties): array { + $status = $client->getConnection()->getStatus($folder->full_name, array('MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY'))->validatedData(); + $c = count($status[2]); + $s = array(); + for ($i = 0; $i < $c; $i++) { + $a = $i++; + $s[$status[2][$a]] = $status[2][$i]; + } + return $s; + } + + /** + * Get folder status information from the EXAMINE command * * @return array * @throws ConnectionFailedException From f71f6daa25edbec1e7e119bd193cdcd00464f197 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 30 Jun 2023 15:21:04 +0200 Subject: [PATCH 135/203] Folder status retrieval fixed and missing methods added --- src/Connection/Protocols/ImapProtocol.php | 55 ++++++++++++------- src/Connection/Protocols/LegacyProtocol.php | 10 ++++ .../Protocols/ProtocolInterface.php | 11 ++++ src/Folder.php | 24 +------- 4 files changed, 58 insertions(+), 42 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index cece4303..4b93d6b7 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -536,25 +536,6 @@ public function getCapabilities(): Response { return $response->setResult($response->validatedData()[0]); } - /** - * Get an array of available STATUS items - * - * @return Response list of STATUS items - * - * @throws ImapBadRequestException - * @throws ImapServerErrorException - * @throws RuntimeException - * @throws ResponseException - */ - public function getStatus(): Response { - $s = implode(" ", $properties); - $response = $this->requestAndResponse('STATUS', array($this->escapeString($folder), "(" . $s . ")")); - - if (!$response->getResponse()) return $response; - - return $response->setResult($response->validatedData()[0]); - } - /** * Examine and select have the same response. * @param string $command can be 'EXAMINE' or 'SELECT' @@ -628,6 +609,42 @@ public function examineFolder(string $folder = 'INBOX'): Response { return $this->examineOrSelect('EXAMINE', $folder); } + /** + * Get the status of a given folder + * + * @param string $folder + * @param string[] $arguments + * @return Response list of STATUS items + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws ResponseException + * @throws RuntimeException + */ + public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response { + $response = $this->requestAndResponse('STATUS', [$this->escapeString($folder), $this->escapeList($arguments)], false); + $data = $response->validatedData(); + + if (!isset($data[0]) || !isset($data[0][2])) { + throw new RuntimeException("folder status could not be fetched"); + } + + $result = []; + $key = null; + foreach($data[0][2] as $value) { + if ($key === null) { + $key = $value; + } else { + $result[$key] = (int)$value; + $key = null; + } + } + + $response->setResult($result); + + return $response; + } + /** * Fetch one or more items of one or more messages * @param array|string $items items to fetch [RFC822.HEADER, FLAGS, RFC822.TEXT, etc] diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 10bc9d9f..cb6393e5 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -236,6 +236,16 @@ public function examineFolder(string $folder = 'INBOX'): Response { }); } + /** + * Get the status of a given folder + * + * @return Response list of STATUS items + * @throws MethodNotSupportedException + */ + public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response { + throw new MethodNotSupportedException(); + } + /** * Fetch message content * @param int|array $uids diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index c02d7800..0a0dbfad 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -117,6 +117,17 @@ public function selectFolder(string $folder = 'INBOX'): Response; */ public function examineFolder(string $folder = 'INBOX'): Response; + /** + * Get the status of a given folder + * + * @return Response list of STATUS items + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + */ + public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response; + /** * Fetch message headers * @param int|array $uids diff --git a/src/Folder.php b/src/Folder.php index 8b3da4db..b3655869 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -491,28 +491,6 @@ public function idle(callable $callback, int $timeout = 300): void { } } - /** - * Get the standard folder status items from STATUS command - * - * @return array - * @throws ConnectionFailedException - * @throws ImapbadRequestException - * @throws ImapServerErrorException - * @throws RuntimeException - * @throws AuthFailedException - * @throws ResponseException - */ - public function getFolderStatus(string $folder, array $properties): array { - $status = $client->getConnection()->getStatus($folder->full_name, array('MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY'))->validatedData(); - $c = count($status[2]); - $s = array(); - for ($i = 0; $i < $c; $i++) { - $a = $i++; - $s[$status[2][$a]] = $status[2][$i]; - } - return $s; - } - /** * Get folder status information from the EXAMINE command * @@ -525,7 +503,7 @@ public function getFolderStatus(string $folder, array $properties): array { * @throws ResponseException */ public function getStatus(): array { - return $this->examine(); + return $this->client->getConnection()->folderStatus($this->path, ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY'])->validatedData(); } /** From be9bded99388f83eade00f1fd4508429d00e5cdc Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 30 Jun 2023 15:21:38 +0200 Subject: [PATCH 136/203] folder status test added --- tests/live/FolderTest.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/live/FolderTest.php b/tests/live/FolderTest.php index 78da7017..7e826452 100644 --- a/tests/live/FolderTest.php +++ b/tests/live/FolderTest.php @@ -347,6 +347,31 @@ public function testUnsubscribe(): void { self::assertTrue(str_starts_with($status[0], 'OK')); } + /** + * Test Folder::status() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testStatus(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $status = $folder->getStatus(); + self::assertEquals(0, $status['MESSAGES']); + self::assertEquals(0, $status['RECENT']); + self::assertEquals(0, $status['UNSEEN']); + self::assertGreaterThan(0, $status['UIDNEXT']); + self::assertGreaterThan(0, $status['UIDVALIDITY']); + } + /** * Test Folder::examine() * From b8ce7993b010cc74b9026363860b3e8ebe1d9580 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 30 Jun 2023 15:21:51 +0200 Subject: [PATCH 137/203] file formatted --- src/Connection/Protocols/ImapProtocol.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 4b93d6b7..0d13d417 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -114,7 +114,7 @@ protected function enableStartTls() { */ public function nextLine(Response $response): string { $line = ""; - while (($next_char = fread($this->stream, 1)) !== false && !in_array($next_char, ["","\n"])) { + while (($next_char = fread($this->stream, 1)) !== false && !in_array($next_char, ["", "\n"])) { $line .= $next_char; } if ($line === "" && ($next_char === false || $next_char === "")) { @@ -300,7 +300,7 @@ public function readResponse(Response $response, string $tag, bool $dontParse = $tokens = [trim(substr($tokens, 0, 3))]; } - $original = is_array($original)?$original : [$original]; + $original = is_array($original) ? $original : [$original]; // last line has response code if ($tokens[0] == 'OK') { @@ -492,7 +492,7 @@ public function logout(): Response { if (!$this->stream) { $this->reset(); return new Response(0, $this->debug); - }elseif ($this->meta()["timed_out"]) { + } elseif ($this->meta()["timed_out"]) { $this->reset(); return new Response(0, $this->debug); } @@ -501,7 +501,8 @@ public function logout(): Response { try { $result = $this->requestAndResponse('LOGOUT', [], true); fclose($this->stream); - } catch (\Throwable) {} + } catch (\Throwable) { + } $this->reset(); @@ -769,7 +770,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in * @throws RuntimeException */ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["$rfc.TEXT"], is_array($uids)?$uids:[$uids], null, $uid); + return $this->fetch(["$rfc.TEXT"], is_array($uids) ? $uids : [$uids], null, $uid); } /** @@ -783,7 +784,7 @@ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid * @throws RuntimeException */ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["$rfc.HEADER"], is_array($uids)?$uids:[$uids], null, $uid); + return $this->fetch(["$rfc.HEADER"], is_array($uids) ? $uids : [$uids], null, $uid); } /** @@ -796,7 +797,7 @@ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid * @throws RuntimeException */ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["FLAGS"], is_array($uids)?$uids:[$uids], null, $uid); + return $this->fetch(["FLAGS"], is_array($uids) ? $uids : [$uids], null, $uid); } /** @@ -809,7 +810,7 @@ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response * @throws RuntimeException */ public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["RFC822.SIZE"], is_array($uids)?$uids:[$uids], null, $uid); + return $this->fetch(["RFC822.SIZE"], is_array($uids) ? $uids : [$uids], null, $uid); } /** From 0bd96d1e8639746d84c7e48482ca9a81d84e17b5 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 30 Jun 2023 15:35:20 +0200 Subject: [PATCH 138/203] getStatus() deprecated --- src/Folder.php | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Folder.php b/src/Folder.php index b3655869..5bfe2f55 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -502,20 +502,39 @@ public function idle(callable $callback, int $timeout = 300): void { * @throws AuthFailedException * @throws ResponseException */ - public function getStatus(): array { + public function status(): array { return $this->client->getConnection()->folderStatus($this->path, ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY'])->validatedData(); } /** + * Get folder status information from the EXAMINE command + * + * @return array + * @throws AuthFailedException * @throws ConnectionFailedException * @throws ImapBadRequestException * @throws ImapServerErrorException + * @throws ResponseException * @throws RuntimeException + * + * @deprecated Use Folder::status() instead + */ + public function getStatus(): array { + return $this->status(); + } + + /** + * Load folder status information from the EXAMINE command + * @return Folder * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws ResponseException + * @throws RuntimeException */ public function loadStatus(): Folder { - $this->status = $this->getStatus(); + $this->status = $this->examine(); return $this; } From b08cbebc7a8f2e8003819e606ef74d0ec44df874 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 30 Jun 2023 15:36:21 +0200 Subject: [PATCH 139/203] switched to lower case letters to be inline with legacy and other command responses --- src/Connection/Protocols/ImapProtocol.php | 2 +- tests/live/FolderTest.php | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 0d13d417..1115b0e0 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -636,7 +636,7 @@ public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', if ($key === null) { $key = $value; } else { - $result[$key] = (int)$value; + $result[strtolower($key)] = (int)$value; $key = null; } } diff --git a/tests/live/FolderTest.php b/tests/live/FolderTest.php index 7e826452..d9997d75 100644 --- a/tests/live/FolderTest.php +++ b/tests/live/FolderTest.php @@ -364,12 +364,12 @@ public function testStatus(): void { $folder = $this->getFolder('INBOX'); self::assertInstanceOf(Folder::class, $folder); - $status = $folder->getStatus(); - self::assertEquals(0, $status['MESSAGES']); - self::assertEquals(0, $status['RECENT']); - self::assertEquals(0, $status['UNSEEN']); - self::assertGreaterThan(0, $status['UIDNEXT']); - self::assertGreaterThan(0, $status['UIDVALIDITY']); + $status = $folder->status(); + self::assertEquals(0, $status['messages']); + self::assertEquals(0, $status['recent']); + self::assertEquals(0, $status['unseen']); + self::assertGreaterThan(0, $status['uidnext']); + self::assertGreaterThan(0, $status['uidvalidity']); } /** From 0966e53399f7c14880f7a3453ef28e6f8efe09b7 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 30 Jun 2023 15:36:40 +0200 Subject: [PATCH 140/203] Changelog updated --- CHANGELOG.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e775710..79cf049f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,17 +6,13 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- Error token length mismatch in `ImapProtocol::readResponse` #400 -- Attachment name parsing fixed #410 #421 (thanks @nuernbergerA) -- Additional Attachment name fallback added to prevent missing attachments -- Attachment id is now static (based on the raw part content) and now longer random -- Always parse the attachment description if it is available +- NaN ### Added -- Attachment content hash added +- IMAP STATUS command support added `Folder::status()` #424 (thanks @InterLinked1) ### Breaking changes -- NaN +- `Folder::getStatus()` no longer returns the results of `EXAMINE` but `STATUS` instead. If you want to use `EXAMINE` you can use the `Folder::examine()` method instead. ## [5.5.0] - 2023-06-28 From bb818faa8566b38ed1429d921cc6b56e4a00099b Mon Sep 17 00:00:00 2001 From: Mikhail Sazanov Date: Thu, 11 Apr 2024 16:55:06 +0300 Subject: [PATCH 141/203] Add attributes and special flags (#428) Signed-off-by: Mikhail Sazanov Co-authored-by: Mikhail Sazanov --- src/Folder.php | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/Folder.php b/src/Folder.php index 5bfe2f55..58211e8c 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -121,6 +121,22 @@ class Folder { /** @var array */ public array $status; + /** @var array */ + public array $attributes = []; + + + const SPECIAL_ATTRIBUTES = [ + 'haschildren' => ['\haschildren'], + 'hasnochildren' => ['\hasnochildren'], + 'template' => ['\template', '\templates'], + 'inbox' => ['\inbox'], + 'sent' => ['\sent'], + 'drafts' => ['\draft', '\drafts'], + 'archive' => ['\archive', '\archives'], + 'trash' => ['\trash'], + 'junk' => ['\junk', '\spam'], + ]; + /** * Folder constructor. * @param Client $client @@ -235,7 +251,7 @@ public function getChildren(): FolderCollection { */ protected function decodeName($name): string|array|bool|null { $parts = []; - foreach (explode($this->delimiter, $name) as $item) { + foreach(explode($this->delimiter, $name) as $item) { $parts[] = EncodingAliases::convert($item, "UTF7-IMAP", "UTF-8"); } @@ -264,6 +280,14 @@ protected function parseAttributes($attributes): void { $this->marked = in_array('\Marked', $attributes); $this->referral = in_array('\Referral', $attributes); $this->has_children = in_array('\HasChildren', $attributes); + + array_map(function($el) { + foreach(self::SPECIAL_ATTRIBUTES as $key => $attribute) { + if(in_array(strtolower($el), $attribute)){ + $this->attributes[] = $key; + } + } + }, $attributes); } /** @@ -284,7 +308,7 @@ protected function parseAttributes($attributes): void { public function move(string $new_name, bool $expunge = true): array { $this->client->checkConnection(); $status = $this->client->getConnection()->renameFolder($this->full_name, $new_name)->validatedData(); - if ($expunge) $this->client->expunge(); + if($expunge) $this->client->expunge(); $folder = $this->client->getFolder($new_name); $event = $this->getEvent("folder", "moved"); @@ -336,7 +360,7 @@ public function appendMessage(string $message, array $options = null, Carbon|str * date string that conforms to the rfc2060 specifications for a date_time value or be a Carbon object. */ - if ($internal_date instanceof Carbon) { + if($internal_date instanceof Carbon){ $internal_date = $internal_date->format('d-M-Y H:i:s O'); } @@ -377,11 +401,11 @@ public function rename(string $new_name, bool $expunge = true): array { */ public function delete(bool $expunge = true): array { $status = $this->client->getConnection()->deleteFolder($this->path)->validatedData(); - if ($this->client->getActiveFolder() == $this->path){ + if($this->client->getActiveFolder() == $this->path){ $this->client->setActiveFolder(null); } - if ($expunge) $this->client->expunge(); + if($expunge) $this->client->expunge(); $event = $this->getEvent("folder", "deleted"); $event::dispatch($this); @@ -437,7 +461,7 @@ public function unsubscribe(): array { public function idle(callable $callback, int $timeout = 300): void { $this->client->setTimeout($timeout); - if (!in_array("IDLE", $this->client->getConnection()->getCapabilities()->validatedData())) { + if(!in_array("IDLE", $this->client->getConnection()->getCapabilities()->validatedData())){ throw new Exceptions\NotSupportedCapabilityException("IMAP server does not support IDLE"); } @@ -450,15 +474,15 @@ public function idle(callable $callback, int $timeout = 300): void { $sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); - while (true) { + while(true) { // This polymorphic call is fine - Protocol::idle() will throw an exception beforehand $line = $idle_client->getConnection()->nextLine(Response::empty()); - if (($pos = strpos($line, "EXISTS")) !== false) { + if(($pos = strpos($line, "EXISTS")) !== false){ $msgn = (int)substr($line, 2, $pos - 2); // Check if the stream is still alive or should be considered stale - if (!$this->client->isConnected() || $last_action->isBefore(Carbon::now())) { + if(!$this->client->isConnected() || $last_action->isBefore(Carbon::now())){ // Reset the connection before interacting with it. Otherwise, the resource might be stale which // would result in a stuck interaction. If you know of a way of detecting a stale resource, please // feel free to improve this logic. I tried a lot but nothing seem to work reliably... @@ -582,7 +606,7 @@ public function getClient(): Client { * @param $delimiter */ public function setDelimiter($delimiter): void { - if (in_array($delimiter, [null, '', ' ', false]) === true) { + if(in_array($delimiter, [null, '', ' ', false]) === true){ $delimiter = ClientManager::get('options.delimiter', '/'); } From 70a134254f20a3e608edd3a960de7e4de96441a0 Mon Sep 17 00:00:00 2001 From: ferrisbuellers Date: Thu, 11 Apr 2024 14:56:32 +0100 Subject: [PATCH 142/203] Fixed date issue if timezone is UT and a 2 digit year (#429) --- src/Header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index 9c8ae046..6eda3fea 100644 --- a/src/Header.php +++ b/src/Header.php @@ -722,7 +722,7 @@ private function parseDate(object $header): void { $date = Carbon::createFromFormat("d M Y H:i:s O", trim(implode(',', $array))); break; case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: - case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: + case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ ([0-9]{2}|[0-9]{4})\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: $date .= 'C'; break; case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}[\,]\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0: From ed1eb0410806afef149f2da951eb30cd2afb8a9c Mon Sep 17 00:00:00 2001 From: Adam Morrison Date: Thu, 11 Apr 2024 09:58:28 -0400 Subject: [PATCH 143/203] Make the space optional after a comma separator (#437) --- src/Header.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Header.php b/src/Header.php index 6eda3fea..ce386b40 100644 --- a/src/Header.php +++ b/src/Header.php @@ -510,7 +510,7 @@ private function decodeAddresses($values): array { } foreach ($values as $address) { - foreach (preg_split('/, (?=(?:[^"]*"[^"]*")*[^"]*$)/', $address) as $split_address) { + foreach (preg_split('/, ?(?=(?:[^"]*"[^"]*")*[^"]*$)/', $address) as $split_address) { $split_address = trim(rtrim($split_address)); if (strpos($split_address, ",") == strlen($split_address) - 1) { From b916e4e4cb910f12b9fef29166d6afd6f56da183 Mon Sep 17 00:00:00 2001 From: Daniel Castilla Date: Thu, 11 Apr 2024 16:00:28 +0200 Subject: [PATCH 144/203] Update ImapProtocol.php (#449) --- src/Connection/Protocols/ImapProtocol.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 1115b0e0..e29d0470 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -90,6 +90,24 @@ public function connect(string $host, int $port = null): bool { return true; } + /** + * Check if the current session is connected + * + * @return bool + */ + public function connected(): bool { + if ((bool)$this->stream) { + try { + $this->requestAndResponse('NOOP'); + return true; + } + catch (ImapServerErrorException|RuntimeException) { + return false; + } + } + return false; + } + /** * Enable tls on the current connection * From fad09ad197dfe3880c65929db63a945e8ed602a4 Mon Sep 17 00:00:00 2001 From: Michal Kortas Date: Thu, 11 Apr 2024 16:02:13 +0200 Subject: [PATCH 145/203] Return recursively detected parts only if there are more than 1 of them. (#455) Co-authored-by: Michal Kortas --- src/Structure.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Structure.php b/src/Structure.php index 745a6234..00a44db2 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -112,7 +112,11 @@ private function parsePart(string $context, int $part_number = 0): array { $headers = new Header($headers); if (($boundary = $headers->getBoundary()) !== null) { - return $this->detectParts($boundary, $body, $part_number); + $parts = $this->detectParts($boundary, $body, $part_number); + + if(count($parts) > 1) { + return $parts; + } } return [new Part($body, $headers, $part_number)]; From e5e8eb0fb1fa08b81bf6996c3c0c35ab65418f9d Mon Sep 17 00:00:00 2001 From: Oliver Scase Date: Thu, 11 Apr 2024 16:03:31 +0200 Subject: [PATCH 146/203] Fix: improve return type hints and return docblocks for query classes (#470) --- src/Query/Query.php | 84 ++++++++++++------------ src/Query/WhereQuery.php | 138 +++++++++++++++++++-------------------- 2 files changed, 111 insertions(+), 111 deletions(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index 727e641f..8ecdf09e 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -640,7 +640,7 @@ public function getByUidLowerThan(int $uid): MessageCollection { * * @return $this */ - public function leaveUnread(): Query { + public function leaveUnread(): static { $this->setFetchOptions(IMAP::FT_PEEK); return $this; @@ -651,7 +651,7 @@ public function leaveUnread(): Query { * * @return $this */ - public function markAsRead(): Query { + public function markAsRead(): static { $this->setFetchOptions(IMAP::FT_UID); return $this; @@ -663,7 +663,7 @@ public function markAsRead(): Query { * * @return $this */ - public function setSequence(int $sequence): Query { + public function setSequence(int $sequence): static { $this->sequence = $sequence; return $this; @@ -699,7 +699,7 @@ public function getClient(): Client { * * @return $this */ - public function limit(int $limit, int $page = 1): Query { + public function limit(int $limit, int $page = 1): static { if ($page >= 1) $this->page = $page; $this->limit = $limit; @@ -719,9 +719,9 @@ public function getQuery(): Collection { * Set all query parameters * @param array $query * - * @return Query + * @return $this */ - public function setQuery(array $query): Query { + public function setQuery(array $query): static { $this->query = new Collection($query); return $this; } @@ -739,9 +739,9 @@ public function getRawQuery(): string { * Set the raw query * @param string $raw_query * - * @return Query + * @return $this */ - public function setRawQuery(string $raw_query): Query { + public function setRawQuery(string $raw_query): static { $this->raw_query = $raw_query; return $this; } @@ -759,9 +759,9 @@ public function getExtensions(): array { * Set all extensions that should be used * @param string[] $extensions * - * @return Query + * @return $this */ - public function setExtensions(array $extensions): Query { + public function setExtensions(array $extensions): static { $this->extensions = $extensions; if (count($this->extensions) > 0) { if (in_array("UID", $this->extensions) === false) { @@ -775,9 +775,9 @@ public function setExtensions(array $extensions): Query { * Set the client instance * @param Client $client * - * @return Query + * @return $this */ - public function setClient(Client $client): Query { + public function setClient(Client $client): static { $this->client = $client; return $this; } @@ -795,9 +795,9 @@ public function getLimit(): ?int { * Set the fetch limit * @param int $limit * - * @return Query + * @return $this */ - public function setLimit(int $limit): Query { + public function setLimit(int $limit): static { $this->limit = $limit <= 0 ? null : $limit; return $this; } @@ -815,9 +815,9 @@ public function getPage(): int { * Set the page * @param int $page * - * @return Query + * @return $this */ - public function setPage(int $page): Query { + public function setPage(int $page): static { $this->page = $page; return $this; } @@ -826,9 +826,9 @@ public function setPage(int $page): Query { * Set the fetch option flag * @param int $fetch_options * - * @return Query + * @return $this */ - public function setFetchOptions(int $fetch_options): Query { + public function setFetchOptions(int $fetch_options): static { $this->fetch_options = $fetch_options; return $this; } @@ -837,9 +837,9 @@ public function setFetchOptions(int $fetch_options): Query { * Set the fetch option flag * @param int $fetch_options * - * @return Query + * @return $this */ - public function fetchOptions(int $fetch_options): Query { + public function fetchOptions(int $fetch_options): static { return $this->setFetchOptions($fetch_options); } @@ -865,9 +865,9 @@ public function getFetchBody(): bool { * Set the fetch body flag * @param boolean $fetch_body * - * @return Query + * @return $this */ - public function setFetchBody(bool $fetch_body): Query { + public function setFetchBody(bool $fetch_body): static { $this->fetch_body = $fetch_body; return $this; } @@ -876,9 +876,9 @@ public function setFetchBody(bool $fetch_body): Query { * Set the fetch body flag * @param boolean $fetch_body * - * @return Query + * @return $this */ - public function fetchBody(bool $fetch_body): Query { + public function fetchBody(bool $fetch_body): static { return $this->setFetchBody($fetch_body); } @@ -895,9 +895,9 @@ public function getFetchFlags(): bool { * Set the fetch flag * @param bool $fetch_flags * - * @return Query + * @return $this */ - public function setFetchFlags(bool $fetch_flags): Query { + public function setFetchFlags(bool $fetch_flags): static { $this->fetch_flags = $fetch_flags; return $this; } @@ -906,9 +906,9 @@ public function setFetchFlags(bool $fetch_flags): Query { * Set the fetch order * @param string $fetch_order * - * @return Query + * @return $this */ - public function setFetchOrder(string $fetch_order): Query { + public function setFetchOrder(string $fetch_order): static { $fetch_order = strtolower($fetch_order); if (in_array($fetch_order, ['asc', 'desc'])) { @@ -922,9 +922,9 @@ public function setFetchOrder(string $fetch_order): Query { * Set the fetch order * @param string $fetch_order * - * @return Query + * @return $this */ - public function fetchOrder(string $fetch_order): Query { + public function fetchOrder(string $fetch_order): static { return $this->setFetchOrder($fetch_order); } @@ -940,36 +940,36 @@ public function getFetchOrder(): string { /** * Set the fetch order to ascending * - * @return Query + * @return $this */ - public function setFetchOrderAsc(): Query { + public function setFetchOrderAsc(): static { return $this->setFetchOrder('asc'); } /** * Set the fetch order to ascending * - * @return Query + * @return $this */ - public function fetchOrderAsc(): Query { + public function fetchOrderAsc(): static { return $this->setFetchOrderAsc(); } /** * Set the fetch order to descending * - * @return Query + * @return $this */ - public function setFetchOrderDesc(): Query { + public function setFetchOrderDesc(): static { return $this->setFetchOrder('desc'); } /** * Set the fetch order to descending * - * @return Query + * @return $this */ - public function fetchOrderDesc(): Query { + public function fetchOrderDesc(): static { return $this->setFetchOrderDesc(); } @@ -977,9 +977,9 @@ public function fetchOrderDesc(): Query { * Set soft fail mode * @var boolean $state * - * @return Query + * @return $this */ - public function softFail(bool $state = true): Query { + public function softFail(bool $state = true): static { return $this->setSoftFail($state); } @@ -987,9 +987,9 @@ public function softFail(bool $state = true): Query { * Set soft fail mode * * @var boolean $state - * @return Query + * @return $this */ - public function setSoftFail(bool $state = true): Query { + public function setSoftFail(bool $state = true): static { $this->soft_fail = $state; return $this; diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index b218e545..74024ddc 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -132,7 +132,7 @@ protected function validate_criteria($criteria): string { * $query->where(["FROM" => "someone@email.tld", "SEEN"]); * $query->where("FROM", "someone@email.tld")->where("SEEN"); */ - public function where(mixed $criteria, mixed $value = null): WhereQuery { + public function where(mixed $criteria, mixed $value = null): static { if (is_array($criteria)) { foreach ($criteria as $key => $value) { if (is_numeric($key)) { @@ -171,7 +171,7 @@ protected function push_search_criteria(string $criteria, mixed $value){ * * @return $this */ - public function orWhere(Closure $closure = null): WhereQuery { + public function orWhere(Closure $closure = null): static { $this->query->push(['OR']); if ($closure !== null) $closure($this); @@ -183,7 +183,7 @@ public function orWhere(Closure $closure = null): WhereQuery { * * @return $this */ - public function andWhere(Closure $closure = null): WhereQuery { + public function andWhere(Closure $closure = null): static { $this->query->push(['AND']); if ($closure !== null) $closure($this); @@ -191,38 +191,38 @@ public function andWhere(Closure $closure = null): WhereQuery { } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereAll(): WhereQuery { + public function whereAll(): static { return $this->where('ALL'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereAnswered(): WhereQuery { + public function whereAnswered(): static { return $this->where('ANSWERED'); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereBcc(string $value): WhereQuery { + public function whereBcc(string $value): static { return $this->where('BCC', $value); } /** * @param mixed $value - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException * @throws MessageSearchValidationException */ - public function whereBefore(mixed $value): WhereQuery { + public function whereBefore(mixed $value): static { $date = $this->parse_date($value); return $this->where('BEFORE', $date); } @@ -230,121 +230,121 @@ public function whereBefore(mixed $value): WhereQuery { /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereBody(string $value): WhereQuery { + public function whereBody(string $value): static { return $this->where('BODY', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereCc(string $value): WhereQuery { + public function whereCc(string $value): static { return $this->where('CC', $value); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereDeleted(): WhereQuery { + public function whereDeleted(): static { return $this->where('DELETED'); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereFlagged(string $value): WhereQuery { + public function whereFlagged(string $value): static { return $this->where('FLAGGED', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereFrom(string $value): WhereQuery { + public function whereFrom(string $value): static { return $this->where('FROM', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereKeyword(string $value): WhereQuery { + public function whereKeyword(string $value): static { return $this->where('KEYWORD', $value); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereNew(): WhereQuery { + public function whereNew(): static { return $this->where('NEW'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereNot(): WhereQuery { + public function whereNot(): static { return $this->where('NOT'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereOld(): WhereQuery { + public function whereOld(): static { return $this->where('OLD'); } /** * @param mixed $value * - * @return WhereQuery + * @return $this * @throws MessageSearchValidationException * @throws InvalidWhereQueryCriteriaException */ - public function whereOn(mixed $value): WhereQuery { + public function whereOn(mixed $value): static { $date = $this->parse_date($value); return $this->where('ON', $date); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereRecent(): WhereQuery { + public function whereRecent(): static { return $this->where('RECENT'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereSeen(): WhereQuery { + public function whereSeen(): static { return $this->where('SEEN'); } /** * @param mixed $value * - * @return WhereQuery + * @return $this * @throws MessageSearchValidationException * @throws InvalidWhereQueryCriteriaException */ - public function whereSince(mixed $value): WhereQuery { + public function whereSince(mixed $value): static { $date = $this->parse_date($value); return $this->where('SINCE', $date); } @@ -352,88 +352,88 @@ public function whereSince(mixed $value): WhereQuery { /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereSubject(string $value): WhereQuery { + public function whereSubject(string $value): static { return $this->where('SUBJECT', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereText(string $value): WhereQuery { + public function whereText(string $value): static { return $this->where('TEXT', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereTo(string $value): WhereQuery { + public function whereTo(string $value): static { return $this->where('TO', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUnkeyword(string $value): WhereQuery { + public function whereUnkeyword(string $value): static { return $this->where('UNKEYWORD', $value); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUnanswered(): WhereQuery { + public function whereUnanswered(): static { return $this->where('UNANSWERED'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUndeleted(): WhereQuery { + public function whereUndeleted(): static { return $this->where('UNDELETED'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUnflagged(): WhereQuery { + public function whereUnflagged(): static { return $this->where('UNFLAGGED'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUnseen(): WhereQuery { + public function whereUnseen(): static { return $this->where('UNSEEN'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereNoXSpam(): WhereQuery { + public function whereNoXSpam(): static { return $this->where("CUSTOM X-Spam-Flag NO"); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereIsXSpam(): WhereQuery { + public function whereIsXSpam(): static { return $this->where("CUSTOM X-Spam-Flag YES"); } @@ -442,10 +442,10 @@ public function whereIsXSpam(): WhereQuery { * @param $header * @param $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereHeader($header, $value): WhereQuery { + public function whereHeader($header, $value): static { return $this->where("CUSTOM HEADER $header $value"); } @@ -453,10 +453,10 @@ public function whereHeader($header, $value): WhereQuery { * Search for a specific message id * @param $messageId * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereMessageId($messageId): WhereQuery { + public function whereMessageId($messageId): static { return $this->whereHeader("Message-ID", $messageId); } @@ -464,20 +464,20 @@ public function whereMessageId($messageId): WhereQuery { * Search for a specific message id * @param $messageId * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereInReplyTo($messageId): WhereQuery { + public function whereInReplyTo($messageId): static { return $this->whereHeader("In-Reply-To", $messageId); } /** * @param $country_code * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereLanguage($country_code): WhereQuery { + public function whereLanguage($country_code): static { return $this->where("Content-Language $country_code"); } @@ -486,10 +486,10 @@ public function whereLanguage($country_code): WhereQuery { * * @param int|string $uid * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUid(int|string $uid): WhereQuery { + public function whereUid(int|string $uid): static { return $this->where('UID', $uid); } @@ -498,10 +498,10 @@ public function whereUid(int|string $uid): WhereQuery { * * @param array $uids * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUidIn(array $uids): WhereQuery { + public function whereUidIn(array $uids): static { $uids = implode(',', $uids); return $this->where('UID', $uids); } From 61e0f1a3ed76c23658ab977b65e9df2782065bdb Mon Sep 17 00:00:00 2001 From: NeekTheNook <75854740+NeekTheNook@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:10:35 +0100 Subject: [PATCH 147/203] Query - Chunked - Resolved infinite loop when start chunk > 1 (#477) Resolved an issue when the start chunks value was > 1 an infinite loop would be created as handled messages would always be less than available messages when chunks are skipped. Added more safety around input arguments, forcing minimums of 1 for both $chunk_size and $start_chunk. --- src/Query/Query.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index 8ecdf09e..b0c9eddd 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -395,8 +395,14 @@ public function get(): MessageCollection { * @throws ResponseException */ public function chunked(callable $callback, int $chunk_size = 10, int $start_chunk = 1): void { + $start_chunk = max($start_chunk,1); + $chunk_size = max($chunk_size,1); + $skipped_messages_count = $chunk_size * ($start_chunk-1); + $available_messages = $this->search(); - if (($available_messages_count = $available_messages->count()) > 0) { + $available_messages_count = max($available_messages->count() - $skipped_messages_count,0); + + if ($available_messages_count > 0) { $old_limit = $this->limit; $old_page = $this->page; From 0d61de530e196eb28dea82def899729c7dee84e0 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 12 Apr 2024 02:12:35 +0200 Subject: [PATCH 148/203] Config handling moved into a new class to allow class serialization --- src/Attachment.php | 75 ++++-- src/Client.php | 93 +++---- src/ClientManager.php | 200 ++------------- src/Config.php | 266 ++++++++++++++++++++ src/Connection/Protocols/ImapProtocol.php | 8 +- src/Connection/Protocols/LegacyProtocol.php | 9 +- src/Connection/Protocols/Protocol.php | 17 +- src/Folder.php | 6 +- src/Header.php | 56 ++++- src/Message.php | 99 +++++--- src/Part.php | 24 +- src/Query/Query.php | 18 +- src/Structure.php | 15 +- 13 files changed, 576 insertions(+), 310 deletions(-) create mode 100644 src/Config.php diff --git a/src/Attachment.php b/src/Attachment.php index 15b83f63..451dfe26 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -57,16 +57,23 @@ class Attachment { /** - * @var Message $oMessage + * @var Message $message */ - protected Message $oMessage; + protected Message $message; /** * Used config * - * @var array $config + * @var Config $config */ - protected array $config = []; + protected Config $config; + + /** + * Attachment options + * + * @var array $options + */ + protected array $options = []; /** @var Part $part */ protected Part $part; @@ -100,23 +107,24 @@ class Attachment { /** * Attachment constructor. - * @param Message $oMessage + * @param Message $message * @param Part $part */ - public function __construct(Message $oMessage, Part $part) { - $this->config = ClientManager::get('options'); + public function __construct(Message $message, Part $part) { + $this->message = $message; + $this->config = $this->message->getConfig(); + $this->options = $this->config->get('options'); - $this->oMessage = $oMessage; $this->part = $part; $this->part_number = $part->part_number; - if ($this->oMessage->getClient()) { - $default_mask = $this->oMessage->getClient()?->getDefaultAttachmentMask(); + if ($this->message->getClient()) { + $default_mask = $this->message->getClient()?->getDefaultAttachmentMask(); if ($default_mask != null) { $this->mask = $default_mask; } } else { - $default_mask = ClientManager::getMask("attachment"); + $default_mask = $this->config->getMask("attachment"); if ($default_mask != "") { $this->mask = $default_mask; } @@ -205,7 +213,7 @@ protected function fetch(): void { $content = $this->part->content; $this->content_type = $this->part->content_type; - $this->content = $this->oMessage->decodeString($content, $this->part->encoding); + $this->content = $this->message->decodeString($content, $this->part->encoding); // Create a hash of the raw part - this can be used to identify the attachment in the message context. However, // it is not guaranteed to be unique and collisions are possible. @@ -292,7 +300,7 @@ public function decodeName(?string $name): string { } } - $decoder = $this->config['decoder']['message']; + $decoder = $this->options['decoder']['message']; if (preg_match('/=\?([^?]+)\?(Q|B)\?(.+)\?=/i', $name, $matches)) { $name = $this->part->getHeader()->decode($name); } elseif ($decoder === 'utf-8' && extension_loaded('imap')) { @@ -364,7 +372,7 @@ public function getAttributes(): array { * @return Message */ public function getMessage(): Message { - return $this->oMessage; + return $this->message; } /** @@ -390,6 +398,45 @@ public function getMask(): string { return $this->mask; } + /** + * Get the attachment options + * @return array + */ + public function getOptions(): array { + return $this->options; + } + + /** + * Set the attachment options + * @param array $options + * + * @return $this + */ + public function setOptions(array $options): Attachment { + $this->options = $options; + return $this; + } + + /** + * Get the used config + * + * @return Config + */ + public function getConfig(): Config { + return $this->config; + } + + /** + * Set the used config + * @param Config $config + * + * @return $this + */ + public function setConfig(Config $config): Attachment { + $this->config = $config; + return $this; + } + /** * Get a masked instance by providing a mask name * @param string|null $mask diff --git a/src/Client.php b/src/Client.php index 8027dc53..2ec9ebd9 100755 --- a/src/Client.php +++ b/src/Client.php @@ -46,6 +46,13 @@ class Client { */ public ?ProtocolInterface $connection = null; + /** + * Client configuration + * + * @var Config + */ + protected Config $config; + /** * Server hostname. * @@ -174,14 +181,14 @@ class Client { /** * Client constructor. - * @param array $config + * @param Config $config * * @throws MaskNotFoundException */ - public function __construct(array $config = []) { + public function __construct(Config $config) { $this->setConfig($config); - $this->setMaskFromConfig($config); - $this->setEventsFromConfig($config); + $this->setMaskFromConfig(); + $this->setEventsFromConfig(); } /** @@ -199,16 +206,17 @@ public function __destruct() { * Clone the current Client instance * * @return Client + * @throws MaskNotFoundException */ public function clone(): Client { - $client = new self(); + $client = new self($this->config); $client->events = $this->events; $client->timeout = $this->timeout; $client->active_folder = $this->active_folder; $client->default_account_config = $this->default_account_config; $config = $this->getAccountConfig(); foreach($config as $key => $value) { - $client->setAccountConfig($key, $config, $this->default_account_config); + $client->setAccountConfig($key, $this->default_account_config); } $client->default_message_mask = $this->default_message_mask; $client->default_attachment_mask = $this->default_message_mask; @@ -217,16 +225,17 @@ public function clone(): Client { /** * Set the Client configuration - * @param array $config + * @param Config $config * * @return self */ - public function setConfig(array $config): Client { - $default_account = ClientManager::get('default'); - $default_config = ClientManager::get("accounts.$default_account"); + public function setConfig(Config $config): Client { + $this->config = $config; + $default_account = $this->config->get('default'); + $default_config = $this->config->get("accounts.$default_account"); foreach ($this->default_account_config as $key => $value) { - $this->setAccountConfig($key, $config, $default_config); + $this->setAccountConfig($key, $default_config); } return $this; @@ -235,27 +244,20 @@ public function setConfig(array $config): Client { /** * Get the current config * - * @return array + * @return Config */ - public function getConfig(): array { - $config = []; - foreach($this->default_account_config as $key => $value) { - $config[$key] = $this->$key; - } - return $config; + public function getConfig(): Config { + return $this->config; } /** * Set a specific account config * @param string $key - * @param array $config * @param array $default_config */ - private function setAccountConfig(string $key, array $config, array $default_config): void { + private function setAccountConfig(string $key, array $default_config): void { $value = $this->default_account_config[$key]; - if(isset($config[$key])) { - $value = $config[$key]; - }elseif(isset($default_config[$key])) { + if(isset($default_config[$key])) { $value = $default_config[$key]; } $this->$key = $value; @@ -278,10 +280,9 @@ public function getAccountConfig(): array { /** * Look for a possible events in any available config - * @param $config */ - protected function setEventsFromConfig($config): void { - $this->events = ClientManager::get("events"); + protected function setEventsFromConfig(): void { + $this->events = $this->config->get("events"); if(isset($config['events'])){ foreach($config['events'] as $section => $events) { $this->events[$section] = array_merge($this->events[$section], $events); @@ -291,35 +292,35 @@ protected function setEventsFromConfig($config): void { /** * Look for a possible mask in any available config - * @param $config * * @throws MaskNotFoundException */ - protected function setMaskFromConfig($config): void { + protected function setMaskFromConfig(): void { + $masks = $this->config->get("masks"); - if(isset($config['masks'])){ - if(isset($config['masks']['message'])) { - if(class_exists($config['masks']['message'])) { - $this->default_message_mask = $config['masks']['message']; + if(isset($masks)){ + if(isset($masks['message'])) { + if(class_exists($masks['message'])) { + $this->default_message_mask = $masks['message']; }else{ - throw new MaskNotFoundException("Unknown mask provided: ".$config['masks']['message']); + throw new MaskNotFoundException("Unknown mask provided: ".$masks['message']); } }else{ - $default_mask = ClientManager::getMask("message"); + $default_mask = $this->config->getMask("message"); if($default_mask != ""){ $this->default_message_mask = $default_mask; }else{ throw new MaskNotFoundException("Unknown message mask provided"); } } - if(isset($config['masks']['attachment'])) { - if(class_exists($config['masks']['attachment'])) { - $this->default_attachment_mask = $config['masks']['attachment']; + if(isset($masks['attachment'])) { + if(class_exists($masks['attachment'])) { + $this->default_attachment_mask = $masks['attachment']; }else{ - throw new MaskNotFoundException("Unknown mask provided: ". $config['masks']['attachment']); + throw new MaskNotFoundException("Unknown mask provided: ". $masks['attachment']); } }else{ - $default_mask = ClientManager::getMask("attachment"); + $default_mask = $this->config->getMask("attachment"); if($default_mask != ""){ $this->default_attachment_mask = $default_mask; }else{ @@ -327,14 +328,14 @@ protected function setMaskFromConfig($config): void { } } }else{ - $default_mask = ClientManager::getMask("message"); + $default_mask = $this->config->getMask("message"); if($default_mask != ""){ $this->default_message_mask = $default_mask; }else{ throw new MaskNotFoundException("Unknown message mask provided"); } - $default_mask = ClientManager::getMask("attachment"); + $default_mask = $this->config->getMask("attachment"); if($default_mask != ""){ $this->default_attachment_mask = $default_mask; }else{ @@ -424,25 +425,25 @@ public function connect(): Client { $protocol = strtolower($this->protocol); if (in_array($protocol, ['imap', 'imap4', 'imap4rev1'])) { - $this->connection = new ImapProtocol($this->validate_cert, $this->encryption); + $this->connection = new ImapProtocol($this->config, $this->validate_cert, $this->encryption); $this->connection->setConnectionTimeout($this->timeout); $this->connection->setProxy($this->proxy); }else{ if (extension_loaded('imap') === false) { throw new ConnectionFailedException("connection setup failed", 0, new ProtocolNotSupportedException($protocol." is an unsupported protocol")); } - $this->connection = new LegacyProtocol($this->validate_cert, $this->encryption); + $this->connection = new LegacyProtocol($this->config, $this->validate_cert, $this->encryption); if (str_starts_with($protocol, "legacy-")) { $protocol = substr($protocol, 7); } $this->connection->setProtocol($protocol); } - if (ClientManager::get('options.debug')) { + if ($this->config->get('options.debug')) { $this->connection->enableDebug(); } - if (!ClientManager::get('options.uid_cache')) { + if (!$this->config->get('options.uid_cache')) { $this->connection->disableUidCache(); } @@ -507,7 +508,7 @@ public function disconnect(): Client { */ public function getFolder(string $folder_name, ?string $delimiter = null, bool $utf7 = false): ?Folder { // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names) - $delimiter = is_null($delimiter) ? ClientManager::get('options.delimiter', "/") : $delimiter; + $delimiter = is_null($delimiter) ? $this->config->get('options.delimiter', "/") : $delimiter; if (str_contains($folder_name, (string)$delimiter)) { return $this->getFolderByPath($folder_name, $utf7); diff --git a/src/ClientManager.php b/src/ClientManager.php index 7f724ffe..a63f3a4e 100644 --- a/src/ClientManager.php +++ b/src/ClientManager.php @@ -16,17 +16,15 @@ * Class ClientManager * * @package Webklex\IMAP - * - * @mixin Client */ class ClientManager { /** * All library config * - * @var array $config + * @var Config $config */ - public static array $config = []; + public Config $config; /** * @var array $accounts @@ -35,9 +33,9 @@ class ClientManager { /** * ClientManager constructor. - * @param array|string $config + * @param array|string|Config $config */ - public function __construct(array|string $config = []) { + public function __construct(array|string|Config $config = []) { $this->setConfig($config); } @@ -63,52 +61,10 @@ public function __call(string $method, array $parameters) { * @throws Exceptions\MaskNotFoundException */ public function make(array $config): Client { - return new Client($config); - } - - /** - * Get a dotted config parameter - * @param string $key - * @param null $default - * - * @return mixed|null - */ - public static function get(string $key, $default = null): mixed { - $parts = explode('.', $key); - $value = null; - foreach ($parts as $part) { - if ($value === null) { - if (isset(self::$config[$part])) { - $value = self::$config[$part]; - } else { - break; - } - } else { - if (isset($value[$part])) { - $value = $value[$part]; - } else { - break; - } - } - } - - return $value === null ? $default : $value; - } - - /** - * Get the mask for a given section - * @param string $section section name such as "message" or "attachment" - * - * @return string|null - */ - public static function getMask(string $section): ?string { - $default_masks = ClientManager::get("masks"); - if (isset($default_masks[$section])) { - if (class_exists($default_masks[$section])) { - return $default_masks[$section]; - } - } - return null; + $name = $this->config->getDefaultAccount(); + $clientConfig = $this->config->all(); + $clientConfig["accounts"] = [$name => $config]; + return new Client(Config::make($clientConfig)); } /** @@ -119,7 +75,7 @@ public static function getMask(string $section): ?string { * @throws Exceptions\MaskNotFoundException */ public function account(string $name = null): Client { - $name = $name ?: $this->getDefaultAccount(); + $name = $name ?: $this->config->getDefaultAccount(); // If the connection has not been resolved we will resolve it now as all // the connections are resolved when they are actually needed, so we do @@ -139,45 +95,11 @@ public function account(string $name = null): Client { * @throws Exceptions\MaskNotFoundException */ protected function resolve(string $name): Client { - $config = $this->getClientConfig($name); + $config = $this->config->getClientConfig($name); return new Client($config); } - /** - * Get the account configuration. - * @param string|null $name - * - * @return array - */ - protected function getClientConfig(?string $name): array { - if ($name === null || $name === 'null' || $name === "") { - return ['driver' => 'null']; - } - $account = self::$config["accounts"][$name] ?? []; - - return is_array($account) ? $account : []; - } - - /** - * Get the name of the default account. - * - * @return string - */ - public function getDefaultAccount(): string { - return self::$config['default']; - } - - /** - * Set the name of the default account. - * @param string $name - * - * @return void - */ - public function setDefaultAccount(string $name): void { - self::$config['default'] = $name; - } - /** * Merge the vendor settings with the local config @@ -186,108 +108,24 @@ public function setDefaultAccount(string $name): void { * If however the default account is missing a parameter the package default account parameter will be used. * This can be disabled by setting imap.default in your config file to 'false' * - * @param array|string $config + * @param array|string|Config $config * * @return $this */ - public function setConfig(array|string $config): ClientManager { - - if (is_array($config) === false) { - $config = require $config; - } - - $config_key = 'imap'; - $path = __DIR__ . '/config/' . $config_key . '.php'; - - $vendor_config = require $path; - $config = $this->array_merge_recursive_distinct($vendor_config, $config); - - if (is_array($config)) { - if (isset($config['default'])) { - if (isset($config['accounts']) && $config['default']) { - - $default_config = $vendor_config['accounts']['default']; - if (isset($config['accounts'][$config['default']])) { - $default_config = array_merge($default_config, $config['accounts'][$config['default']]); - } - - if (is_array($config['accounts'])) { - foreach ($config['accounts'] as $account_key => $account) { - $config['accounts'][$account_key] = array_merge($default_config, $account); - } - } - } - } + public function setConfig(array|string|Config $config): ClientManager { + if (!$config instanceof Config) { + $config = Config::make($config); } - - self::$config = $config; + $this->config = $config; return $this; } /** - * Marge arrays recursively and distinct - * - * Merges any number of arrays / parameters recursively, replacing - * entries with string keys with values from latter arrays. - * If the entry or the next value to be assigned is an array, then it - * automatically treats both arguments as an array. - * Numeric entries are appended, not replaced, but only if they are - * unique - * - * @return array|mixed - * - * @link http://www.php.net/manual/en/function.array-merge-recursive.php#96201 - * @author Mark Roduner + * Get the config instance + * @return Config */ - private function array_merge_recursive_distinct(): mixed { - - $arrays = func_get_args(); - $base = array_shift($arrays); - - // From https://stackoverflow.com/a/173479 - $isAssoc = function(array $arr) { - if (array() === $arr) return false; - return array_keys($arr) !== range(0, count($arr) - 1); - }; - - if (!is_array($base)) $base = empty($base) ? array() : array($base); - - foreach ($arrays as $append) { - - if (!is_array($append)) $append = array($append); - - foreach ($append as $key => $value) { - - if (!array_key_exists($key, $base) and !is_numeric($key)) { - $base[$key] = $value; - continue; - } - - if ( - ( - is_array($value) - && $isAssoc($value) - ) - || ( - is_array($base[$key]) - && $isAssoc($base[$key]) - ) - ) { - // If the arrays are not associates we don't want to array_merge_recursive_distinct - // else merging $baseConfig['dispositions'] = ['attachment', 'inline'] with $customConfig['dispositions'] = ['attachment'] - // results in $resultConfig['dispositions'] = ['attachment', 'inline'] - $base[$key] = $this->array_merge_recursive_distinct($base[$key], $value); - } else if (is_numeric($key)) { - if (!in_array($value, $base)) $base[] = $value; - } else { - $base[$key] = $value; - } - - } - - } - - return $base; + public function getConfig(): Config { + return $this->config; } } \ No newline at end of file diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 00000000..b5cf2599 --- /dev/null +++ b/src/Config.php @@ -0,0 +1,266 @@ +config = $config; + } + + /** + * Get a dotted config parameter + * @param string $key + * @param null $default + * + * @return mixed|null + */ + public function get(string $key, $default = null): mixed { + $parts = explode('.', $key); + $value = null; + foreach ($parts as $part) { + if ($value === null) { + if (isset($this->config[$part])) { + $value = $this->config[$part]; + } else { + break; + } + } else { + if (isset($value[$part])) { + $value = $value[$part]; + } else { + break; + } + } + } + + return $value === null ? $default : $value; + } + + /** + * Set a dotted config parameter + * @param string $key + * @param string|array|mixed$value + * + * @return void + */ + public function set(string $key, mixed $value): void { + $parts = explode('.', $key); + $config = &$this->config; + + foreach ($parts as $part) { + if (!isset($config[$part])) { + $config[$part] = []; + } + $config = &$config[$part]; + } + + if(is_array($config) && is_array($value)){ + $config = array_merge($config, $value); + }else{ + $config = $value; + } + } + + /** + * Get the mask for a given section + * @param string $section section name such as "message" or "attachment" + * + * @return string|null + */ + public function getMask(string $section): ?string { + $default_masks = $this->get('masks', []); + if (isset($default_masks[$section])) { + if (class_exists($default_masks[$section])) { + return $default_masks[$section]; + } + } + return null; + } + + /** + * Get the account configuration. + * @param string|null $name + * + * @return self + */ + public function getClientConfig(?string $name): self { + $config = $this->all(); + $defaultName = $this->getDefaultAccount(); + $defaultAccount = $this->get('accounts.'.$defaultName, []); + + if ($name === null || $name === 'null' || $name === "") { + $account = $defaultAccount; + $name = $defaultName; + }else{ + $account = $this->get('accounts.'.$name, $defaultAccount); + } + + $config["default"] = $name; + $config["accounts"] = [ + $name => $account + ]; + + return new self($config); + } + + /** + * Get the name of the default account. + * + * @return string + */ + public function getDefaultAccount(): string { + return $this->get('default', 'default'); + } + + /** + * Set the name of the default account. + * @param string $name + * + * @return void + */ + public function setDefaultAccount(string $name): void { + $this->set('default', $name); + } + + /** + * Create a new instance of the Config class + * @param array|string $config + * @return Config + */ + public static function make(array|string $config = []): Config { + if (is_array($config) === false) { + $config = require $config; + } + + $config_key = 'imap'; + $path = __DIR__ . '/config/' . $config_key . '.php'; + + $vendor_config = require $path; + $config = self::array_merge_recursive_distinct($vendor_config, $config); + + if (isset($config['default'])) { + if (isset($config['accounts']) && $config['default']) { + + $default_config = $vendor_config['accounts']['default']; + if (isset($config['accounts'][$config['default']])) { + $default_config = array_merge($default_config, $config['accounts'][$config['default']]); + } + + if (is_array($config['accounts'])) { + foreach ($config['accounts'] as $account_key => $account) { + $config['accounts'][$account_key] = array_merge($default_config, $account); + } + } + } + } + + return new self($config); + } + + /** + * Marge arrays recursively and distinct + * + * Merges any number of arrays / parameters recursively, replacing + * entries with string keys with values from latter arrays. + * If the entry or the next value to be assigned is an array, then it + * automatically treats both arguments as an array. + * Numeric entries are appended, not replaced, but only if they are + * unique + * + * @return array + * + * @link http://www.php.net/manual/en/function.array-merge-recursive.php#96201 + * @author Mark Roduner + */ + private static function array_merge_recursive_distinct(): array { + $arrays = func_get_args(); + $base = array_shift($arrays); + + // From https://stackoverflow.com/a/173479 + $isAssoc = function(array $arr) { + if (array() === $arr) return false; + return array_keys($arr) !== range(0, count($arr) - 1); + }; + + if (!is_array($base)) $base = empty($base) ? array() : array($base); + + foreach ($arrays as $append) { + if (!is_array($append)) $append = array($append); + + foreach ($append as $key => $value) { + + if (!array_key_exists($key, $base) and !is_numeric($key)) { + $base[$key] = $value; + continue; + } + + if ((is_array($value) && $isAssoc($value)) || (is_array($base[$key]) && $isAssoc($base[$key]))) { + // If the arrays are not associates we don't want to array_merge_recursive_distinct + // else merging $baseConfig['dispositions'] = ['attachment', 'inline'] with $customConfig['dispositions'] = ['attachment'] + // results in $resultConfig['dispositions'] = ['attachment', 'inline'] + $base[$key] = self::array_merge_recursive_distinct($base[$key], $value); + } else if (is_numeric($key)) { + if (!in_array($value, $base)) $base[] = $value; + } else { + $base[$key] = $value; + } + + } + + } + + return $base; + } + + /** + * Get all configuration values + * @return array + */ + public function all(): array { + return $this->config; + } + + /** + * Check if a configuration value exists + * @param string $key + * @return bool + */ + public function has(string $key): bool { + return $this->get($key) !== null; + } + + /** + * Remove all configuration values + * @return $this + */ + public function clear(): static { + $this->config = []; + return $this; + } +} \ No newline at end of file diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index e29d0470..4de12c1e 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -13,6 +13,8 @@ namespace Webklex\PHPIMAP\Connection\Protocols; use Exception; +use Throwable; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; @@ -41,10 +43,12 @@ class ImapProtocol extends Protocol { /** * Imap constructor. + * @param Config $config * @param bool $cert_validation set to false to skip SSL certificate validation * @param mixed $encryption Connection encryption method */ - public function __construct(bool $cert_validation = true, mixed $encryption = false) { + public function __construct(Config $config, bool $cert_validation = true, mixed $encryption = false) { + $this->config = $config; $this->setCertValidation($cert_validation); $this->encryption = $encryption; } @@ -1314,7 +1318,7 @@ public function overview(string $sequence, int|string $uid = IMAP::ST_UID): Resp $headers = $this->headers($ids, "RFC822", $uid); $response->stack($headers); foreach ($headers->data() as $id => $raw_header) { - $result[$id] = (new Header($raw_header, false))->getAttributes(); + $result[$id] = (new Header($raw_header, $this->config))->getAttributes(); } } return $response->setResult($result)->setCanBeEmpty(true); diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index cb6393e5..6313dca9 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -13,6 +13,7 @@ namespace Webklex\PHPIMAP\Connection\Protocols; use Webklex\PHPIMAP\ClientManager; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; use Webklex\PHPIMAP\Exceptions\MethodNotSupportedException; @@ -32,10 +33,12 @@ class LegacyProtocol extends Protocol { /** * Imap constructor. + * @param Config $config * @param bool $cert_validation set to false to skip SSL certificate validation * @param mixed $encryption Connection encryption method */ - public function __construct(bool $cert_validation = true, mixed $encryption = false) { + public function __construct(Config $config, bool $cert_validation = true, mixed $encryption = false) { + $this->config = $config; $this->setCertValidation($cert_validation); $this->encryption = $encryption; } @@ -52,7 +55,7 @@ public function __destruct() { * @param string $host * @param int|null $port */ - public function connect(string $host, int $port = null) { + public function connect(string $host, int $port = null): void { if ($this->encryption) { $encryption = strtolower($this->encryption); if ($encryption == "ssl") { @@ -81,7 +84,7 @@ public function login(string $user, string $password): Response { $password, 0, $attempts = 3, - ClientManager::get('options.open') + $this->config->get('options.open') ); $response->addCommand("imap_open"); } catch (\ErrorException $e) { diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index 6fe88ee9..dd909b40 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -12,6 +12,7 @@ namespace Webklex\PHPIMAP\Connection\Protocols; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\IMAP; @@ -38,10 +39,15 @@ abstract class Protocol implements ProtocolInterface { protected bool $enable_uid_cache = true; /** - * @var resource + * @var resource|mixed|boolean|null $stream */ public $stream = false; + /** + * @var Config $config + */ + protected Config $config; + /** * Connection encryption method * @var string $encryption @@ -363,4 +369,13 @@ public function meta(): array { public function getStream(): mixed { return $this->stream; } + + /** + * Set the Config instance + * + * @return Config + */ + public function getConfig(): Config { + return $this->config; + } } diff --git a/src/Folder.php b/src/Folder.php index 58211e8c..81634e30 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -334,7 +334,7 @@ public function move(string $new_name, bool $expunge = true): array { public function overview(string $sequence = null): array { $this->client->openFolder($this->path); $sequence = $sequence === null ? "1:*" : $sequence; - $uid = ClientManager::get('options.sequence', IMAP::ST_MSGN); + $uid = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN); $response = $this->client->getConnection()->overview($sequence, $uid); return $response->validatedData(); } @@ -472,7 +472,7 @@ public function idle(callable $callback, int $timeout = 300): void { $last_action = Carbon::now()->addSeconds($timeout); - $sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); + $sequence = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN); while(true) { // This polymorphic call is fine - Protocol::idle() will throw an exception beforehand @@ -607,7 +607,7 @@ public function getClient(): Client { */ public function setDelimiter($delimiter): void { if(in_array($delimiter, [null, '', ' ', false]) === true){ - $delimiter = ClientManager::get('options.delimiter', '/'); + $delimiter = $this->client->getConfig()->get('options.delimiter', '/'); } $this->delimiter = $delimiter; diff --git a/src/Header.php b/src/Header.php index ce386b40..d4b86975 100644 --- a/src/Header.php +++ b/src/Header.php @@ -41,9 +41,16 @@ class Header { /** * Config holder * - * @var array $config + * @var Config $config */ - protected array $config = []; + protected Config $config; + + /** + * Config holder + * + * @var array $options + */ + protected array $options = []; /** * Fallback Encoding @@ -54,13 +61,15 @@ class Header { /** * Header constructor. + * @param Config $config * @param string $raw_header * * @throws InvalidMessageDateException */ - public function __construct(string $raw_header) { + public function __construct(string $raw_header, Config $config) { $this->raw = $raw_header; - $this->config = ClientManager::get('options'); + $this->config = $config; + $this->options = $this->config->get('options'); $this->parse(); } @@ -162,7 +171,7 @@ public function find($pattern): mixed { * @return string|null */ public function getBoundary(): ?string { - $regex = $this->config["boundary"] ?? "/boundary=(.*?(?=;)|(.*))/i"; + $regex = $this->options["boundary"] ?? "/boundary=(.*?(?=;)|(.*))/i"; $boundary = $this->find($regex); if ($boundary === null) { @@ -229,7 +238,7 @@ protected function parse(): void { public function rfc822_parse_headers($raw_headers): object { $headers = []; $imap_headers = []; - if (extension_loaded('imap') && $this->config["rfc822"]) { + if (extension_loaded('imap') && $this->options["rfc822"]) { $raw_imap_headers = (array)\imap_rfc822_parse_headers($raw_headers); foreach ($raw_imap_headers as $key => $values) { $key = strtolower(str_replace("-", "_", $key)); @@ -418,7 +427,7 @@ public function decode(mixed $value): mixed { return $this->decodeArray($value); } $original_value = $value; - $decoder = $this->config['decoder']['message']; + $decoder = $this->options['decoder']['message']; if ($value !== null) { if ($decoder === 'utf-8') { @@ -490,7 +499,7 @@ private function findPriority(): void { private function decodeAddresses($values): array { $addresses = []; - if (extension_loaded('mailparse') && $this->config["rfc822"]) { + if (extension_loaded('mailparse') && $this->options["rfc822"]) { foreach ($values as $address) { foreach (\mailparse_rfc822_parse_addresses($address) as $parsed_address) { if (isset($parsed_address['address'])) { @@ -800,9 +809,38 @@ public function setAttributes(array $attributes): Header { * * @return Header */ - public function setConfig(array $config): Header { + public function setOptions(array $config): Header { + $this->options = $config; + return $this; + } + + /** + * Get the configuration used for parsing a raw header + * + * @return array + */ + public function getOptions(): array { + return $this->options; + } + + /** + * Set the configuration used for parsing a raw header + * @param Config $config + * + * @return Header + */ + public function setConfig(Config $config): Header { $this->config = $config; return $this; } + /** + * Get the configuration used for parsing a raw header + * + * @return Config + */ + public function getConfig(): Config { + return $this->config; + } + } diff --git a/src/Message.php b/src/Message.php index 09a534f2..050ebc9d 100755 --- a/src/Message.php +++ b/src/Message.php @@ -97,11 +97,18 @@ class Message { protected string $mask = MessageMask::class; /** - * Used config + * Used options * - * @var array $config + * @var array $options */ - protected array $config = []; + protected array $options = []; + + /** + * All library configs + * + * @var Config $config + */ + protected Config $config; /** * Attribute holder @@ -205,7 +212,7 @@ class Message { * @throws ResponseException */ public function __construct(int $uid, ?int $msglist, Client $client, int $fetch_options = null, bool $fetch_body = false, bool $fetch_flags = false, int $sequence = null) { - $this->boot(); + $this->boot($client->getConfig()); $default_mask = $client->getDefaultMessageMask(); if ($default_mask != null) { @@ -269,7 +276,7 @@ public static function make(int $uid, ?int $msglist, Client $client, string $raw $reflection = new ReflectionClass(self::class); /** @var Message $instance */ $instance = $reflection->newInstanceWithoutConstructor(); - $instance->boot(); + $instance->boot($client->getConfig()); $default_mask = $client->getDefaultMessageMask(); if ($default_mask != null) { @@ -296,29 +303,33 @@ public static function make(int $uid, ?int $msglist, Client $client, string $raw /** * Create a new message instance by reading and loading a file or remote location + * @param string $filename + * @param ?Config $config * - * @throws RuntimeException - * @throws MessageContentFetchingException - * @throws ResponseException - * @throws ImapBadRequestException - * @throws InvalidMessageDateException + * @return Message + * @throws AuthFailedException * @throws ConnectionFailedException + * @throws ImapBadRequestException * @throws ImapServerErrorException - * @throws ReflectionException - * @throws AuthFailedException + * @throws InvalidMessageDateException * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException */ - public static function fromFile($filename): Message { + public static function fromFile(string $filename, Config $config = null): Message { $blob = file_get_contents($filename); if ($blob === false) { throw new RuntimeException("Unable to read file"); } - return self::fromString($blob); + return self::fromString($blob, $config); } /** * Create a new message instance by reading and loading a string * @param string $blob + * @param ?Config $config * * @return Message * @throws AuthFailedException @@ -332,13 +343,13 @@ public static function fromFile($filename): Message { * @throws ResponseException * @throws RuntimeException */ - public static function fromString(string $blob): Message { + public static function fromString(string $blob, Config $config = null): Message { $reflection = new ReflectionClass(self::class); /** @var Message $instance */ $instance = $reflection->newInstanceWithoutConstructor(); - $instance->boot(); + $instance->boot($config); - $default_mask = ClientManager::getMask("message"); + $default_mask = $instance->getConfig()->getMask("message"); if($default_mask != ""){ $instance->setMask($default_mask); }else{ @@ -361,15 +372,18 @@ public static function fromString(string $blob): Message { /** * Boot a new instance + * @param ?Config $config */ - public function boot(): void { + public function boot(Config $config = null): void { $this->attributes = []; + $this->client = null; + $this->config = $config ?? Config::make(); - $this->config = ClientManager::get('options'); - $this->available_flags = ClientManager::get('flags'); + $this->options = $this->config->get('options'); + $this->available_flags = $this->config->get('flags'); - $this->attachments = AttachmentCollection::make([]); - $this->flags = FlagCollection::make([]); + $this->attachments = AttachmentCollection::make(); + $this->flags = FlagCollection::make(); } /** @@ -543,7 +557,7 @@ private function parseHeader(): void { * @throws InvalidMessageDateException */ public function parseRawHeader(string $raw_header): void { - $this->header = new Header($raw_header); + $this->header = new Header($raw_header, $this->getConfig()); } /** @@ -786,7 +800,7 @@ public function setFetchOption($option): Message { if (is_long($option) === true) { $this->fetch_options = $option; } elseif (is_null($option) === true) { - $config = ClientManager::get('options.fetch', IMAP::FT_UID); + $config = $this->config->get('options.fetch', IMAP::FT_UID); $this->fetch_options = is_long($config) ? $config : 1; } @@ -803,7 +817,7 @@ public function setSequence(?int $sequence): Message { if (is_long($sequence)) { $this->sequence = $sequence; } elseif (is_null($sequence)) { - $config = ClientManager::get('options.sequence', IMAP::ST_MSGN); + $config = $this->config->get('options.sequence', IMAP::ST_MSGN); $this->sequence = is_long($config) ? $config : IMAP::ST_MSGN; } @@ -820,7 +834,7 @@ public function setFetchBodyOption($option): Message { if (is_bool($option)) { $this->fetch_body = $option; } elseif (is_null($option)) { - $config = ClientManager::get('options.fetch_body', true); + $config = $this->config->get('options.fetch_body', true); $this->fetch_body = is_bool($config) ? $config : true; } @@ -837,7 +851,7 @@ public function setFetchFlagsOption($option): Message { if (is_bool($option)) { $this->fetch_flags = $option; } elseif (is_null($option)) { - $config = ClientManager::get('options.fetch_flags', true); + $config = $this->config->get('options.fetch_flags', true); $this->fetch_flags = is_bool($config) ? $config : true; } @@ -973,7 +987,7 @@ public function getFolder(): ?Folder { public function thread(Folder $sent_folder = null, MessageCollection &$thread = null, Folder $folder = null): MessageCollection { $thread = $thread ?: MessageCollection::make([]); $folder = $folder ?: $this->getFolder(); - $sent_folder = $sent_folder ?: $this->client->getFolderByPath(ClientManager::get("options.common_folders.sent", "INBOX/Sent")); + $sent_folder = $sent_folder ?: $this->client->getFolderByPath($this->config->get("options.common_folders.sent", "INBOX/Sent")); /** @var Message $message */ foreach ($thread as $message) { @@ -1547,11 +1561,11 @@ public function setFolderPath($folder_path): Message { /** * Set the config - * @param array $config + * @param Config $config * * @return Message */ - public function setConfig(array $config): Message { + public function setConfig(Config $config): Message { $this->config = $config; return $this; @@ -1560,12 +1574,33 @@ public function setConfig(array $config): Message { /** * Get the config * - * @return array + * @return Config */ - public function getConfig(): array { + public function getConfig(): Config { return $this->config; } + /** + * Set the options + * @param array $options + * + * @return Message + */ + public function setOptions(array $options): Message { + $this->options = $options; + + return $this; + } + + /** + * Get the options + * + * @return array + */ + public function getOptions(): array { + return $this->options; + } + /** * Set the available flags * @param $available_flags diff --git a/src/Part.php b/src/Part.php index 1759b8de..1dbf9439 100644 --- a/src/Part.php +++ b/src/Part.php @@ -139,16 +139,23 @@ class Part { */ private ?Header $header; + /** + * @var Config $config + */ + protected Config $config; + /** * Part constructor. - * @param $raw_part + * @param string $raw_part + * @param Config $config * @param Header|null $header * @param integer $part_number * * @throws InvalidMessageDateException */ - public function __construct($raw_part, Header $header = null, int $part_number = 0) { + public function __construct(string $raw_part, Config $config, Header $header = null, int $part_number = 0) { $this->raw = $raw_part; + $this->config = $config; $this->header = $header; $this->part_number = $part_number; $this->parse(); @@ -211,7 +218,7 @@ private function findHeaders(): string { $headers = substr($this->raw, 0, strlen($body) * -1); $body = substr($body, 0, -2); - $this->header = new Header($headers); + $this->header = new Header($headers, $this->config); return $body; } @@ -282,7 +289,7 @@ private function parseEncoding(): void { * @return bool */ public function isAttachment(): bool { - $valid_disposition = in_array(strtolower($this->disposition ?? ''), ClientManager::get('options.dispositions')); + $valid_disposition = in_array(strtolower($this->disposition ?? ''), $this->config->get('options.dispositions')); if ($this->type == IMAP::MESSAGE_TYPE_TEXT && ($this->ifdisposition == 0 || empty($this->disposition) || !$valid_disposition)) { if (($this->subtype == null || in_array((strtolower($this->subtype)), ["plain", "html"])) && $this->filename == null && $this->name == null) { @@ -305,4 +312,13 @@ public function getHeader(): ?Header { return $this->header; } + /** + * Get the Config instance + * + * @return Config + */ + public function getConfig(): Config { + return $this->config; + } + } diff --git a/src/Query/Query.php b/src/Query/Query.php index b0c9eddd..7ae035de 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -18,7 +18,6 @@ use Illuminate\Support\Collection; use ReflectionException; use Webklex\PHPIMAP\Client; -use Webklex\PHPIMAP\ClientManager; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\EventNotFoundException; @@ -93,18 +92,19 @@ class Query { */ public function __construct(Client $client, array $extensions = []) { $this->setClient($client); + $config = $this->client->getConfig(); - $this->sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); - if (ClientManager::get('options.fetch') === IMAP::FT_PEEK) $this->leaveUnread(); + $this->sequence = $config->get('options.sequence', IMAP::ST_MSGN); + if ($config->get('options.fetch') === IMAP::FT_PEEK) $this->leaveUnread(); - if (ClientManager::get('options.fetch_order') === 'desc') { + if ($config->get('options.fetch_order') === 'desc') { $this->fetch_order = 'desc'; } else { $this->fetch_order = 'asc'; } - $this->date_format = ClientManager::get('date_format', 'd M y'); - $this->soft_fail = ClientManager::get('options.soft_fail', false); + $this->date_format = $config->get('date_format', 'd M y'); + $this->soft_fail = $config->get('options.soft_fail', false); $this->setExtensions($extensions); $this->query = new Collection(); @@ -235,6 +235,7 @@ protected function fetch(Collection $available_messages): array { $uids = $available_messages->forPage($this->page, $this->limit)->toArray(); $extensions = $this->getExtensions(); if (empty($extensions) === false && method_exists($this->client->getConnection(), "fetch")) { + // this polymorphic call is fine - the method exists at this point $extensions = $this->client->getConnection()->fetch($extensions, $uids, null, $this->sequence)->validatedData(); } $flags = $this->client->getConnection()->flags($uids, $this->sequence)->validatedData(); @@ -336,11 +337,12 @@ public function curate_messages(Collection $available_messages): MessageCollecti * @throws ResponseException */ protected function populate(Collection $available_messages): MessageCollection { - $messages = MessageCollection::make([]); + $messages = MessageCollection::make(); + $config = $this->client->getConfig(); $messages->total($available_messages->count()); - $message_key = ClientManager::get('options.message_key'); + $message_key = $config->get('options.message_key'); $raw_messages = $this->fetch($available_messages); diff --git a/src/Structure.php b/src/Structure.php index 00a44db2..4907c542 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -50,11 +50,11 @@ class Structure { public array $parts = []; /** - * Config holder + * Options holder * - * @var array $config + * @var array $options */ - protected array $config = []; + protected array $options = []; /** * Structure constructor. @@ -67,7 +67,7 @@ class Structure { public function __construct($raw_structure, Header $header) { $this->raw = $raw_structure; $this->header = $header; - $this->config = ClientManager::get('options'); + $this->options = $header->getConfig()->get('options'); $this->parse(); } @@ -110,7 +110,8 @@ private function parsePart(string $context, int $part_number = 0): array { $headers = substr($context, 0, strlen($body) * -1); $body = substr($body, 0, -2); - $headers = new Header($headers); + $config = $this->header->getConfig(); + $headers = new Header($headers, $config); if (($boundary = $headers->getBoundary()) !== null) { $parts = $this->detectParts($boundary, $body, $part_number); @@ -119,7 +120,7 @@ private function parsePart(string $context, int $part_number = 0): array { } } - return [new Part($body, $headers, $part_number)]; + return [new Part($body, $this->header->getConfig(), $headers, $part_number)]; } /** @@ -163,6 +164,6 @@ public function find_parts(): array { return $this->detectParts($boundary, $this->raw); } - return [new Part($this->raw, $this->header)]; + return [new Part($this->raw, $this->header->getConfig(), $this->header)]; } } From d23aecc035533464230788f1eff6c13c257478db Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 12 Apr 2024 02:15:23 +0200 Subject: [PATCH 149/203] code quality improved & doc blocks added --- examples/custom_attachment_mask.php | 3 +- examples/custom_message_mask.php | 5 +-- src/Connection/Protocols/ImapProtocol.php | 39 +++++++++++++++------ src/Connection/Protocols/LegacyProtocol.php | 4 +-- src/Connection/Protocols/Protocol.php | 4 +-- src/Folder.php | 6 ++-- src/Header.php | 14 ++++---- src/Message.php | 13 +++---- src/Query/Query.php | 2 +- src/Query/WhereQuery.php | 2 +- src/Support/Masks/AttachmentMask.php | 1 + src/Support/Masks/MessageMask.php | 1 + 12 files changed, 58 insertions(+), 36 deletions(-) diff --git a/examples/custom_attachment_mask.php b/examples/custom_attachment_mask.php index 5a6323a3..eb4973e3 100644 --- a/examples/custom_attachment_mask.php +++ b/examples/custom_attachment_mask.php @@ -33,8 +33,9 @@ public function custom_save(): bool { } -/** @var \Webklex\PHPIMAP\Client $client */ $cm = new \Webklex\PHPIMAP\ClientManager('path/to/config/imap.php'); + +/** @var \Webklex\PHPIMAP\Client $client */ $client = $cm->account('default'); $client->connect(); $client->setDefaultAttachmentMask(CustomAttachmentMask::class); diff --git a/examples/custom_message_mask.php b/examples/custom_message_mask.php index 187eeed4..0463c65b 100644 --- a/examples/custom_message_mask.php +++ b/examples/custom_message_mask.php @@ -30,8 +30,9 @@ public function getAttachmentCount(): int { } -/** @var \Webklex\PHPIMAP\Client $client */ $cm = new \Webklex\PHPIMAP\ClientManager('path/to/config/imap.php'); + +/** @var \Webklex\PHPIMAP\Client $client */ $client = $cm->account('default'); $client->connect(); @@ -44,7 +45,7 @@ public function getAttachmentCount(): int { /** @var CustomMessageMask $masked_message */ $masked_message = $message->mask(CustomMessageMask::class); -echo 'Token for uid ['.$masked_message->uid.']: '.$masked_message->token().' @atms:'.$masked_message->getAttachmentCount(); +echo 'Token for uid [' . $masked_message->uid . ']: ' . $masked_message->token() . ' @atms:' . $masked_message->getAttachmentCount(); $masked_message->setFlag('seen'); diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 4de12c1e..cb7cf4b4 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -98,14 +98,14 @@ public function connect(string $host, int $port = null): bool { * Check if the current session is connected * * @return bool + * @throws ImapBadRequestException */ public function connected(): bool { if ((bool)$this->stream) { try { $this->requestAndResponse('NOOP'); return true; - } - catch (ImapServerErrorException|RuntimeException) { + } catch (ImapServerErrorException|RuntimeException) { return false; } } @@ -120,7 +120,7 @@ public function connected(): bool { * @throws ImapServerErrorException * @throws RuntimeException */ - protected function enableStartTls() { + protected function enableStartTls(): void { $response = $this->requestAndResponse('STARTTLS'); $result = $response->successful() && stream_socket_enable_crypto($this->stream, true, $this->getCryptoMethod()); if (!$result) { @@ -324,14 +324,33 @@ public function readResponse(Response $response, string $tag, bool $dontParse = $original = is_array($original) ? $original : [$original]; + // last line has response code if ($tokens[0] == 'OK') { return $lines ?: [true]; } elseif ($tokens[0] == 'NO' || $tokens[0] == 'BAD' || $tokens[0] == 'BYE') { - throw new ImapServerErrorException(implode("\n", $original)); + throw new ImapServerErrorException($this->stringifyArray($original)); } - throw new ImapBadRequestException(implode("\n", $original)); + throw new ImapBadRequestException($this->stringifyArray($original)); + } + + /** + * Convert an array to a string + * @param array $arr array to stringify + * + * @return string stringified array + */ + private function stringifyArray(array $arr): string { + $string = ""; + foreach ($arr as $value) { + if (is_array($value)) { + $string .= "(" . $this->stringifyArray($value) . ")"; + } else { + $string .= $value . " "; + } + } + return $string; } /** @@ -523,7 +542,7 @@ public function logout(): Response { try { $result = $this->requestAndResponse('LOGOUT', [], true); fclose($this->stream); - } catch (\Throwable) { + } catch (Throwable) { } $this->reset(); @@ -572,7 +591,7 @@ public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'I $result = []; $tokens = []; // define $tokens variable before first use - while (!$this->readLine($response, $tokens, $tag, false)) { + while (!$this->readLine($response, $tokens, $tag)) { if ($tokens[0] == 'FLAGS') { array_shift($tokens); $result['flags'] = $tokens; @@ -645,7 +664,7 @@ public function examineFolder(string $folder = 'INBOX'): Response { * @throws RuntimeException */ public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response { - $response = $this->requestAndResponse('STATUS', [$this->escapeString($folder), $this->escapeList($arguments)], false); + $response = $this->requestAndResponse('STATUS', [$this->escapeString($folder), $this->escapeList($arguments)]); $data = $response->validatedData(); if (!isset($data[0]) || !isset($data[0][2])) { @@ -654,7 +673,7 @@ public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', $result = []; $key = null; - foreach($data[0][2] as $value) { + foreach ($data[0][2] as $value) { if ($key === null) { $key = $value; } else { @@ -1245,7 +1264,7 @@ public function getQuotaRoot(string $quota_root = 'INBOX'): Response { * * @throws RuntimeException */ - public function idle() { + public function idle(): void { $response = $this->sendRequest("IDLE"); if (!$this->assumedNextLine($response, '+ ')) { throw new RuntimeException('idle failed'); diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 6313dca9..c6471a02 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -125,8 +125,6 @@ public function login(string $user, string $password): Response { * @param string $token access token * * @return Response - * @throws AuthFailedException - * @throws RuntimeException */ public function authenticate(string $user, string $token): Response { return $this->login($user, $token); @@ -392,7 +390,7 @@ public function getUid(int $id = null): Response { } /** - * Get a message number for a uid + * Get the message number of a given uid * @param string $id uid * * @return Response message number diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index dd909b40..a3886b55 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -274,7 +274,7 @@ public function buildUIDCommand(string $command, int|string $uid): string { * * @param array|null $uids */ - public function setUidCache(?array $uids) { + public function setUidCache(?array $uids): void { if (is_null($uids)) { $this->uid_cache = []; return; @@ -336,7 +336,7 @@ public function connected(): bool { } /** - * Retrieves header/meta data from the resource stream + * Retrieves header/metadata from the resource stream * * @return array */ diff --git a/src/Folder.php b/src/Folder.php index 81634e30..8c9abd61 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -252,7 +252,7 @@ public function getChildren(): FolderCollection { protected function decodeName($name): string|array|bool|null { $parts = []; foreach(explode($this->delimiter, $name) as $item) { - $parts[] = EncodingAliases::convert($item, "UTF7-IMAP", "UTF-8"); + $parts[] = EncodingAliases::convert($item, "UTF7-IMAP"); } return implode($this->delimiter, $parts); @@ -402,7 +402,7 @@ public function rename(string $new_name, bool $expunge = true): array { public function delete(bool $expunge = true): array { $status = $this->client->getConnection()->deleteFolder($this->path)->validatedData(); if($this->client->getActiveFolder() == $this->path){ - $this->client->setActiveFolder(null); + $this->client->setActiveFolder(); } if($expunge) $this->client->expunge(); @@ -527,7 +527,7 @@ public function idle(callable $callback, int $timeout = 300): void { * @throws ResponseException */ public function status(): array { - return $this->client->getConnection()->folderStatus($this->path, ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY'])->validatedData(); + return $this->client->getConnection()->folderStatus($this->path)->validatedData(); } /** diff --git a/src/Header.php b/src/Header.php index d4b86975..539bd05e 100644 --- a/src/Header.php +++ b/src/Header.php @@ -205,7 +205,7 @@ protected function parse(): void { $this->set("subject", $this->decode($header->subject)); } if (property_exists($header, 'references')) { - $this->set("references", array_map(function ($item) { + $this->set("references", array_map(function($item) { return str_replace(['<', '>'], '', $item); }, explode(" ", $header->references))); } @@ -440,14 +440,14 @@ public function decode(mixed $value): mixed { $value = $tempValue; } else if (extension_loaded('imap')) { $value = \imap_utf8($value); - }else if (function_exists('iconv_mime_decode')){ + } else if (function_exists('iconv_mime_decode')) { $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); - }else{ + } else { $value = mb_decode_mimeheader($value); } - }elseif ($decoder === 'iconv') { + } elseif ($decoder === 'iconv') { $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); - }else if ($this->is_uft8($value)) { + } else if ($this->is_uft8($value)) { $value = mb_decode_mimeheader($value); } @@ -771,10 +771,10 @@ private function parseDate(object $header): void { try { $parsed_date = Carbon::parse($date); } catch (\Exception $_e) { - if (!isset($this->config["fallback_date"])) { + if (!isset($this->options["fallback_date"])) { throw new InvalidMessageDateException("Invalid message date. ID:" . $this->get("message_id") . " Date:" . $header->date . "/" . $date, 1100, $e); } else { - $parsed_date = Carbon::parse($this->config["fallback_date"]); + $parsed_date = Carbon::parse($this->options["fallback_date"]); } } } diff --git a/src/Message.php b/src/Message.php index 050ebc9d..10b96018 100755 --- a/src/Message.php +++ b/src/Message.php @@ -12,6 +12,7 @@ namespace Webklex\PHPIMAP; +use Exception; use ReflectionClass; use ReflectionException; use Webklex\PHPIMAP\Exceptions\AuthFailedException; @@ -87,7 +88,7 @@ class Message { * * @var ?Client */ - private ?Client $client = null; + private ?Client $client; /** * Default mask @@ -565,7 +566,7 @@ public function parseRawHeader(string $raw_header): void { * @param array $raw_flags */ public function parseRawFlags(array $raw_flags): void { - $this->flags = FlagCollection::make([]); + $this->flags = FlagCollection::make(); foreach ($raw_flags as $flag) { if (str_starts_with($flag, "\\")) { @@ -592,7 +593,7 @@ public function parseRawFlags(array $raw_flags): void { */ private function parseFlags(): void { $this->client->openFolder($this->folder_path); - $this->flags = FlagCollection::make([]); + $this->flags = FlagCollection::make(); $sequence_id = $this->getSequenceId(); try { @@ -628,7 +629,7 @@ public function parseBody(): Message { try { $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence)->validatedData(); } catch (Exceptions\RuntimeException $e) { - throw new MessageContentFetchingException("failed to fetch content", 0); + throw new MessageContentFetchingException("failed to fetch content", 0, $e); } if (!isset($contents[$sequence_id])) { throw new MessageContentFetchingException("no content found", 0); @@ -919,7 +920,7 @@ public function convertEncoding($str, string $from = "ISO-8859-2", string $to = if (function_exists('iconv') && !EncodingAliases::isUtf7($from) && !EncodingAliases::isUtf7($to)) { try { return iconv($from, $to.'//IGNORE', $str); - } catch (\Exception $e) { + } catch (Exception) { return @iconv($from, $to, $str); } } else { @@ -985,7 +986,7 @@ public function getFolder(): ?Folder { * @throws ResponseException */ public function thread(Folder $sent_folder = null, MessageCollection &$thread = null, Folder $folder = null): MessageCollection { - $thread = $thread ?: MessageCollection::make([]); + $thread = $thread ?: MessageCollection::make(); $folder = $folder ?: $this->getFolder(); $sent_folder = $sent_folder ?: $this->client->getFolderByPath($this->config->get("options.common_folders.sent", "INBOX/Sent")); diff --git a/src/Query/Query.php b/src/Query/Query.php index 7ae035de..cade729d 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -315,7 +315,7 @@ public function curate_messages(Collection $available_messages): MessageCollecti if ($available_messages->count() > 0) { return $this->populate($available_messages); } - return MessageCollection::make([]); + return MessageCollection::make(); } catch (Exception $e) { throw new GetMessagesFailedException($e->getMessage(), 0, $e); } diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index 74024ddc..e76cbe85 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -155,7 +155,7 @@ public function where(mixed $criteria, mixed $value = null): static { * * @throws InvalidWhereQueryCriteriaException */ - protected function push_search_criteria(string $criteria, mixed $value){ + protected function push_search_criteria(string $criteria, mixed $value): void { $criteria = $this->validate_criteria($criteria); $value = $this->parse_value($value); diff --git a/src/Support/Masks/AttachmentMask.php b/src/Support/Masks/AttachmentMask.php index d79b948a..2559c5b9 100644 --- a/src/Support/Masks/AttachmentMask.php +++ b/src/Support/Masks/AttachmentMask.php @@ -18,6 +18,7 @@ * Class AttachmentMask * * @package Webklex\PHPIMAP\Support\Masks + * @mixin Attachment */ class AttachmentMask extends Mask { diff --git a/src/Support/Masks/MessageMask.php b/src/Support/Masks/MessageMask.php index 4cc3d5c0..aa3623f9 100644 --- a/src/Support/Masks/MessageMask.php +++ b/src/Support/Masks/MessageMask.php @@ -19,6 +19,7 @@ * Class MessageMask * * @package Webklex\PHPIMAP\Support\Masks + * @mixin Message */ class MessageMask extends Mask { From 5e0f4a81193512ec17fa9906146a3a6dfda9c2ea Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 12 Apr 2024 02:17:10 +0200 Subject: [PATCH 150/203] Test for issue #420 added --- tests/issues/Issue420Test.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/issues/Issue420Test.php diff --git a/tests/issues/Issue420Test.php b/tests/issues/Issue420Test.php new file mode 100644 index 00000000..0cac9b6e --- /dev/null +++ b/tests/issues/Issue420Test.php @@ -0,0 +1,31 @@ +get("subject"); + + // Ticket No: [��17] Mailbox Inbox - (17) Incoming failed messages + $this->assertEquals('Ticket No: [??17] Mailbox Inbox - (17) Incoming failed messages', utf8_decode($subject->toString())); + } + +} \ No newline at end of file From 46a75ab83ab5735d1585e3e2dc5c3b0542bb756a Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 12 Apr 2024 02:17:46 +0200 Subject: [PATCH 151/203] tests updated to use the correct config --- tests/ClientManagerTest.php | 25 +++++++++-------- tests/ClientTest.php | 42 ++++++++++++++++------------- tests/HeaderTest.php | 19 ++++++++++--- tests/ImapProtocolTest.php | 15 ++++++++++- tests/MessageTest.php | 30 ++++++++++++--------- tests/PartTest.php | 25 ++++++++++++----- tests/StructureTest.php | 15 ++++++++++- tests/fixtures/DateTemplateTest.php | 2 +- tests/fixtures/FixtureTestCase.php | 5 ++-- tests/issues/Issue355Test.php | 3 ++- tests/issues/Issue383Test.php | 2 +- tests/issues/Issue393Test.php | 2 +- tests/issues/Issue412Test.php | 1 + tests/issues/Issue413Test.php | 1 + tests/live/ClientTest.php | 2 +- tests/live/FolderTest.php | 12 ++++----- tests/live/LegacyTest.php | 6 ++--- tests/live/MessageTest.php | 18 ++++++------- tests/live/QueryTest.php | 4 +-- 19 files changed, 149 insertions(+), 80 deletions(-) diff --git a/tests/ClientManagerTest.php b/tests/ClientManagerTest.php index 80ec4ceb..e7910cf1 100644 --- a/tests/ClientManagerTest.php +++ b/tests/ClientManagerTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase; use Webklex\PHPIMAP\Client; use Webklex\PHPIMAP\ClientManager; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; use Webklex\PHPIMAP\IMAP; @@ -38,10 +39,12 @@ public function setUp(): void { * @return void */ public function testConfigAccessorAccount(): void { - self::assertSame("default", ClientManager::get("default")); - self::assertSame("d-M-Y", ClientManager::get("date_format")); - self::assertSame(IMAP::FT_PEEK, ClientManager::get("options.fetch")); - self::assertSame([], ClientManager::get("options.open")); + $config = $this->cm->getConfig(); + self::assertInstanceOf(Config::class, $config); + self::assertSame("default", $config->get("default")); + self::assertSame("d-M-Y", $config->get("date_format")); + self::assertSame(IMAP::FT_PEEK, $config->get("options.fetch")); + self::assertSame([], $config->get("options.open")); } /** @@ -59,12 +62,12 @@ public function testMakeClient(): void { * @throws MaskNotFoundException */ public function testAccountAccessor(): void { - self::assertSame("default", $this->cm->getDefaultAccount()); + self::assertSame("default", $this->cm->getConfig()->getDefaultAccount()); self::assertNotEmpty($this->cm->account("default")); - $this->cm->setDefaultAccount("foo"); - self::assertSame("foo", $this->cm->getDefaultAccount()); - $this->cm->setDefaultAccount("default"); + $this->cm->getConfig()->setDefaultAccount("foo"); + self::assertSame("foo", $this->cm->getConfig()->getDefaultAccount()); + $this->cm->getConfig()->setDefaultAccount("default"); } /** @@ -82,10 +85,10 @@ public function testSetConfig(): void { ]; $cm = new ClientManager($config); - self::assertSame("foo", $cm->getDefaultAccount()); + self::assertSame("foo", $cm->getConfig()->getDefaultAccount()); self::assertInstanceOf(Client::class, $cm->account("foo")); - self::assertSame(IMAP::ST_MSGN, $cm->get("options.fetch")); - self::assertSame(false, is_array($cm->get("options.open"))); + self::assertSame(IMAP::ST_MSGN, $cm->getConfig()->get("options.fetch")); + self::assertSame(false, is_array($cm->getConfig()->get("options.open"))); } } \ No newline at end of file diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 8d9f8e66..315f3c42 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Webklex\PHPIMAP\Client; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Connection\Protocols\ImapProtocol; use Webklex\PHPIMAP\Connection\Protocols\Response; use Webklex\PHPIMAP\Exceptions\AuthFailedException; @@ -42,18 +43,22 @@ class ClientTest extends TestCase { * @throws MaskNotFoundException */ public function setUp(): void { - $this->client = new Client([ - 'protocol' => 'imap', - 'encryption' => 'ssl', - 'username' => 'foo@domain.tld', - 'password' => 'bar', - 'proxy' => [ - 'socket' => null, - 'request_fulluri' => false, - 'username' => null, - 'password' => null, - ], - ]); + $config = Config::make([ + "accounts" => [ + "default" => [ + 'protocol' => 'imap', + 'encryption' => 'ssl', + 'username' => 'foo@domain.tld', + 'password' => 'bar', + 'proxy' => [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ], + ]] + ]); + $this->client = new Client($config); } /** @@ -274,18 +279,19 @@ public function testClientId(): void { } public function testClientConfig(): void { - $config = $this->client->getConfig(); + $config = $this->client->getConfig()->get("accounts.".$this->client->getConfig()->getDefaultAccount()); self::assertSame("foo@domain.tld", $config["username"]); self::assertSame("bar", $config["password"]); self::assertSame("localhost", $config["host"]); self::assertSame(true, $config["validate_cert"]); self::assertSame(993, $config["port"]); - $this->client->setConfig([ - "host" => "domain.tld", - 'password' => 'bar', - ]); - $config = $this->client->getConfig(); + $this->client->getConfig()->set("accounts.".$this->client->getConfig()->getDefaultAccount(), [ + "host" => "domain.tld", + 'password' => 'bar', + ]); + $config = $this->client->getConfig()->get("accounts.".$this->client->getConfig()->getDefaultAccount()); + self::assertSame("bar", $config["password"]); self::assertSame("domain.tld", $config["host"]); self::assertSame(true, $config["validate_cert"]); diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index 7afec065..65e31d7f 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -16,12 +16,25 @@ use PHPUnit\Framework\TestCase; use Webklex\PHPIMAP\Address; use Webklex\PHPIMAP\Attribute; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Header; use Webklex\PHPIMAP\IMAP; class HeaderTest extends TestCase { + /** @var Config $config */ + protected Config $config; + + /** + * Setup the test environment. + * + * @return void + */ + public function setUp(): void { + $this->config = Config::make(); + } + /** * Test parsing email headers * @@ -35,7 +48,7 @@ public function testHeaderParsing(): void { $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); - $header = new Header($raw_header); + $header = new Header($raw_header, $this->config); $subject = $header->get("subject"); $returnPath = $header->get("Return-Path"); /** @var Carbon $date */ @@ -80,9 +93,9 @@ public function testRfc822ParseHeaders() { ->onlyMethods([]) ->getMock(); - $config = new \ReflectionProperty($mock, 'config'); + $config = new \ReflectionProperty($mock, 'options'); $config->setAccessible(true); - $config->setValue($mock, ['rfc822' => true]); + $config->setValue($mock, $this->config->get("options")); $mockHeader = "Content-Type: text/csv; charset=WINDOWS-1252; name*0=\"TH_Is_a_F ile name example 20221013.c\"; name*1=sv\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Disposition: attachment; filename*0=\"TH_Is_a_F ile name example 20221013.c\"; filename*1=\"sv\"\r\n"; diff --git a/tests/ImapProtocolTest.php b/tests/ImapProtocolTest.php index 4d744332..78716548 100644 --- a/tests/ImapProtocolTest.php +++ b/tests/ImapProtocolTest.php @@ -13,11 +13,24 @@ namespace Tests; use PHPUnit\Framework\TestCase; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Connection\Protocols\ImapProtocol; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; class ImapProtocolTest extends TestCase { + /** @var Config $config */ + protected Config $config; + + /** + * Setup the test environment. + * + * @return void + */ + public function setUp(): void { + $this->config = Config::make(); + } + /** * ImapProtocol test @@ -26,7 +39,7 @@ class ImapProtocolTest extends TestCase { */ public function testImapProtocol(): void { - $protocol = new ImapProtocol(false); + $protocol = new ImapProtocol($this->config, false); self::assertSame(false, $protocol->getCertValidation()); self::assertSame("", $protocol->getEncryption()); diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 3f854918..1bc9ce6f 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -18,6 +18,7 @@ use Webklex\PHPIMAP\Attachment; use Webklex\PHPIMAP\Attribute; use Webklex\PHPIMAP\Client; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Connection\Protocols\Response; use Webklex\PHPIMAP\Exceptions\EventNotFoundException; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; @@ -51,21 +52,24 @@ class MessageTest extends TestCase { * Setup the test environment. * * @return void - * @throws MaskNotFoundException */ public function setUp(): void { - $this->client = new Client([ - 'protocol' => 'imap', - 'encryption' => 'ssl', - 'username' => 'foo@domain.tld', - 'password' => 'bar', - 'proxy' => [ - 'socket' => null, - 'request_fulluri' => false, - 'username' => null, - 'password' => null, - ], - ]); + $config = Config::make([ + "accounts" => [ + "default" => [ + 'protocol' => 'imap', + 'encryption' => 'ssl', + 'username' => 'foo@domain.tld', + 'password' => 'bar', + 'proxy' => [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ], + ]] + ]); + $this->client = new Client($config); } /** diff --git a/tests/PartTest.php b/tests/PartTest.php index f4653519..8543c46b 100644 --- a/tests/PartTest.php +++ b/tests/PartTest.php @@ -14,6 +14,7 @@ use Carbon\Carbon; use PHPUnit\Framework\TestCase; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; use Webklex\PHPIMAP\Header; @@ -23,6 +24,18 @@ class PartTest extends TestCase { + /** @var Config $config */ + protected Config $config; + + /** + * Setup the test environment. + * + * @return void + */ + public function setUp(): void { + $this->config = Config::make(); + } + /** * Test parsing a text Part * @throws InvalidMessageDateException @@ -31,8 +44,8 @@ public function testTextPart(): void { $raw_headers = "Content-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n"; $raw_body = "\r\nAny updates?"; - $headers = new Header($raw_headers); - $part = new Part($raw_body, $headers, 0); + $headers = new Header($raw_headers, $this->config); + $part = new Part($raw_body, $this->config, $headers, 0); self::assertSame("UTF-8", $part->charset); self::assertSame("text/plain", $part->content_type); @@ -53,8 +66,8 @@ public function testHTMLPart(): void { $raw_headers = "Content-Type: text/html;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n"; $raw_body = "\r\n

\r\n

Any updates?

"; - $headers = new Header($raw_headers); - $part = new Part($raw_body, $headers, 0); + $headers = new Header($raw_headers, $this->config); + $part = new Part($raw_body, $this->config, $headers, 0); self::assertSame("UTF-8", $part->charset); self::assertSame("text/html", $part->content_type); @@ -75,8 +88,8 @@ public function testBase64Part(): void { $raw_headers = "Content-Type: application/octet-stream; name=6mfFxiU5Yhv9WYJx.txt\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=6mfFxiU5Yhv9WYJx.txt\r\n"; $raw_body = "em5rNTUxTVAzVFAzV1BwOUtsMWduTEVycldFZ2tKRkF0dmFLcWtUZ3JrM2RLSThkWDM4WVQ4QmFW\r\neFJjT0VSTg=="; - $headers = new Header($raw_headers); - $part = new Part($raw_body, $headers, 0); + $headers = new Header($raw_headers, $this->config); + $part = new Part($raw_body, $this->config, $headers, 0); self::assertSame("", $part->charset); self::assertSame("application/octet-stream", $part->content_type); diff --git a/tests/StructureTest.php b/tests/StructureTest.php index a1df098b..22d71bbf 100644 --- a/tests/StructureTest.php +++ b/tests/StructureTest.php @@ -13,6 +13,7 @@ namespace Tests; use PHPUnit\Framework\TestCase; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; use Webklex\PHPIMAP\Header; @@ -20,6 +21,18 @@ class StructureTest extends TestCase { + /** @var Config $config */ + protected Config $config; + + /** + * Setup the test environment. + * + * @return void + */ + public function setUp(): void { + $this->config = Config::make(); + } + /** * Test parsing email headers * @@ -35,7 +48,7 @@ public function testStructureParsing(): void { $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); $raw_body = substr($email, strlen($raw_header)+8); - $header = new Header($raw_header); + $header = new Header($raw_header, $this->config); $structure = new Structure($raw_body, $header); self::assertSame(2, count($structure->parts)); diff --git a/tests/fixtures/DateTemplateTest.php b/tests/fixtures/DateTemplateTest.php index 79f42dfe..9c43ae8b 100644 --- a/tests/fixtures/DateTemplateTest.php +++ b/tests/fixtures/DateTemplateTest.php @@ -81,7 +81,7 @@ public function testFixture() : void { "fallback_date" => "2021-01-01 00:00:00", ], ]); - $message = $this->getFixture("date-template.eml"); + $message = $this->getFixture("date-template.eml", self::$manager->getConfig()); self::assertEquals("test", $message->subject); self::assertEquals("1.0", $message->mime_version); diff --git a/tests/fixtures/FixtureTestCase.php b/tests/fixtures/FixtureTestCase.php index 8660bedc..00f31157 100644 --- a/tests/fixtures/FixtureTestCase.php +++ b/tests/fixtures/FixtureTestCase.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Webklex\PHPIMAP\ClientManager; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; @@ -83,9 +84,9 @@ final public function __construct(?string $name = null, array $data = [], $dataN * @throws ResponseException * @throws RuntimeException */ - final public function getFixture(string $template) : Message { + final public function getFixture(string $template, Config $config = null) : Message { $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", $template]); - $message = Message::fromFile($filename); + $message = Message::fromFile($filename, $config); self::assertInstanceOf(Message::class, $message); return $message; diff --git a/tests/issues/Issue355Test.php b/tests/issues/Issue355Test.php index a61cd07a..0fa6d07e 100644 --- a/tests/issues/Issue355Test.php +++ b/tests/issues/Issue355Test.php @@ -13,6 +13,7 @@ namespace Tests\issues; use PHPUnit\Framework\TestCase; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Header; class Issue355Test extends TestCase { @@ -20,7 +21,7 @@ class Issue355Test extends TestCase { public function testIssue() { $raw_header = "Subject: =?UTF-8?Q?Re=3A_Uppdaterat_=C3=A4rende_=28447899=29=2C_kostnader_f=C3=B6r_hj=C3=A4?= =?UTF-8?Q?lp_med_stadge=C3=A4ndring_enligt_ny_lagstiftning?=\r\n"; - $header = new Header($raw_header); + $header = new Header($raw_header, Config::make()); $subject = $header->get("subject"); $this->assertEquals("Re: Uppdaterat ärende (447899), kostnader för hjälp med stadgeändring enligt ny lagstiftning", $subject->toString()); diff --git a/tests/issues/Issue383Test.php b/tests/issues/Issue383Test.php index 0f20a396..30cb3a9b 100644 --- a/tests/issues/Issue383Test.php +++ b/tests/issues/Issue383Test.php @@ -43,7 +43,7 @@ public function testIssue(): void { $client = $this->getClient(); $client->connect(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'Entwürfe+']); $folder = $client->getFolder($folder_path); diff --git a/tests/issues/Issue393Test.php b/tests/issues/Issue393Test.php index 73099e07..017ff535 100644 --- a/tests/issues/Issue393Test.php +++ b/tests/issues/Issue393Test.php @@ -43,7 +43,7 @@ public function testIssue(): void { $client = $this->getClient(); $client->connect(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $pattern = implode($delimiter, ['doesnt_exist', '%']); $folder = $client->getFolder('doesnt_exist'); diff --git a/tests/issues/Issue412Test.php b/tests/issues/Issue412Test.php index 5d105541..bfaa883d 100644 --- a/tests/issues/Issue412Test.php +++ b/tests/issues/Issue412Test.php @@ -13,6 +13,7 @@ namespace Tests\issues; use PHPUnit\Framework\TestCase; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Message; class Issue412Test extends TestCase { diff --git a/tests/issues/Issue413Test.php b/tests/issues/Issue413Test.php index cfdae548..2162d6b5 100644 --- a/tests/issues/Issue413Test.php +++ b/tests/issues/Issue413Test.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Tests\live\LiveMailboxTestCase; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Folder; use Webklex\PHPIMAP\Message; diff --git a/tests/live/ClientTest.php b/tests/live/ClientTest.php index 307894a7..1d224d57 100644 --- a/tests/live/ClientTest.php +++ b/tests/live/ClientTest.php @@ -187,7 +187,7 @@ public function testOpenFolder(): void { public function testCreateFolder(): void { $client = $this->getClient()->connect(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', $this->getSpecialChars()]); $folder = $client->getFolder($folder_path); diff --git a/tests/live/FolderTest.php b/tests/live/FolderTest.php index d9997d75..178329cf 100644 --- a/tests/live/FolderTest.php +++ b/tests/live/FolderTest.php @@ -79,7 +79,7 @@ public function testHasChildren(): void { $folder = $this->getFolder('INBOX'); self::assertInstanceOf(Folder::class, $folder); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $child_path = implode($delimiter, ['INBOX', 'test']); if ($folder->getClient()->getFolder($child_path) === null) { $folder->getClient()->createFolder($child_path, false); @@ -107,7 +107,7 @@ public function testSetChildren(): void { $folder = $this->getFolder('INBOX'); self::assertInstanceOf(Folder::class, $folder); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $child_path = implode($delimiter, ['INBOX', 'test']); if ($folder->getClient()->getFolder($child_path) === null) { $folder->getClient()->createFolder($child_path, false); @@ -137,7 +137,7 @@ public function testGetChildren(): void { $folder = $this->getFolder('INBOX'); self::assertInstanceOf(Folder::class, $folder); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $child_path = implode($delimiter, ['INBOX', 'test']); if ($folder->getClient()->getFolder($child_path) === null) { $folder->getClient()->createFolder($child_path, false); @@ -167,7 +167,7 @@ public function testGetChildren(): void { public function testMove(): void { $client = $this->getClient(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'test']); $folder = $client->getFolder($folder_path); @@ -208,7 +208,7 @@ public function testMove(): void { public function testDelete(): void { $client = $this->getClient(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'test']); $folder = $client->getFolder($folder_path); @@ -439,7 +439,7 @@ public function testSetDelimiter(): void { $folder->setDelimiter("."); self::assertEquals(".", $folder->delimiter); - $default_delimiter = $this->getManager()->get("options.delimiter", "/"); + $default_delimiter = $this->getManager()->getConfig()->get("options.delimiter", "/"); $folder->setDelimiter(null); self::assertEquals($default_delimiter, $folder->delimiter); } diff --git a/tests/live/LegacyTest.php b/tests/live/LegacyTest.php index 85fa4448..61e2ee09 100644 --- a/tests/live/LegacyTest.php +++ b/tests/live/LegacyTest.php @@ -98,7 +98,7 @@ public function __construct(?string $name = null, array $data = [], int|string $ */ public function testSizes(): void { - $delimiter = ClientManager::get("options.delimiter"); + $delimiter = self::$client->getConfig()->get("options.delimiter"); $child_path = implode($delimiter, ['INBOX', 'test']); if (self::$client->getFolder($child_path) === null) { self::$client->createFolder($child_path, false); @@ -276,7 +276,7 @@ final protected function deleteFolder(Folder $folder = null): bool { * @throws MessageSearchValidationException */ public function testQueryWhere(): void { - $delimiter = ClientManager::get("options.delimiter"); + $delimiter = self::$client->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'search']); $folder = self::$client->getFolder($folder_path); @@ -431,7 +431,7 @@ protected function assertWhereSearchCriteria(Folder $folder, string $criteria, C $criteria = str_replace("CUSTOM ", "", $criteria); $expected = $value === null ? [$criteria] : [$criteria, $value]; if ($date === true && $value instanceof Carbon) { - $date_format = ClientManager::get('date_format', 'd M y'); + $date_format = $folder->getClient()->getConfig()->get('date_format', 'd M y'); $expected[1] = $value->format($date_format); } diff --git a/tests/live/MessageTest.php b/tests/live/MessageTest.php index 31b15579..30e953ec 100644 --- a/tests/live/MessageTest.php +++ b/tests/live/MessageTest.php @@ -126,7 +126,7 @@ public function testConvertEncoding(): void { public function testThread(): void { $client = $this->getClient(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'thread']); $folder = $client->getFolder($folder_path); @@ -420,7 +420,7 @@ public function testSetFlag(): void { public function testGetMsgn(): void { $client = $this->getClient(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'test']); $folder = $client->getFolder($folder_path); @@ -830,13 +830,13 @@ public function testGetSequenceId(): void { public function testSetConfig(): void { $message = $this->getDefaultMessage(); - $config = $message->getConfig(); - self::assertIsArray($config); + $options = $message->getOptions(); + self::assertIsArray($options); - $message->setConfig(["foo" => "bar"]); - self::assertArrayHasKey("foo", $message->getConfig()); + $message->setOptions(["foo" => "bar"]); + self::assertArrayHasKey("foo", $message->getOptions()); - $message->setConfig($config); + $message->setOptions($options); // Cleanup self::assertTrue($message->delete()); @@ -1406,7 +1406,7 @@ public function testCopy(): void { $client = $message->getClient(); self::assertInstanceOf(Client::class, $client); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'test']); $folder = $client->getFolder($folder_path); @@ -2161,7 +2161,7 @@ public function testMove(): void { $client = $message->getClient(); self::assertInstanceOf(Client::class, $client); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'test']); $folder = $client->getFolder($folder_path); diff --git a/tests/live/QueryTest.php b/tests/live/QueryTest.php index af651ef4..34cd77a4 100644 --- a/tests/live/QueryTest.php +++ b/tests/live/QueryTest.php @@ -86,7 +86,7 @@ public function testQuery(): void { public function testQueryWhere(): void { $client = $this->getClient(); - $delimiter = $this->getManager()->get("options.delimiter"); + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); $folder_path = implode($delimiter, ['INBOX', 'search']); $folder = $client->getFolder($folder_path); @@ -239,7 +239,7 @@ protected function assertWhereSearchCriteria(Folder $folder, string $criteria, C $criteria = str_replace("CUSTOM ", "", $criteria); $expected = $value === null ? [$criteria] : [$criteria, $value]; if ($date === true && $value instanceof Carbon) { - $date_format = ClientManager::get('date_format', 'd M y'); + $date_format = $folder->getClient()->getConfig()->get('date_format', 'd M y'); $expected[1] = $value->format($date_format); } From 777c6d87a57f4302ef53d2c05305712c758506ce Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 12 Apr 2024 02:24:15 +0200 Subject: [PATCH 152/203] Sponsor list extended --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c110d48f..2eb4ef40 100755 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ foreach($folders as $folder){ ``` ## Sponsors +[![elb-BIT][ico-sponsor-elb-bit]][link-sponsor-elb-bit] [![Feline][ico-sponsor-feline]][link-sponsor-feline] @@ -229,4 +230,6 @@ The MIT License (MIT). Please see [License File][link-license] for more informat [ico-sponsor-feline]: https://cdn.feline.dk/public/feline.png -[link-sponsor-feline]: https://www.feline.dk \ No newline at end of file +[link-sponsor-feline]: https://www.feline.dk +[ico-sponsor-elb-bit]: https://www.elb-bit.de/user/themes/deliver/images/logo_small.png +[link-sponsor-elb-bit]: https://www.elb-bit.de?ref=webklex/php-imap \ No newline at end of file From 8696b28ba0899185f1250b54d10254f54afcb5bb Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 12 Apr 2024 02:26:12 +0200 Subject: [PATCH 153/203] Support for Carbon 3 added #483 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 37efc1c5..4956e549 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "ext-libxml": "*", "ext-zip": "*", "ext-fileinfo": "*", - "nesbot/carbon": "^2.62.1", + "nesbot/carbon": "^2.62.1|^3.2.4", "symfony/http-foundation": ">=2.8.0", "illuminate/pagination": ">=5.0.0" }, From 6d999438d29ed0bb920cd897b200a3a5fd6b6380 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 12 Apr 2024 02:26:41 +0200 Subject: [PATCH 154/203] Changelog updated --- CHANGELOG.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79cf049f..edeea50d 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,21 +6,43 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Fixed date issue if timezone is UT and a 2 digit year #429 (thanks @ferrisbuellers) +- Make the space optional after a comma separator #437 (thanks @marc0adam) +- Fix bug when multipart message getHTMLBody() method returns null #455 (thanks @michalkortas) +- Fix: Improve return type hints and return docblocks for query classes #470 (thanks @olliescase) +- Fix - Query - Chunked - Resolved infinite loop when start chunk > 1 #477 (thanks @NeekTheNook) ### Added - IMAP STATUS command support added `Folder::status()` #424 (thanks @InterLinked1) +- Add attributes and special flags #428 (thanks @sazanof) +- Better connection check for IMAP #449 (thanks @thin-k-design) +- Config handling moved into a new class `Config::class` to allow class serialization (sponsored by elb-BIT GmbH) +- Support for Carbon 3 added #483 ### Breaking changes - `Folder::getStatus()` no longer returns the results of `EXAMINE` but `STATUS` instead. If you want to use `EXAMINE` you can use the `Folder::examine()` method instead. - +- `ClientManager::class` has now longer access to all configs. Config handling has been moved to its own class `Config::class`. If you want to access the config you can use the retriever method `::getConfig()` instead. Example: `$client->getConfig()` or `$message->getConfig()`, etc. +- `ClientManager::get` isn't available anymore. Use the regular config accessor instead. Example: `$cm->getConfig()` +- `M̀essage::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$message->getOptions()` instead. +- `Attachment::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$attachment->getOptions()` instead. +- `Header::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$header->getOptions()` instead. +- `M̀essage::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$message->setOptions` instead. +- `Attachment::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$attachment->setOptions` instead. +- `Header::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$header->setOptions` instead. +- All protocol constructors now require a `Config::class` instance +- The `Client::class` constructors now require a `Config::class` instance +- The `Part::class` constructors now require a `Config::class` instance +- The `Header::class` constructors now require a `Config::class` instance +- The `Message::fromFile` method now requires a `Config::class` instance +- The `Message::fromString` method now requires a `Config::class` instance +- The `Message::boot` method now requires a `Config::class` instance ## [5.5.0] - 2023-06-28 ### Fixed - Error token length mismatch in `ImapProtocol::readResponse` #400 - Attachment name parsing fixed #410 #421 (thanks @nuernbergerA) - Additional Attachment name fallback added to prevent missing attachments -- Attachment id is now static (based on the raw part content) and now longer random +- Attachment id is now static (based on the raw part content) instead of random - Always parse the attachment description if it is available ### Added From a1b29eb354343867fb515c5821b4fa917592c726 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 01:19:05 +0100 Subject: [PATCH 155/203] Custom decoder support added --- CHANGELOG.md | 16 +- src/Attachment.php | 39 ++++- src/Config.php | 29 ++++ src/Decoder/AttachmentDecoder.php | 25 +++ src/Decoder/Decoder.php | 161 ++++++++++++++++++ src/Decoder/DecoderInterface.php | 52 ++++++ src/Decoder/HeaderDecoder.php | 98 +++++++++++ src/Decoder/MessageDecoder.php | 119 +++++++++++++ src/Exceptions/DecoderNotFoundException.php | 24 +++ src/Header.php | 177 ++++---------------- src/Message.php | 134 ++++----------- src/config/imap.php | 32 +++- tests/live/MessageTest.php | 4 +- 13 files changed, 648 insertions(+), 262 deletions(-) create mode 100644 src/Decoder/AttachmentDecoder.php create mode 100644 src/Decoder/Decoder.php create mode 100644 src/Decoder/DecoderInterface.php create mode 100644 src/Decoder/HeaderDecoder.php create mode 100644 src/Decoder/MessageDecoder.php create mode 100644 src/Exceptions/DecoderNotFoundException.php diff --git a/CHANGELOG.md b/CHANGELOG.md index edeea50d..f97aa391 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,24 +18,30 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Better connection check for IMAP #449 (thanks @thin-k-design) - Config handling moved into a new class `Config::class` to allow class serialization (sponsored by elb-BIT GmbH) - Support for Carbon 3 added #483 +- Custom decoder support added ### Breaking changes +- The decoder config has been moved from `options.decoder` to `decoding` and contains now the `decoder` class to used as well as their decoding fallbacks - `Folder::getStatus()` no longer returns the results of `EXAMINE` but `STATUS` instead. If you want to use `EXAMINE` you can use the `Folder::examine()` method instead. - `ClientManager::class` has now longer access to all configs. Config handling has been moved to its own class `Config::class`. If you want to access the config you can use the retriever method `::getConfig()` instead. Example: `$client->getConfig()` or `$message->getConfig()`, etc. -- `ClientManager::get` isn't available anymore. Use the regular config accessor instead. Example: `$cm->getConfig()` +- `ClientManager::get` isn't available anymore. Use the regular config accessor instead. Example: `$cm->getConfig()->get($key)` - `M̀essage::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$message->getOptions()` instead. - `Attachment::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$attachment->getOptions()` instead. - `Header::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$header->getOptions()` instead. - `M̀essage::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$message->setOptions` instead. - `Attachment::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$attachment->setOptions` instead. - `Header::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$header->setOptions` instead. -- All protocol constructors now require a `Config::class` instance -- The `Client::class` constructors now require a `Config::class` instance -- The `Part::class` constructors now require a `Config::class` instance -- The `Header::class` constructors now require a `Config::class` instance +- All protocol constructors now require a `Config::class` instance +- The `Client::class` constructor now require a `Config::class` instance +- The `Part::class` constructor now require a `Config::class` instance +- The `Header::class` constructor now require a `Config::class` instance - The `Message::fromFile` method now requires a `Config::class` instance - The `Message::fromString` method now requires a `Config::class` instance - The `Message::boot` method now requires a `Config::class` instance +- The `Message::decode` method has been removed. Use `Message::getDecoder()->decode($str)` instead. +- The `Message::getEncoding` method has been removed. Use `Message::getDecoder()->getEncoding($str)` instead. +- The `Message::convertEncoding` method has been removed. Use `Message::getDecoder()->convertEncoding()` instead. +- The `Header::decode` method has been removed. Use `Header::getDecoder()->decode($str)` instead. ## [5.5.0] - 2023-06-28 ### Fixed diff --git a/src/Attachment.php b/src/Attachment.php index 451dfe26..bcbe91c5 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -13,6 +13,8 @@ namespace Webklex\PHPIMAP; use Illuminate\Support\Str; +use Webklex\PHPIMAP\Decoder\DecoderInterface; +use Webklex\PHPIMAP\Exceptions\DecoderNotFoundException; use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; use Webklex\PHPIMAP\Exceptions\MethodNotFoundException; use Webklex\PHPIMAP\Support\Masks\AttachmentMask; @@ -78,6 +80,13 @@ class Attachment { /** @var Part $part */ protected Part $part; + /** + * Decoder instance + * + * @var DecoderInterface $decoder + */ + protected DecoderInterface $decoder; + /** * Attribute holder * @@ -109,11 +118,13 @@ class Attachment { * Attachment constructor. * @param Message $message * @param Part $part + * @throws DecoderNotFoundException */ public function __construct(Message $message, Part $part) { $this->message = $message; $this->config = $this->message->getConfig(); $this->options = $this->config->get('options'); + $this->decoder = $this->config->getDecoder("attachment"); $this->part = $part; $this->part_number = $part->part_number; @@ -213,7 +224,7 @@ protected function fetch(): void { $content = $this->part->content; $this->content_type = $this->part->content_type; - $this->content = $this->message->decodeString($content, $this->part->encoding); + $this->content = $this->decoder->decode($content, $this->part->encoding); // Create a hash of the raw part - this can be used to identify the attachment in the message context. However, // it is not guaranteed to be unique and collisions are possible. @@ -245,7 +256,7 @@ protected function fetch(): void { } if (($description = $this->part->description) !== null) { - $this->description = $this->part->getHeader()->decode($description); + $this->description = $this->part->getHeader()->getDecoder()->decode($description); } if (($name = $this->part->name) !== null) { @@ -300,9 +311,9 @@ public function decodeName(?string $name): string { } } - $decoder = $this->options['decoder']['message']; + $decoder = $this->decoder->getOptions()['message']; if (preg_match('/=\?([^?]+)\?(Q|B)\?(.+)\?=/i', $name, $matches)) { - $name = $this->part->getHeader()->decode($name); + $name = $this->part->getHeader()->getDecoder()->decode($name); } elseif ($decoder === 'utf-8' && extension_loaded('imap')) { $name = \imap_utf8($name); } @@ -452,4 +463,24 @@ public function mask(string $mask = null): mixed { throw new MaskNotFoundException("Unknown mask provided: " . $mask); } + + /** + * Get the decoder instance + * + * @return DecoderInterface + */ + public function getDecoder(): DecoderInterface { + return $this->decoder; + } + + /** + * Set the decoder instance + * @param DecoderInterface $decoder + * + * @return $this + */ + public function setDecoder(DecoderInterface $decoder): static { + $this->decoder = $decoder; + return $this; + } } diff --git a/src/Config.php b/src/Config.php index b5cf2599..f2f1a174 100644 --- a/src/Config.php +++ b/src/Config.php @@ -12,6 +12,9 @@ namespace Webklex\PHPIMAP; +use Webklex\PHPIMAP\Decoder\DecoderInterface; +use Webklex\PHPIMAP\Exceptions\DecoderNotFoundException; + /** * Class Config * @@ -87,6 +90,32 @@ public function set(string $key, mixed $value): void { } } + /** + * Get the decoder for a given name + * @param $name string Decoder name + * + * @return DecoderInterface + * @throws DecoderNotFoundException + */ + public function getDecoder(string $name): DecoderInterface { + $default_decoders = $this->get('decoding.decoder', [ + 'header' => \Webklex\PHPIMAP\Decoder\HeaderDecoder::class, + 'message' => \Webklex\PHPIMAP\Decoder\MessageDecoder::class, + 'attachment' => \Webklex\PHPIMAP\Decoder\AttachmentDecoder::class + ]); + $options = $this->get('decoding.options', [ + 'header' => 'utf-8', + 'message' => 'utf-8', + 'attachment' => 'utf-8', + ]); + if (isset($default_decoders[$name])) { + if (class_exists($default_decoders[$name])) { + return new $default_decoders[$name]($options); + } + } + throw new DecoderNotFoundException(); + } + /** * Get the mask for a given section * @param string $section section name such as "message" or "attachment" diff --git a/src/Decoder/AttachmentDecoder.php b/src/Decoder/AttachmentDecoder.php new file mode 100644 index 00000000..22780353 --- /dev/null +++ b/src/Decoder/AttachmentDecoder.php @@ -0,0 +1,25 @@ +options = array_merge([ + 'header' => 'utf-8', + 'message' => 'utf-8', + 'attachment' => 'utf-8', + ], $this->options); + } + + /** + * Decode a given value + * @param array|string|null $value + * @param string|null $encoding + * @return mixed + */ + public function decode(array|string|null $value, string $encoding = null): mixed { + return $value; + } + + /** + * Convert the encoding + * @param string $str The string to convert + * @param string $from The source encoding + * @param string $to The target encoding + * + * @return mixed|string + */ + public function convertEncoding(string $str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed { + $from = EncodingAliases::get($from, $this->fallback_encoding); + $to = EncodingAliases::get($to, $this->fallback_encoding); + + if ($from === $to) { + return $str; + } + + return EncodingAliases::convert($str, $from, $to); + } + + /** + * Decode MIME header elements + * @link https://php.net/manual/en/function.imap-mime-header-decode.php + * @param string $text The MIME text + * + * @return array Returns an array of objects. Each *object has two properties, charset and text. + */ + public function mimeHeaderDecode(string $text): array { + if (extension_loaded('imap')) { + $result = \imap_mime_header_decode($text); + return is_array($result) ? $result : []; + } + $charset = $this->getEncoding($text); + return [(object)[ + "charset" => $charset, + "text" => $this->convertEncoding($text, $charset) + ]]; + } + + /** + * Test if a given value is utf-8 encoded + * @param $value + * + * @return bool + */ + public static function isUTF8($value): bool { + return str_starts_with(strtolower($value), '=?utf-8?'); + } + + /** + * Check if a given pair of strings has been decoded + * @param $encoded + * @param $decoded + * + * @return bool + */ + public static function notDecoded($encoded, $decoded): bool { + return str_starts_with($decoded, '=?') + && strlen($decoded) - 2 === strpos($decoded, '?=') + && str_contains($encoded, $decoded); + } + + /** + * Set the configuration used for decoding + * @param array $config + * + * @return Decoder + */ + public function setOptions(array $config): static { + $this->options = $config; + return $this; + } + + /** + * Get the configuration used for decoding + * + * @return array + */ + public function getOptions(): array { + return $this->options; + } + + /** + * Get the fallback encoding + * + * @return string + */ + public function getFallbackEncoding(): string { + return $this->fallback_encoding; + } + + /** + * Set the fallback encoding + * + * @param string $fallback_encoding + * @return Decoder + */ + public function setFallbackEncoding(string $fallback_encoding): static { + $this->fallback_encoding = $fallback_encoding; + return $this; + } +} \ No newline at end of file diff --git a/src/Decoder/DecoderInterface.php b/src/Decoder/DecoderInterface.php new file mode 100644 index 00000000..c44f2118 --- /dev/null +++ b/src/Decoder/DecoderInterface.php @@ -0,0 +1,52 @@ +decodeHeaderArray($value); + } + $original_value = $value; + $decoder = $this->options['header']; + + if ($value !== null) { + if ($decoder === 'utf-8') { + $decoded_values = $this->mimeHeaderDecode($value); + $tempValue = ""; + foreach ($decoded_values as $decoded_value) { + $tempValue .= $this->convertEncoding($decoded_value->text, $decoded_value->charset); + } + if ($tempValue) { + $value = $tempValue; + } else if (extension_loaded('imap')) { + $value = \imap_utf8($value); + } else if (function_exists('iconv_mime_decode')) { + $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); + } else { + $value = mb_decode_mimeheader($value); + } + } elseif ($decoder === 'iconv') { + $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); + } else if (self::isUTF8($value)) { + $value = mb_decode_mimeheader($value); + } + + if (self::notDecoded($original_value, $value)) { + $value = $this->convertEncoding($original_value, $this->getEncoding($original_value)); + } + } + + return $value; + } + + /** + * Get the encoding of a given abject + * @param object|string $structure + * + * @return string + */ + public function getEncoding(object|string $structure): string { + if (property_exists($structure, 'parameters')) { + foreach ($structure->parameters as $parameter) { + if (strtolower($parameter->attribute) == "charset") { + return EncodingAliases::get($parameter->value, $this->fallback_encoding); + } + } + } elseif (property_exists($structure, 'charset')) { + return EncodingAliases::get($structure->charset, $this->fallback_encoding); + } elseif (is_string($structure) === true) { + $result = mb_detect_encoding($structure); + return $result === false ? $this->fallback_encoding : $result; + } + + return $this->fallback_encoding; + } + + + /** + * Decode a given array + * @param array $values + * + * @return array + */ + private function decodeHeaderArray(array $values): array { + foreach ($values as $key => $value) { + $values[$key] = $this->decode($value); + } + return $values; + } + +} \ No newline at end of file diff --git a/src/Decoder/MessageDecoder.php b/src/Decoder/MessageDecoder.php new file mode 100644 index 00000000..5d4178bc --- /dev/null +++ b/src/Decoder/MessageDecoder.php @@ -0,0 +1,119 @@ +decode($item); + }, $value); + } + + switch ($encoding) { + case IMAP::MESSAGE_ENC_BINARY: + if (extension_loaded('imap')) { + return base64_decode(\imap_binary($value)); + } + return base64_decode($value); + case IMAP::MESSAGE_ENC_BASE64: + return base64_decode($value); + case IMAP::MESSAGE_ENC_QUOTED_PRINTABLE: + return quoted_printable_decode($value); + case IMAP::MESSAGE_ENC_8BIT: + case IMAP::MESSAGE_ENC_7BIT: + case IMAP::MESSAGE_ENC_OTHER: + default: + return $value; + } + } + + /** + * Get the encoding of a given abject + * @param object|string $structure + * + * @return string + */ + public function getEncoding(object|string $structure): string { + if (property_exists($structure, 'parameters')) { + foreach ($structure->parameters as $parameter) { + if (strtolower($parameter->attribute) == "charset") { + return EncodingAliases::get($parameter->value, "ISO-8859-2"); + } + } + } elseif (property_exists($structure, 'charset')) { + return EncodingAliases::get($structure->charset, "ISO-8859-2"); + } elseif (is_string($structure) === true) { + return EncodingAliases::detectEncoding($structure); + } + + return $this->fallback_encoding; + } + + + /** + * Convert the encoding + * @param $str + * @param string $from + * @param string $to + * + * @return mixed|string + */ + public function convertEncoding($str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed { + $from = EncodingAliases::get($from); + $to = EncodingAliases::get($to); + + if ($from === $to) { + return $str; + } + + // We don't need to do convertEncoding() if charset is ASCII (us-ascii): + // ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded + // https://stackoverflow.com/a/11303410 + // + // us-ascii is the same as ASCII: + // ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA) + // prefers the updated name US-ASCII, which clarifies that this system was developed in the US and + // based on the typographical symbols predominantly in use there. + // https://en.wikipedia.org/wiki/ASCII + // + // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken. + if (strtolower($from ?? '') == 'us-ascii' && $to == 'UTF-8') { + return $str; + } + + if (function_exists('iconv') && !EncodingAliases::isUtf7($from) && !EncodingAliases::isUtf7($to)) { + try { + return iconv($from, $to.'//IGNORE', $str); + } catch (Exception) { + return @iconv($from, $to, $str); + } + } else { + if (!$from) { + return mb_convert_encoding($str, $to); + } + return mb_convert_encoding($str, $to, $from); + } + } + +} \ No newline at end of file diff --git a/src/Exceptions/DecoderNotFoundException.php b/src/Exceptions/DecoderNotFoundException.php new file mode 100644 index 00000000..0acdb9c8 --- /dev/null +++ b/src/Exceptions/DecoderNotFoundException.php @@ -0,0 +1,24 @@ +decoder = $config->getDecoder("header"); $this->raw = $raw_header; $this->config = $config; $this->options = $this->config->get('options'); @@ -202,7 +206,7 @@ protected function parse(): void { $this->extractAddresses($header); if (property_exists($header, 'subject')) { - $this->set("subject", $this->decode($header->subject)); + $this->set("subject", $this->decoder->decode($header->subject)); } if (property_exists($header, 'references')) { $this->set("references", array_map(function($item) { @@ -331,147 +335,6 @@ public function rfc822_parse_headers($raw_headers): object { return (object)array_merge($headers, $imap_headers); } - /** - * Decode MIME header elements - * @link https://php.net/manual/en/function.imap-mime-header-decode.php - * @param string $text The MIME text - * - * @return array The decoded elements are returned in an array of objects, where each - * object has two properties, charset and text. - */ - public function mime_header_decode(string $text): array { - if (extension_loaded('imap')) { - $result = \imap_mime_header_decode($text); - return is_array($result) ? $result : []; - } - $charset = $this->getEncoding($text); - return [(object)[ - "charset" => $charset, - "text" => $this->convertEncoding($text, $charset) - ]]; - } - - /** - * Check if a given pair of strings has been decoded - * @param $encoded - * @param $decoded - * - * @return bool - */ - private function notDecoded($encoded, $decoded): bool { - return str_starts_with($decoded, '=?') - && strlen($decoded) - 2 === strpos($decoded, '?=') - && str_contains($encoded, $decoded); - } - - /** - * Convert the encoding - * @param $str - * @param string $from - * @param string $to - * - * @return mixed|string - */ - public function convertEncoding($str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed { - $from = EncodingAliases::get($from, $this->fallback_encoding); - $to = EncodingAliases::get($to, $this->fallback_encoding); - - if ($from === $to) { - return $str; - } - - return EncodingAliases::convert($str, $from, $to); - } - - /** - * Get the encoding of a given abject - * @param object|string $structure - * - * @return string - */ - public function getEncoding(object|string $structure): string { - if (property_exists($structure, 'parameters')) { - foreach ($structure->parameters as $parameter) { - if (strtolower($parameter->attribute) == "charset") { - return EncodingAliases::get($parameter->value, $this->fallback_encoding); - } - } - } elseif (property_exists($structure, 'charset')) { - return EncodingAliases::get($structure->charset, $this->fallback_encoding); - } elseif (is_string($structure) === true) { - $result = mb_detect_encoding($structure); - return $result === false ? $this->fallback_encoding : $result; - } - - return $this->fallback_encoding; - } - - /** - * Test if a given value is utf-8 encoded - * @param $value - * - * @return bool - */ - private function is_uft8($value): bool { - return str_starts_with(strtolower($value), '=?utf-8?'); - } - - /** - * Try to decode a specific header - * @param mixed $value - * - * @return mixed - */ - public function decode(mixed $value): mixed { - if (is_array($value)) { - return $this->decodeArray($value); - } - $original_value = $value; - $decoder = $this->options['decoder']['message']; - - if ($value !== null) { - if ($decoder === 'utf-8') { - $decoded_values = $this->mime_header_decode($value); - $tempValue = ""; - foreach ($decoded_values as $decoded_value) { - $tempValue .= $this->convertEncoding($decoded_value->text, $decoded_value->charset); - } - if ($tempValue) { - $value = $tempValue; - } else if (extension_loaded('imap')) { - $value = \imap_utf8($value); - } else if (function_exists('iconv_mime_decode')) { - $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); - } else { - $value = mb_decode_mimeheader($value); - } - } elseif ($decoder === 'iconv') { - $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); - } else if ($this->is_uft8($value)) { - $value = mb_decode_mimeheader($value); - } - - if ($this->notDecoded($original_value, $value)) { - $value = $this->convertEncoding($original_value, $this->getEncoding($original_value)); - } - } - - return $value; - } - - /** - * Decode a given array - * @param array $values - * - * @return array - */ - private function decodeArray(array $values): array { - foreach ($values as $key => $value) { - $values[$key] = $this->decode($value); - } - return $values; - } - /** * Try to extract the priority from a given raw header string */ @@ -582,11 +445,11 @@ private function parseAddresses($list): array { if (!property_exists($address, 'personal')) { $address->personal = false; } else { - $personalParts = $this->mime_header_decode($address->personal); + $personalParts = $this->decoder->mimeHeaderDecode($address->personal); $address->personal = ''; foreach ($personalParts as $p) { - $address->personal .= $this->convertEncoding($p->text, $this->getEncoding($p)); + $address->personal .= $this->decoder->convertEncoding($p->text, $this->decoder->getEncoding($p)); } if (str_starts_with($address->personal, "'")) { @@ -843,4 +706,24 @@ public function getConfig(): Config { return $this->config; } + /** + * Get the decoder instance + * + * @return DecoderInterface + */ + public function getDecoder(): DecoderInterface { + return $this->decoder; + } + + /** + * Set the decoder instance + * @param DecoderInterface $decoder + * + * @return $this + */ + public function setDecoder(DecoderInterface $decoder): static { + $this->decoder = $decoder; + return $this; + } + } diff --git a/src/Message.php b/src/Message.php index 10b96018..bae2acfe 100755 --- a/src/Message.php +++ b/src/Message.php @@ -13,10 +13,14 @@ namespace Webklex\PHPIMAP; use Exception; +use Illuminate\Support\Str; use ReflectionClass; use ReflectionException; +use Webklex\PHPIMAP\Decoder\Decoder; +use Webklex\PHPIMAP\Decoder\DecoderInterface; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\DecoderNotFoundException; use Webklex\PHPIMAP\Exceptions\EventNotFoundException; use Webklex\PHPIMAP\Exceptions\FolderFetchingException; use Webklex\PHPIMAP\Exceptions\GetMessagesFailedException; @@ -35,7 +39,6 @@ use Webklex\PHPIMAP\Support\AttachmentCollection; use Webklex\PHPIMAP\Support\FlagCollection; use Webklex\PHPIMAP\Support\Masks\MessageMask; -use Illuminate\Support\Str; use Webklex\PHPIMAP\Support\MessageCollection; use Webklex\PHPIMAP\Traits\HasEvents; @@ -111,6 +114,13 @@ class Message { */ protected Config $config; + /** + * Decoder instance + * + * @var DecoderInterface $decoder + */ + protected DecoderInterface $decoder; + /** * Attribute holder * @@ -374,11 +384,13 @@ public static function fromString(string $blob, Config $config = null): Message /** * Boot a new instance * @param ?Config $config + * @throws DecoderNotFoundException */ public function boot(Config $config = null): void { $this->attributes = []; $this->client = null; $this->config = $config ?? Config::make(); + $this->decoder = $this->config->getDecoder("message"); $this->options = $this->config->get('options'); $this->available_flags = $this->config->get('flags'); @@ -732,9 +744,9 @@ private function fetchPart(Part $part): void { if ($part->isAttachment()) { $this->fetchAttachment($part); } else { - $encoding = $this->getEncoding($part); + $encoding = $this->decoder->getEncoding($part); - $content = $this->decodeString($part->content, $part->encoding); + $content = $this->decoder->decode($part->content, $part->encoding); // We don't need to do convertEncoding() if charset is ASCII (us-ascii): // ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded @@ -748,7 +760,7 @@ private function fetchPart(Part $part): void { // // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken. if ($encoding != 'us-ascii') { - $content = $this->convertEncoding($content, $encoding); + $content = $this->decoder->convertEncoding($content, $encoding); } $this->addBody($part->subtype ?? '', $content); @@ -859,100 +871,6 @@ public function setFetchFlagsOption($option): Message { return $this; } - /** - * Decode a given string - * @param $string - * @param $encoding - * - * @return string - */ - public function decodeString($string, $encoding): string { - switch ($encoding) { - case IMAP::MESSAGE_ENC_BINARY: - if (extension_loaded('imap')) { - return base64_decode(\imap_binary($string)); - } - return base64_decode($string); - case IMAP::MESSAGE_ENC_BASE64: - return base64_decode($string); - case IMAP::MESSAGE_ENC_QUOTED_PRINTABLE: - return quoted_printable_decode($string); - case IMAP::MESSAGE_ENC_8BIT: - case IMAP::MESSAGE_ENC_7BIT: - case IMAP::MESSAGE_ENC_OTHER: - default: - return $string; - } - } - - /** - * Convert the encoding - * @param $str - * @param string $from - * @param string $to - * - * @return mixed|string - */ - public function convertEncoding($str, string $from = "ISO-8859-2", string $to = "UTF-8"): mixed { - - $from = EncodingAliases::get($from); - $to = EncodingAliases::get($to); - - if ($from === $to) { - return $str; - } - - // We don't need to do convertEncoding() if charset is ASCII (us-ascii): - // ASCII is a subset of UTF-8, so all ASCII files are already UTF-8 encoded - // https://stackoverflow.com/a/11303410 - // - // us-ascii is the same as ASCII: - // ASCII is the traditional name for the encoding system; the Internet Assigned Numbers Authority (IANA) - // prefers the updated name US-ASCII, which clarifies that this system was developed in the US and - // based on the typographical symbols predominantly in use there. - // https://en.wikipedia.org/wiki/ASCII - // - // convertEncoding() function basically means convertToUtf8(), so when we convert ASCII string into UTF-8 it gets broken. - if (strtolower($from ?? '') == 'us-ascii' && $to == 'UTF-8') { - return $str; - } - - if (function_exists('iconv') && !EncodingAliases::isUtf7($from) && !EncodingAliases::isUtf7($to)) { - try { - return iconv($from, $to.'//IGNORE', $str); - } catch (Exception) { - return @iconv($from, $to, $str); - } - } else { - if (!$from) { - return mb_convert_encoding($str, $to); - } - return mb_convert_encoding($str, $to, $from); - } - } - - /** - * Get the encoding of a given abject - * @param object|string $structure - * - * @return string - */ - public function getEncoding(object|string $structure): string { - if (property_exists($structure, 'parameters')) { - foreach ($structure->parameters as $parameter) { - if (strtolower($parameter->attribute) == "charset") { - return EncodingAliases::get($parameter->value, "ISO-8859-2"); - } - } - } elseif (property_exists($structure, 'charset')) { - return EncodingAliases::get($structure->charset, "ISO-8859-2"); - } elseif (is_string($structure) === true) { - return EncodingAliases::detectEncoding($structure); - } - - return 'UTF-8'; - } - /** * Get the messages folder * @@ -1727,6 +1645,26 @@ public function setSequenceId($uid, int $msglist = null): void { } } + /** + * Get the decoder instance + * + * @return DecoderInterface + */ + public function getDecoder(): DecoderInterface { + return $this->decoder; + } + + /** + * Set the decoder instance + * @param DecoderInterface $decoder + * + * @return $this + */ + public function setDecoder(DecoderInterface $decoder): static { + $this->decoder = $decoder; + return $this; + } + /** * Safe the entire message in a file * @param $filename diff --git a/src/config/imap.php b/src/config/imap.php index 590d27cb..7c184435 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -136,9 +136,6 @@ | error: "Kerberos error: No credentials cache | file found (try running kinit) (...)" | or ['GSSAPI','PLAIN'] if you are using outlook mail - | -Decoder options (currently only the message subject and attachment name decoder can be set) - | 'utf-8' - Uses imap_utf8($string) to decode a string - | 'mimeheader' - Uses mb_decode_mimeheader($string) to decode a string | */ 'options' => [ @@ -163,12 +160,35 @@ "sent" => "INBOX/Sent", "trash" => "INBOX/Trash", ], - 'decoder' => [ + 'open' => [ + // 'DISABLE_AUTHENTICATOR' => 'GSSAPI' + ] + ], + + /** + * |-------------------------------------------------------------------------- + * | Available IMAP options + * |-------------------------------------------------------------------------- + * | + * | Available php imap config parameters are listed below + * | -options: Decoder options (currently only the message subject and attachment name decoder can be set) + * | 'utf-8' - Uses imap_utf8($string) to decode a string + * | 'mimeheader' - Uses mb_decode_mimeheader($string) to decode a string + * | -decoder: Decoder to be used. Can be replaced by custom decoders if needed. + * | 'header' - HeaderDecoder + * | 'message' - MessageDecoder + * | 'attachment' - AttachmentDecoder + */ + 'decoding' => [ + 'options' => [ + 'header' => 'utf-8', // mimeheader 'message' => 'utf-8', // mimeheader 'attachment' => 'utf-8' // mimeheader ], - 'open' => [ - // 'DISABLE_AUTHENTICATOR' => 'GSSAPI' + 'decoder' => [ + 'header' => \Webklex\PHPIMAP\Decoder\HeaderDecoder::class, + 'message' => \Webklex\PHPIMAP\Decoder\MessageDecoder::class, + 'attachment' => \Webklex\PHPIMAP\Decoder\AttachmentDecoder::class ] ], diff --git a/tests/live/MessageTest.php b/tests/live/MessageTest.php index 30e953ec..841b125b 100644 --- a/tests/live/MessageTest.php +++ b/tests/live/MessageTest.php @@ -97,7 +97,7 @@ protected function getDefaultMessage() : Message { */ public function testConvertEncoding(): void { $message = $this->getDefaultMessage(); - self::assertEquals("Entwürfe+", $message->convertEncoding("Entw&APw-rfe+", "UTF7-IMAP", "UTF-8")); + self::assertEquals("Entwürfe+", $message->getDecoder()->convertEncoding("Entw&APw-rfe+", "UTF7-IMAP", "UTF-8")); // Cleanup self::assertTrue($message->delete()); @@ -961,7 +961,7 @@ public function testDecodeString(): void { $message = $this->getDefaultMessage(); $string = '

Test

'; - self::assertEquals('

Test

', $message->decodeString($string, IMAP::MESSAGE_ENC_QUOTED_PRINTABLE)); + self::assertEquals('

Test

', $message->getDecoder()->decode($string, IMAP::MESSAGE_ENC_QUOTED_PRINTABLE)); // Cleanup self::assertTrue($message->delete()); From 72d5812839cff2050159ec2144c9d3e3946a8243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=BCrnberger?= Date: Fri, 17 Jan 2025 01:31:31 +0100 Subject: [PATCH 156/203] Failing test: Attachment with symbols in filename (#436) * add failing test with symbols in filename * use proper test case class --- tests/issues/Issue410Test.php | 22 +++++++++++++++++----- tests/messages/issue-410symbols.eml | 22 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 tests/messages/issue-410symbols.eml diff --git a/tests/issues/Issue410Test.php b/tests/issues/Issue410Test.php index d02724ca..458c8a13 100644 --- a/tests/issues/Issue410Test.php +++ b/tests/issues/Issue410Test.php @@ -13,14 +13,14 @@ namespace Tests\issues; use PHPUnit\Framework\TestCase; +use Tests\fixtures\FixtureTestCase; use Webklex\PHPIMAP\ClientManager; use Webklex\PHPIMAP\Message; -class Issue410Test extends TestCase { +class Issue410Test extends FixtureTestCase { public function testIssueEmail() { - $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-410.eml"]); - $message = Message::fromFile($filename); + $message = $this->getFixture("issue-410.eml"); self::assertSame("☆第132号 「ガーデン&エクステリア」専門店のためのQ&Aサロン 【月刊エクステリア・ワーク】", (string)$message->subject); @@ -34,8 +34,7 @@ public function testIssueEmail() { } public function testIssueEmailB() { - $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-410b.eml"]); - $message = Message::fromFile($filename); + $message = $this->getFixture("issue-410b.eml"); self::assertSame("386 - 400021804 - 19., Heiligenstädter Straße 80 - 0819306 - Anfrage Vergabevorschlag", (string)$message->subject); @@ -49,4 +48,17 @@ public function testIssueEmailB() { self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->name); } + public function testIssueEmailSymbols() { + $message = $this->getFixture("issue-410symbols.eml"); + + $attachments = $message->getAttachments(); + + self::assertSame(1, $attachments->count()); + + $attachment = $attachments->first(); + self::assertSame("Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf", $attachment->description); + self::assertSame("Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf", $attachment->filename); + self::assertSame("Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf", $attachment->name); + } + } \ No newline at end of file diff --git a/tests/messages/issue-410symbols.eml b/tests/messages/issue-410symbols.eml new file mode 100644 index 00000000..b83a2f26 --- /dev/null +++ b/tests/messages/issue-410symbols.eml @@ -0,0 +1,22 @@ +From: from@there.com +To: to@here.com +Subject: =?iso-8859-1?Q?386_-_400021804_-_19.,_Heiligenst=E4dter_Stra=DFe_80_-_081?= + =?iso-8859-1?Q?9306_-_Anfrage_Vergabevorschlag?= +Date: Wed, 13 Sep 2017 13:05:45 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------B832AF745285AEEC6D5AEE42" + +Hi +--------------B832AF745285AEEC6D5AEE42 +Content-Type: application/pdf; name="Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf" +Content-Description: Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf +Content-Disposition: attachment; + filename="Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf"; size=3439313; + creation-date="Tue, 12 Sep 2023 06:53:03 GMT"; + modification-date="Tue, 12 Sep 2023 08:18:16 GMT" +Content-ID: <34A0EDD24A954140A472605B7526F190@there.com> +Content-Transfer-Encoding: base64 + +SGkh +--------------B832AF745285AEEC6D5AEE42-- \ No newline at end of file From e5ad66267382f319f385131cefe5336692a54486 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 03:16:54 +0100 Subject: [PATCH 157/203] fixed Attachment with symbols in filename #436 --- CHANGELOG.md | 1 + src/Header.php | 132 +++++++++++------- tests/HeaderTest.php | 11 +- tests/MessageTest.php | 6 +- tests/fixtures/EmbeddedEmailTest.php | 1 - ...lWithoutContentDispositionEmbeddedTest.php | 1 - ...ddedEmailWithoutContentDispositionTest.php | 1 - tests/fixtures/ExampleBounceTest.php | 2 - tests/fixtures/MultipartWithoutBodyTest.php | 1 - tests/fixtures/UndefinedCharsetHeaderTest.php | 1 - tests/issues/Issue410Test.php | 49 ++++++- 11 files changed, 144 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f97aa391..b006fb36 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Fix bug when multipart message getHTMLBody() method returns null #455 (thanks @michalkortas) - Fix: Improve return type hints and return docblocks for query classes #470 (thanks @olliescase) - Fix - Query - Chunked - Resolved infinite loop when start chunk > 1 #477 (thanks @NeekTheNook) +- Attachment with symbols in filename #436 ### Added - IMAP STATUS command support added `Folder::status()` #424 (thanks @InterLinked1) diff --git a/src/Header.php b/src/Header.php index ef1a348c..d6392906 100644 --- a/src/Header.php +++ b/src/Header.php @@ -484,61 +484,95 @@ private function extractHeaderExtensions(): void { $value = (string)$value; } // Only parse strings and don't parse any attributes like the user-agent - if (!in_array($key, ["user-agent", "subject"])) { - if (($pos = strpos($value, ";")) !== false) { - $original = substr($value, 0, $pos); - $this->set($key, trim(rtrim($original))); - - // Get all potential extensions - $extensions = explode(";", substr($value, $pos + 1)); - $previousKey = null; - $previousValue = ''; - - foreach ($extensions as $extension) { - if (($pos = strpos($extension, "=")) !== false) { - $key = substr($extension, 0, $pos); - $key = trim(rtrim(strtolower($key))); - - $matches = []; - - if (preg_match('/^(?P\w+)\*/', $key, $matches) !== 0) { - $key = $matches['key_name']; - $previousKey = $key; - - $value = substr($extension, $pos + 1); - $value = str_replace('"', "", $value); - $previousValue .= trim(rtrim($value)); - - continue; - } - - if ( - $previousKey !== null - && $previousKey !== $key - && isset($this->attributes[$previousKey]) === false - ) { - $this->set($previousKey, $previousValue); - - $previousValue = ''; - } - - if (isset($this->attributes[$key]) === false) { - $value = substr($extension, $pos + 1); - $value = str_replace('"', "", $value); - $value = trim(rtrim($value)); - - $this->set($key, $value); - } - - $previousKey = $key; + if (!in_array($key, ["user-agent", "subject", "received"])) { + if (str_contains($value, ";") && str_contains($value, "=")) { + $_attributes = $this->read_attribute($value); + foreach($_attributes as $_key => $_value) { + if($_value === "") { + $this->set($key, $_key); + } + if (!isset($this->attributes[$_key])) { + $this->set($_key, $_value); } } - if ($previousValue !== '') { - $this->set($previousKey, $previousValue); + } + } + } + } + + /** + * Read a given attribute string + * - this isn't pretty, but it works - feel free to improve :) + * @param string $raw_attribute + * @return array + */ + private function read_attribute(string $raw_attribute): array { + $attributes = []; + $key = ''; + $value = ''; + $inside_word = false; + $inside_key = true; + $escaped = false; + foreach (str_split($raw_attribute) as $char) { + if($escaped) { + $escaped = false; + continue; + } + if($inside_word) { + if($char === '\\') { + $escaped = true; + }elseif($char === "\"" && $value !== "") { + $inside_word = false; + }else{ + $value .= $char; + } + }else{ + if($inside_key) { + if($char === '"') { + $inside_word = true; + }elseif($char === ';'){ + $attributes[$key] = $value; + $key = ''; + $value = ''; + $inside_key = true; + }elseif($char === '=') { + $inside_key = false; + }else{ + $key .= $char; + } + }else{ + if($char === '"' && $value === "") { + $inside_word = true; + }elseif($char === ';'){ + $attributes[$key] = $value; + $key = ''; + $value = ''; + $inside_key = true; + }else{ + $value .= $char; } } } } + $attributes[$key] = $value; + $result = []; + + foreach($attributes as $key => $value) { + if (($pos = strpos($key, "*")) !== false) { + $key = substr($key, 0, $pos); + } + $key = trim(rtrim(strtolower($key))); + + if(!isset($result[$key])) { + $result[$key] = ""; + } + $value = trim(rtrim(str_replace(["\r", "\n"], "", $value))); + if(str_starts_with($value, "\"") && str_ends_with($value, "\"")) { + $value = substr($value, 1, -1); + } + $result[$key] .= $value; + } + return $result; } /** diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index 65e31d7f..9de25004 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -59,6 +59,13 @@ public function testHeaderParsing(): void { $to = $header->get("to")->first(); self::assertSame($raw_header, $header->raw); + self::assertSame([ + 0 => 'from mx.domain.tld by localhost with LMTP id SABVMNfGqWP+PAAA0J78UA (envelope-from ) for ; Mon, 26 Dec 2022 17:07:51 +0100', + 1 => 'from localhost (localhost [127.0.0.1]) by mx.domain.tld (Postfix) with ESMTP id C3828140227 for ; Mon, 26 Dec 2022 17:07:51 +0100 (CET)', + 2 => 'from mx.domain.tld ([127.0.0.1]) by localhost (mx.domain.tld [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id JcIS9RuNBTNx for ; Mon, 26 Dec 2022 17:07:21 +0100 (CET)', + 3 => 'from smtp.github.com (out-26.smtp.github.com [192.30.252.209]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mx.domain.tld (Postfix) with ESMTPS id 6410B13FEB2 for ; Mon, 26 Dec 2022 17:07:21 +0100 (CET)', + 4 => 'from github-lowworker-891b8d2.va3-iad.github.net (github-lowworker-891b8d2.va3-iad.github.net [10.48.109.104]) by smtp.github.com (Postfix) with ESMTP id 176985E0200 for ; Mon, 26 Dec 2022 08:07:14 -0800 (PST)', + ], $header->get("received")->toArray()); self::assertInstanceOf(Attribute::class, $subject); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", $subject->toString()); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", (string)$header->subject); @@ -66,7 +73,7 @@ public function testHeaderParsing(): void { self::assertSame("return_path", $returnPath->getName()); self::assertSame("-4.299", (string)$header->get("X-Spam-Score")); self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$header->get("Message-ID")); - self::assertSame(6, $header->get("received")->count()); + self::assertSame(5, $header->get("received")->count()); self::assertSame(IMAP::MESSAGE_PRIORITY_UNKNOWN, (int)$header->get("priority")()); self::assertSame("Username", $from->personal); @@ -84,7 +91,7 @@ public function testHeaderParsing(): void { self::assertInstanceOf(Carbon::class, $date); self::assertSame("2022-12-26 08:07:14 GMT-0800", $date->format("Y-m-d H:i:s T")); - self::assertSame(48, count($header->getAttributes())); + self::assertSame(50, count($header->getAttributes())); } public function testRfc822ParseHeaders() { diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 1bc9ce6f..72e32f5e 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -124,7 +124,7 @@ public function testMessage(): void { self::assertSame("return_path", $returnPath->getName()); self::assertSame("-4.299", (string)$message->get("X-Spam-Score")); self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$message->get("Message-ID")); - self::assertSame(6, $message->get("received")->count()); + self::assertSame(5, $message->get("received")->count()); self::assertSame(IMAP::MESSAGE_PRIORITY_UNKNOWN, (int)$message->get("priority")()); } @@ -186,7 +186,7 @@ public function testLoadMessageFromFile(): void { self::assertSame("return_path", $returnPath->getName()); self::assertSame("-4.299", (string)$message->get("X-Spam-Score")); self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$message->get("Message-ID")); - self::assertSame(6, $message->get("received")->count()); + self::assertSame(5, $message->get("received")->count()); self::assertSame(IMAP::MESSAGE_PRIORITY_UNKNOWN, (int)$message->get("priority")()); self::assertNull($message->getClient()); @@ -205,7 +205,7 @@ public function testLoadMessageFromFile(): void { self::assertSame("return_path", $returnPath->getName()); self::assertSame("1.103", (string)$message->get("X-Spam-Score")); self::assertSame("d3a5e91963cb805cee975687d5acb1c6@swift.generated", (string)$message->get("Message-ID")); - self::assertSame(5, $message->get("received")->count()); + self::assertSame(4, $message->get("received")->count()); self::assertSame(IMAP::MESSAGE_PRIORITY_HIGHEST, (int)$message->get("priority")()); self::assertNull($message->getClient()); diff --git a/tests/fixtures/EmbeddedEmailTest.php b/tests/fixtures/EmbeddedEmailTest.php index fb6e2492..024a8885 100644 --- a/tests/fixtures/EmbeddedEmailTest.php +++ b/tests/fixtures/EmbeddedEmailTest.php @@ -32,7 +32,6 @@ public function testFixture() : void { self::assertEquals("embedded message", $message->subject); self::assertEquals([ 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', - 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' ], $message->received->toArray()); self::assertEquals("7e5798da5747415e5b82fdce042ab2a6@cerstor.cz", $message->message_id); self::assertEquals("demo@cerstor.cz", $message->return_path); diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php index 7d3fb2d0..a620936b 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php @@ -32,7 +32,6 @@ public function testFixture() : void { self::assertEquals("embedded_message_subject", $message->subject); self::assertEquals([ 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', - 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' ], $message->received->toArray()); self::assertEquals("AC39946EBF5C034B87BABD5343E96979012671D40E38@VM002.cerk.cc", $message->message_id); self::assertEquals("pl-PL, nl-NL", $message->accept_language); diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php index 603a956d..36287a6b 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -32,7 +32,6 @@ public function testFixture() : void { self::assertEquals("Subject", $message->subject); self::assertEquals([ 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', - 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' ], $message->received->toArray()); self::assertEquals("AC39946EBF5C034B87BABD5343E96979012671D9F7E4@VM002.cerk.cc", $message->message_id); self::assertEquals("pl-PL, nl-NL", $message->accept_language); diff --git a/tests/fixtures/ExampleBounceTest.php b/tests/fixtures/ExampleBounceTest.php index d2f418a9..c92d35c6 100644 --- a/tests/fixtures/ExampleBounceTest.php +++ b/tests/fixtures/ExampleBounceTest.php @@ -36,7 +36,6 @@ public function testFixture(): void { 2 => 'from [192.168.0.10] (helo=sslproxy01.your-server.de) by somewhere06.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-000LYP-9R for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', 3 => 'from localhost ([127.0.0.1] helo=sslproxy01.your-server.de) by sslproxy01.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-0008gy-7x for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', 4 => 'from Debian-exim by sslproxy01.your-server.de with local (Exim 4.92) id 1pXaXO-0008gb-6g for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', - 5 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>)', ], $message->received->all()); self::assertEquals("demo@foo.de", $message->envelope_to); self::assertEquals("Thu, 02 Mar 2023 05:27:29 +0100", $message->delivery_date); @@ -50,7 +49,6 @@ public function testFixture(): void { 2 => 'from [192.168.0.10] (helo=sslproxy01.your-server.de) by somewhere06.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-000LYP-9R for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', 3 => 'from localhost ([127.0.0.1] helo=sslproxy01.your-server.de) by sslproxy01.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-0008gy-7x for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', 4 => 'from Debian-exim by sslproxy01.your-server.de with local (Exim 4.92) id 1pXaXO-0008gb-6g for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', - 5 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>)', ], $message->received->all()); self::assertEquals("ding@ding.de", $message->x_failed_recipients); self::assertEquals("auto-replied", $message->auto_submitted); diff --git a/tests/fixtures/MultipartWithoutBodyTest.php b/tests/fixtures/MultipartWithoutBodyTest.php index 9989b3e7..01112bb7 100644 --- a/tests/fixtures/MultipartWithoutBodyTest.php +++ b/tests/fixtures/MultipartWithoutBodyTest.php @@ -37,7 +37,6 @@ public function testFixture() : void { 0 => 'from AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS; Sat, 11 Mar 2023 08:24:33 +0000', 1 => 'from omef0ahNgeoJu.eurprd02.prod.outlook.com (2603:10a6:10:33c::12) by AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6178.19; Sat, 11 Mar 2023 08:24:31 +0000', 2 => 'from omef0ahNgeoJu.eurprd02.prod.outlook.com ([fe80::38c0:9c40:7fc6:93a7]) by omef0ahNgeoJu.eurprd02.prod.outlook.com ([fe80::38c0:9c40:7fc6:93a7%7]) with mapi id 15.20.6178.019; Sat, 11 Mar 2023 08:24:31 +0000', - 3 => 'from AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS', ], $message->received->all()); self::assertEquals("This mail will not contain a body", $message->thread_topic); self::assertEquals("AdlT8uVmpHPvImbCRM6E9LODIvAcQA==", $message->thread_index); diff --git a/tests/fixtures/UndefinedCharsetHeaderTest.php b/tests/fixtures/UndefinedCharsetHeaderTest.php index acb3029c..b2f9d8fb 100644 --- a/tests/fixtures/UndefinedCharsetHeaderTest.php +++ b/tests/fixtures/UndefinedCharsetHeaderTest.php @@ -37,7 +37,6 @@ public function testFixture() : void { self::assertEquals("", $message->get("Return-Path")); self::assertEquals([ 'from by bla.bla (CommuniGate Pro RULE 6.1.13) with RULE id 14057804; Mon, 27 Feb 2017 13:21:44 +0930', - 'from by bla.bla (CommuniGate Pro RULE 6.1.13) with RULE id 14057804' ], $message->get("Received")->all()); self::assertEquals(")", $message->getHTMLBody()); self::assertFalse($message->hasTextBody()); diff --git a/tests/issues/Issue410Test.php b/tests/issues/Issue410Test.php index 458c8a13..4f8b831d 100644 --- a/tests/issues/Issue410Test.php +++ b/tests/issues/Issue410Test.php @@ -14,11 +14,33 @@ use PHPUnit\Framework\TestCase; use Tests\fixtures\FixtureTestCase; +use Webklex\PHPIMAP\Attachment; use Webklex\PHPIMAP\ClientManager; +use Webklex\PHPIMAP\Exceptions\AuthFailedException; +use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; +use Webklex\PHPIMAP\Exceptions\ImapServerErrorException; +use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; +use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; +use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; +use Webklex\PHPIMAP\Exceptions\ResponseException; +use Webklex\PHPIMAP\Exceptions\RuntimeException; use Webklex\PHPIMAP\Message; class Issue410Test extends FixtureTestCase { + /** + * @throws RuntimeException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws ImapBadRequestException + * @throws InvalidMessageDateException + * @throws ConnectionFailedException + * @throws \ReflectionException + * @throws ImapServerErrorException + * @throws AuthFailedException + * @throws MaskNotFoundException + */ public function testIssueEmail() { $message = $this->getFixture("issue-410.eml"); @@ -33,6 +55,18 @@ public function testIssueEmail() { self::assertSame("☆第132号 「ガーデン&エクステリア」専門店のためのQ&Aサロン 【月刊エクステリア・ワーク】", $attachment->name); } + /** + * @throws RuntimeException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws ImapBadRequestException + * @throws InvalidMessageDateException + * @throws ConnectionFailedException + * @throws \ReflectionException + * @throws ImapServerErrorException + * @throws AuthFailedException + * @throws MaskNotFoundException + */ public function testIssueEmailB() { $message = $this->getFixture("issue-410b.eml"); @@ -48,6 +82,18 @@ public function testIssueEmailB() { self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->name); } + /** + * @throws RuntimeException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws ImapBadRequestException + * @throws ConnectionFailedException + * @throws InvalidMessageDateException + * @throws ImapServerErrorException + * @throws AuthFailedException + * @throws \ReflectionException + * @throws MaskNotFoundException + */ public function testIssueEmailSymbols() { $message = $this->getFixture("issue-410symbols.eml"); @@ -55,10 +101,11 @@ public function testIssueEmailSymbols() { self::assertSame(1, $attachments->count()); + /** @var Attachment $attachment */ $attachment = $attachments->first(); self::assertSame("Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf", $attachment->description); - self::assertSame("Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf", $attachment->filename); self::assertSame("Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf", $attachment->name); + self::assertSame("Checkliste 10.,DAVIDGASSE 76-80;2;2.pdf", $attachment->filename); } } \ No newline at end of file From be974001c3d230df0c697e27f4133837b9488380 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 03:19:29 +0100 Subject: [PATCH 158/203] php8.3 and php8.4 checks added --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b441efd3..682cbd51 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: true matrix: - php: ['8.0', 8.1, 8.2] + php: ['8.0', 8.1, 8.2, 8.3, 8.4] name: PHP ${{ matrix.php }} From 279ad47be78cd07215ecf127c978405feb76d99a Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 03:40:38 +0100 Subject: [PATCH 159/203] Ignore possible untagged lines after IDLE and DONE commands #445 (thanks @gazben) --- CHANGELOG.md | 3 +- src/Connection/Protocols/ImapProtocol.php | 74 +++++++++++++++++- src/Folder.php | 92 +++++++---------------- src/Message.php | 4 +- 4 files changed, 103 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b006fb36..c85451bd 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Fix bug when multipart message getHTMLBody() method returns null #455 (thanks @michalkortas) - Fix: Improve return type hints and return docblocks for query classes #470 (thanks @olliescase) - Fix - Query - Chunked - Resolved infinite loop when start chunk > 1 #477 (thanks @NeekTheNook) -- Attachment with symbols in filename #436 +- Attachment with symbols in filename #436 (thanks @nuernbergerA) +- Ignore possible untagged lines after IDLE and DONE commands #445 (thanks @gazben) ### Added - IMAP STATUS command support added `Folder::status()` #424 (thanks @InterLinked1) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index cb7cf4b4..b054d462 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -160,6 +160,25 @@ protected function assumedNextLine(Response $response, string $start): bool { return str_starts_with($this->nextLine($response), $start); } + /** + * Get the next line and check if it starts with a given string + * The server can send untagged status updates starting with '*' if we are not looking for a status update, + * the untagged lines will be ignored. + * + * @param Response $response + * @param string $start + * + * @return bool + * @throws RuntimeException + */ + protected function assumedNextLineIgnoreUntagged(Response $response, string $start): bool { + do { + $line = $this->nextLine($response); + } while (!(str_starts_with($start, '*')) && $this->isUntaggedLine($line)); + + return str_starts_with($line, $start); + } + /** * Get the next line and split the tag * @param string|null $tag reference tag @@ -176,6 +195,25 @@ protected function nextTaggedLine(Response $response, ?string &$tag): string { return $line ?? ''; } + /** + * Get the next line and split the tag + * The server can send untagged status updates starting with '*', the untagged lines will be ignored. + * + * @param string|null $tag reference tag + * + * @return string next line + * @throws RuntimeException + */ + protected function nextTaggedLineIgnoreUntagged(Response $response, &$tag): string { + do { + $line = $this->nextLine($response); + } while ($this->isUntaggedLine($line)); + + list($tag, $line) = explode(' ', $line, 2); + + return $line; + } + /** * Get the next line and check if it contains a given string and split the tag * @param Response $response @@ -189,6 +227,32 @@ protected function assumedNextTaggedLine(Response $response, string $start, &$ta return str_contains($this->nextTaggedLine($response, $tag), $start); } + /** + * Get the next line and check if it contains a given string and split the tag + * @param string $start + * @param $tag + * + * @return bool + * @throws RuntimeException + */ + protected function assumedNextTaggedLineIgnoreUntagged(Response $response, string $start, &$tag): bool { + $line = $this->nextTaggedLineIgnoreUntagged($response, $tag); + return strpos($line, $start) !== false; + } + + /** + * RFC3501 - 2.2.2 + * Data transmitted by the server to the client and status responses + * that do not indicate command completion are prefixed with the token + * "*", and are called untagged responses. + * + * @param string $line + * @return bool + */ + protected function isUntaggedLine(string $line) : bool { + return str_starts_with($line, '* '); + } + /** * Split a given line in values. A value is literal of any form or a list * @param Response $response @@ -703,10 +767,12 @@ public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', * @throws RuntimeException */ public function fetch(array|string $items, array|int $from, mixed $to = null, int|string $uid = IMAP::ST_UID): Response { - if (is_array($from)) { + if (is_array($from) && count($from) > 1) { $set = implode(',', $from); + } elseif (is_array($from) && count($from) === 1) { + $set = $from[0] . ':' . $from[0]; } elseif ($to === null) { - $set = $from; + $set = $from . ':' . $from; } elseif ($to == INF) { $set = $from . ':*'; } else { @@ -1266,7 +1332,7 @@ public function getQuotaRoot(string $quota_root = 'INBOX'): Response { */ public function idle(): void { $response = $this->sendRequest("IDLE"); - if (!$this->assumedNextLine($response, '+ ')) { + if (!$this->assumedNextLineIgnoreUntagged($response, '+ ')) { throw new RuntimeException('idle failed'); } } @@ -1278,7 +1344,7 @@ public function idle(): void { public function done(): bool { $response = new Response($this->noun, $this->debug); $this->write($response, "DONE"); - if (!$this->assumedNextTaggedLine($response, 'OK', $tags)) { + if (!$this->assumedNextTaggedLineIgnoreUntagged($response, 'OK', $tags)) { throw new RuntimeException('done failed'); } return true; diff --git a/src/Folder.php b/src/Folder.php index 8c9abd61..11affde2 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -121,22 +121,6 @@ class Folder { /** @var array */ public array $status; - /** @var array */ - public array $attributes = []; - - - const SPECIAL_ATTRIBUTES = [ - 'haschildren' => ['\haschildren'], - 'hasnochildren' => ['\hasnochildren'], - 'template' => ['\template', '\templates'], - 'inbox' => ['\inbox'], - 'sent' => ['\sent'], - 'drafts' => ['\draft', '\drafts'], - 'archive' => ['\archive', '\archives'], - 'trash' => ['\trash'], - 'junk' => ['\junk', '\spam'], - ]; - /** * Folder constructor. * @param Client $client @@ -251,8 +235,8 @@ public function getChildren(): FolderCollection { */ protected function decodeName($name): string|array|bool|null { $parts = []; - foreach(explode($this->delimiter, $name) as $item) { - $parts[] = EncodingAliases::convert($item, "UTF7-IMAP"); + foreach (explode($this->delimiter, $name) as $item) { + $parts[] = EncodingAliases::convert($item, "UTF7-IMAP", "UTF-8"); } return implode($this->delimiter, $parts); @@ -280,14 +264,6 @@ protected function parseAttributes($attributes): void { $this->marked = in_array('\Marked', $attributes); $this->referral = in_array('\Referral', $attributes); $this->has_children = in_array('\HasChildren', $attributes); - - array_map(function($el) { - foreach(self::SPECIAL_ATTRIBUTES as $key => $attribute) { - if(in_array(strtolower($el), $attribute)){ - $this->attributes[] = $key; - } - } - }, $attributes); } /** @@ -308,7 +284,7 @@ protected function parseAttributes($attributes): void { public function move(string $new_name, bool $expunge = true): array { $this->client->checkConnection(); $status = $this->client->getConnection()->renameFolder($this->full_name, $new_name)->validatedData(); - if($expunge) $this->client->expunge(); + if ($expunge) $this->client->expunge(); $folder = $this->client->getFolder($new_name); $event = $this->getEvent("folder", "moved"); @@ -334,7 +310,7 @@ public function move(string $new_name, bool $expunge = true): array { public function overview(string $sequence = null): array { $this->client->openFolder($this->path); $sequence = $sequence === null ? "1:*" : $sequence; - $uid = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN); + $uid = ClientManager::get('options.sequence', IMAP::ST_MSGN); $response = $this->client->getConnection()->overview($sequence, $uid); return $response->validatedData(); } @@ -360,7 +336,7 @@ public function appendMessage(string $message, array $options = null, Carbon|str * date string that conforms to the rfc2060 specifications for a date_time value or be a Carbon object. */ - if($internal_date instanceof Carbon){ + if ($internal_date instanceof Carbon) { $internal_date = $internal_date->format('d-M-Y H:i:s O'); } @@ -401,11 +377,11 @@ public function rename(string $new_name, bool $expunge = true): array { */ public function delete(bool $expunge = true): array { $status = $this->client->getConnection()->deleteFolder($this->path)->validatedData(); - if($this->client->getActiveFolder() == $this->path){ - $this->client->setActiveFolder(); + if ($this->client->getActiveFolder() == $this->path){ + $this->client->setActiveFolder(null); } - if($expunge) $this->client->expunge(); + if ($expunge) $this->client->expunge(); $event = $this->getEvent("folder", "deleted"); $event::dispatch($this); @@ -461,7 +437,7 @@ public function unsubscribe(): array { public function idle(callable $callback, int $timeout = 300): void { $this->client->setTimeout($timeout); - if(!in_array("IDLE", $this->client->getConnection()->getCapabilities()->validatedData())){ + if (!in_array("IDLE", $this->client->getConnection()->getCapabilities()->validatedData())) { throw new Exceptions\NotSupportedCapabilityException("IMAP server does not support IDLE"); } @@ -474,15 +450,24 @@ public function idle(callable $callback, int $timeout = 300): void { $sequence = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN); - while(true) { - // This polymorphic call is fine - Protocol::idle() will throw an exception beforehand - $line = $idle_client->getConnection()->nextLine(Response::empty()); + while (true) { + try { + // This polymorphic call is fine - Protocol::idle() will throw an exception beforehand + $line = $idle_client->getConnection()->nextLine(Response::empty()); + } catch (Exceptions\RuntimeException $e) { + if(strpos($e->getMessage(), "empty response") >= 0 && $idle_client->getConnection()->connected()) { + continue; + } + if(!str_contains($e->getMessage(), "connection closed")) { + throw $e; + } + } - if(($pos = strpos($line, "EXISTS")) !== false){ + if (($pos = strpos($line, "EXISTS")) !== false) { $msgn = (int)substr($line, 2, $pos - 2); // Check if the stream is still alive or should be considered stale - if(!$this->client->isConnected() || $last_action->isBefore(Carbon::now())){ + if (!$this->client->isConnected() || $last_action->isBefore(Carbon::now())) { // Reset the connection before interacting with it. Otherwise, the resource might be stale which // would result in a stuck interaction. If you know of a way of detecting a stale resource, please // feel free to improve this logic. I tried a lot but nothing seem to work reliably... @@ -516,7 +501,7 @@ public function idle(callable $callback, int $timeout = 300): void { } /** - * Get folder status information from the EXAMINE command + * Get folder status information * * @return array * @throws ConnectionFailedException @@ -526,39 +511,20 @@ public function idle(callable $callback, int $timeout = 300): void { * @throws AuthFailedException * @throws ResponseException */ - public function status(): array { - return $this->client->getConnection()->folderStatus($this->path)->validatedData(); + public function getStatus(): array { + return $this->examine(); } /** - * Get folder status information from the EXAMINE command - * - * @return array - * @throws AuthFailedException * @throws ConnectionFailedException * @throws ImapBadRequestException * @throws ImapServerErrorException - * @throws ResponseException * @throws RuntimeException - * - * @deprecated Use Folder::status() instead - */ - public function getStatus(): array { - return $this->status(); - } - - /** - * Load folder status information from the EXAMINE command - * @return Folder * @throws AuthFailedException - * @throws ConnectionFailedException - * @throws ImapBadRequestException - * @throws ImapServerErrorException * @throws ResponseException - * @throws RuntimeException */ public function loadStatus(): Folder { - $this->status = $this->examine(); + $this->status = $this->getStatus(); return $this; } @@ -606,8 +572,8 @@ public function getClient(): Client { * @param $delimiter */ public function setDelimiter($delimiter): void { - if(in_array($delimiter, [null, '', ' ', false]) === true){ - $delimiter = $this->client->getConfig()->get('options.delimiter', '/'); + if (in_array($delimiter, [null, '', ' ', false]) === true) { + $delimiter = ClientManager::get('options.delimiter', '/'); } $this->delimiter = $delimiter; diff --git a/src/Message.php b/src/Message.php index bae2acfe..855373b8 100755 --- a/src/Message.php +++ b/src/Message.php @@ -556,7 +556,7 @@ public function getHTMLBody(): string { */ private function parseHeader(): void { $sequence_id = $this->getSequenceId(); - $headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence)->validatedData(); + $headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence)->setCanBeEmpty(true)->validatedData(); if (!isset($headers[$sequence_id])) { throw new MessageHeaderFetchingException("no headers found", 0); } @@ -609,7 +609,7 @@ private function parseFlags(): void { $sequence_id = $this->getSequenceId(); try { - $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence)->validatedData(); + $flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence)->setCanBeEmpty(true)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageFlagException("flag could not be fetched", 0, $e); } From bd1614dcf75d10e959aa9dcb610852b6f58c9996 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 04:05:17 +0100 Subject: [PATCH 160/203] Merge conflict resolved #445 --- src/Folder.php | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/Folder.php b/src/Folder.php index 11affde2..8eff19bc 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -236,7 +236,7 @@ public function getChildren(): FolderCollection { protected function decodeName($name): string|array|bool|null { $parts = []; foreach (explode($this->delimiter, $name) as $item) { - $parts[] = EncodingAliases::convert($item, "UTF7-IMAP", "UTF-8"); + $parts[] = EncodingAliases::convert($item, "UTF7-IMAP"); } return implode($this->delimiter, $parts); @@ -310,7 +310,7 @@ public function move(string $new_name, bool $expunge = true): array { public function overview(string $sequence = null): array { $this->client->openFolder($this->path); $sequence = $sequence === null ? "1:*" : $sequence; - $uid = ClientManager::get('options.sequence', IMAP::ST_MSGN); + $uid = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN); $response = $this->client->getConnection()->overview($sequence, $uid); return $response->validatedData(); } @@ -378,7 +378,7 @@ public function rename(string $new_name, bool $expunge = true): array { public function delete(bool $expunge = true): array { $status = $this->client->getConnection()->deleteFolder($this->path)->validatedData(); if ($this->client->getActiveFolder() == $this->path){ - $this->client->setActiveFolder(null); + $this->client->setActiveFolder(); } if ($expunge) $this->client->expunge(); @@ -501,7 +501,7 @@ public function idle(callable $callback, int $timeout = 300): void { } /** - * Get folder status information + * Get folder status information from the EXAMINE command * * @return array * @throws ConnectionFailedException @@ -511,20 +511,39 @@ public function idle(callable $callback, int $timeout = 300): void { * @throws AuthFailedException * @throws ResponseException */ - public function getStatus(): array { - return $this->examine(); + public function status(): array { + return $this->client->getConnection()->folderStatus($this->path)->validatedData(); } /** + * Get folder status information from the EXAMINE command + * + * @return array + * @throws AuthFailedException * @throws ConnectionFailedException * @throws ImapBadRequestException * @throws ImapServerErrorException + * @throws ResponseException * @throws RuntimeException + * + * @deprecated Use Folder::status() instead + */ + public function getStatus(): array { + return $this->status(); + } + + /** + * Load folder status information from the EXAMINE command + * @return Folder * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException * @throws ResponseException + * @throws RuntimeException */ public function loadStatus(): Folder { - $this->status = $this->getStatus(); + $this->status = $this->examine(); return $this; } @@ -573,7 +592,7 @@ public function getClient(): Client { */ public function setDelimiter($delimiter): void { if (in_array($delimiter, [null, '', ' ', false]) === true) { - $delimiter = ClientManager::get('options.delimiter', '/'); + $delimiter = $this->client->getConfig()->get('options.delimiter', '/'); } $this->delimiter = $delimiter; From 7f0e219a2a496ef6ca3bfee4ad98f5255106d8b1 Mon Sep 17 00:00:00 2001 From: Jeff Bierschbach Date: Thu, 16 Jan 2025 21:11:28 -0600 Subject: [PATCH 161/203] Fix Empty Child Folder Error (#474) --- src/Client.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index 2ec9ebd9..90016a10 100755 --- a/src/Client.php +++ b/src/Client.php @@ -586,7 +586,7 @@ public function getFolders(bool $hierarchical = true, string $parent_folder = nu if ($hierarchical && $folder->hasChildren()) { $pattern = $folder->full_name.$folder->delimiter.'%'; - $children = $this->getFolders(true, $pattern, $soft_fail); + $children = $this->getFolders(true, $pattern, true); $folder->setChildren($children); } @@ -632,7 +632,7 @@ public function getFoldersWithStatus(bool $hierarchical = true, string $parent_f if ($hierarchical && $folder->hasChildren()) { $pattern = $folder->full_name.$folder->delimiter.'%'; - $children = $this->getFoldersWithStatus(true, $pattern, $soft_fail); + $children = $this->getFoldersWithStatus(true, $pattern, true); $folder->setChildren($children); } From 6bd8ba43cf25282ab9a99ec31c4e2b16c7374022 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 04:14:50 +0100 Subject: [PATCH 162/203] #487 TestCase added --- tests/HeaderTest.php | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index 9de25004..767f4388 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -159,4 +159,46 @@ public function testExtractHeaderExtensions() { $this->assertArrayHasKey('attribute_test', $mock->getAttributes()); $this->assertEquals('attribute_test_value', $mock->get('attribute_test')); } + + public function testExtractHeaderExtensions2() { + $mock = $this->getMockBuilder(Header::class) + ->disableOriginalConstructor() + ->onlyMethods([]) + ->getMock(); + + $method = new \ReflectionMethod($mock, 'extractHeaderExtensions'); + $method->setAccessible(true); + + $mockAttributes = [ + 'content_type' => new Attribute('content_type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; name="=?utf-8?Q?=D0=A2=D0=B8=D0=BF=D0=BE=D0=B2=D0=BE=D0=B9_?= =?utf-8?Q?=D1=80=D0=B0=D1=81=D1=87=D0=B5=D1=82_=D0=BF?= =?utf-8?Q?=D0=BE=D1=82=D1=80=D0=B5=D0=B1=D0=BB=D0=B5=D0=BD?= =?utf-8?Q?=D0=B8=D1=8F_=D1=8D=D0=BB=D0=B5=D0=BA=D1=82?= =?utf-8?Q?=D1=80=D0=BE=D1=8D=D0=BD=D0=B5=D1=80=D0=B3=D0=B8=D0=B8_=D0=B2_?= =?utf-8?Q?=D0=9A=D0=9F_=D0=97=D0=B2=D0=B5=D0=B7=D0=B4?= =?utf-8?Q?=D0=BD=D1=8B=D0=B9=2Exlsx?="'), + 'content_transfer_encoding' => new Attribute('content_transfer_encoding', 'base64'), + 'content_disposition' => new Attribute('content_disposition', 'attachment; name*0*=utf-8\'\'%D0%A2%D0%B8%D0%BF%D0%BE%D0%B2%D0%BE%D0%B9%20; name*1*=%D1%80%D0%B0%D1%81%D1%87%D0%B5%D1%82%20%D0%BF; name*2*=%D0%BE%D1%82%D1%80%D0%B5%D0%B1%D0%BB%D0%B5%D0%BD; name*3*=%D0%B8%D1%8F%20%D1%8D%D0%BB%D0%B5%D0%BA%D1%82; name*4*=%D1%80%D0%BE%D1%8D%D0%BD%D0%B5%D1%80%D0%B3%D0%B8; name*5*=%D0%B8%20%D0%B2%20%D0%9A%D0%9F%20%D0%97%D0%B2%D0%B5%D0%B7%D0%B4; name*6*=%D0%BD%D1%8B%D0%B9.xlsx; filename*0*=utf-8\'\'%D0%A2%D0%B8%D0%BF%D0%BE%D0%B2%D0%BE%D0%B9%20; filename*1*=%D1%80%D0%B0%D1%81%D1%87%D0%B5%D1%82%20%D0%BF; filename*2*=%D0%BE%D1%82%D1%80%D0%B5%D0%B1%D0%BB%D0%B5%D0%BD; filename*3*=%D0%B8%D1%8F%20%D1%8D%D0%BB%D0%B5%D0%BA%D1%82; filename*4*=%D1%80%D0%BE%D1%8D%D0%BD%D0%B5%D1%80%D0%B3%D0%B8; filename*5*=%D0%B8%20%D0%B2%20%D0%9A%D0%9F%20%D0%97; filename*6*=%D0%B2%D0%B5%D0%B7%D0%B4%D0%BD%D1%8B%D0%B9.xlsx; attribute_test=attribute_test_value'), + ]; + + $attributes = new \ReflectionProperty($mock, 'attributes'); + $attributes->setAccessible(true); + $attributes->setValue($mock, $mockAttributes); + + $method->invoke($mock); + + $this->assertArrayHasKey('filename', $mock->getAttributes()); + $this->assertArrayNotHasKey('filename*0', $mock->getAttributes()); + $this->assertEquals('utf-8\'\'%D0%A2%D0%B8%D0%BF%D0%BE%D0%B2%D0%BE%D0%B9%20%D1%80%D0%B0%D1%81%D1%87%D0%B5%D1%82%20%D0%BF%D0%BE%D1%82%D1%80%D0%B5%D0%B1%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F%20%D1%8D%D0%BB%D0%B5%D0%BA%D1%82%D1%80%D0%BE%D1%8D%D0%BD%D0%B5%D1%80%D0%B3%D0%B8%D0%B8%20%D0%B2%20%D0%9A%D0%9F%20%D0%97%D0%B2%D0%B5%D0%B7%D0%B4%D0%BD%D1%8B%D0%B9.xlsx', $mock->get('filename')); + + $this->assertArrayHasKey('name', $mock->getAttributes()); + $this->assertArrayNotHasKey('name*0', $mock->getAttributes()); + $this->assertEquals('=?utf-8?Q?=D0=A2=D0=B8=D0=BF=D0=BE=D0=B2=D0=BE=D0=B9_?= =?utf-8?Q?=D1=80=D0=B0=D1=81=D1=87=D0=B5=D1=82_=D0=BF?= =?utf-8?Q?=D0=BE=D1=82=D1=80=D0=B5=D0=B1=D0=BB=D0=B5=D0=BD?= =?utf-8?Q?=D0=B8=D1=8F_=D1=8D=D0=BB=D0=B5=D0=BA=D1=82?= =?utf-8?Q?=D1=80=D0=BE=D1=8D=D0=BD=D0=B5=D1=80=D0=B3=D0=B8=D0=B8_=D0=B2_?= =?utf-8?Q?=D0=9A=D0=9F_=D0=97=D0=B2=D0=B5=D0=B7=D0=B4?= =?utf-8?Q?=D0=BD=D1=8B=D0=B9=2Exlsx?=', $mock->get('name')); + + $this->assertArrayHasKey('content_type', $mock->getAttributes()); + $this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $mock->get('content_type')->last()); + + $this->assertArrayHasKey('content_transfer_encoding', $mock->getAttributes()); + $this->assertEquals('base64', $mock->get('content_transfer_encoding')); + + $this->assertArrayHasKey('content_disposition', $mock->getAttributes()); + $this->assertEquals('attachment', $mock->get('content_disposition')->last()); + + $this->assertArrayHasKey('attribute_test', $mock->getAttributes()); + $this->assertEquals('attribute_test_value', $mock->get('attribute_test')); + } } \ No newline at end of file From 1cabca657b8139fb7ecbac16c59f2e94b47ffd65 Mon Sep 17 00:00:00 2001 From: neolip Date: Fri, 17 Jan 2025 06:20:46 +0300 Subject: [PATCH 163/203] Fix: Attachment::decodeName remove .. from file name (#501) If attached file has name like test..xml, then dots remove and broke file extension. --- src/Attachment.php | 7 ++++++- tests/AttachmentTest.php | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/AttachmentTest.php diff --git a/src/Attachment.php b/src/Attachment.php index bcbe91c5..19b639f6 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -325,7 +325,12 @@ public function decodeName(?string $name): string { // sanitize $name // order of '..' is important - return str_replace(['\\', '/', chr(0), ':', '..'], '', $name); + $replaces = [ + '/\\\\/' => '', + '/[\/\0:]+/' => '', + '/\.+/' => '.', + ]; + return preg_replace(array_keys($replaces), array_values($replaces), $name); } return ""; } diff --git a/tests/AttachmentTest.php b/tests/AttachmentTest.php new file mode 100644 index 00000000..566ea658 --- /dev/null +++ b/tests/AttachmentTest.php @@ -0,0 +1,37 @@ +getFixture("attachment_encoded_filename.eml"); + $this->attachment = $message->getAttachments()->first(); + } + /** + * @dataProvider decodeNameDataProvider + */ + public function testDecodeName(string $input, string $output): void + { + $name = $this->attachment->decodeName($input); + $this->assertEquals($output, $name); + } + + public function decodeNameDataProvider(): array + { + return [ + ['../../../../../../../../../../../var/www/shell.php', '.varwwwshell.php'], + ['test..xml', 'test.xml'], + [chr(0), ''], + ['C:\\file.txt', 'Cfile.txt'], + ]; + } +} From af752f3eb4fdb9f6e1d0e90e250203dcfaccced7 Mon Sep 17 00:00:00 2001 From: Arnaud Lemercier Date: Fri, 17 Jan 2025 04:24:44 +0100 Subject: [PATCH 164/203] docs: fix a comment (#504) --- src/Connection/Protocols/ProtocolInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index 0a0dbfad..eb6d7c9d 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -129,7 +129,7 @@ public function examineFolder(string $folder = 'INBOX'): Response; public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response; /** - * Fetch message headers + * Fetch message contents * @param int|array $uids * @param string $rfc * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use From 4601d2f9b3389171d57d536f8dabe386bc5082ba Mon Sep 17 00:00:00 2001 From: Arnaud Lemercier Date: Fri, 17 Jan 2025 04:26:21 +0100 Subject: [PATCH 165/203] feat: client->getFolderPath return null if folder is not set. (#506) * feat: client->getFolderPath return null if folder is not set. * docs: update comment --- src/Client.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index 90016a10..ceaae8f6 100755 --- a/src/Client.php +++ b/src/Client.php @@ -773,9 +773,9 @@ public function checkFolder(string $folder_path): array { /** * Get the current active folder * - * @return string + * @return null|string */ - public function getFolderPath(): string { + public function getFolderPath(): ?string { return $this->active_folder; } From e88fd4c62767d2323bc9ea496880ed5c162cccac Mon Sep 17 00:00:00 2001 From: campbell-m <87438215+campbell-m@users.noreply.github.com> Date: Fri, 17 Jan 2025 03:27:56 +0000 Subject: [PATCH 166/203] Fix implicit marking of parameters as nullable, deprecated in PHP 8.4 (#518) * Fix implicit marking of parameters as nullable, which is deprecated in PHP 8.4. * Change line separators to LF --- src/Client.php | 6 +- src/ClientManager.php | 2 +- src/Connection/Protocols/ImapProtocol.php | 12 +- src/Connection/Protocols/LegacyProtocol.php | 12 +- src/EncodingAliases.php | 2 +- src/Folder.php | 4 +- src/Message.php | 18 +- src/Part.php | 2 +- src/Query/WhereQuery.php | 4 +- tests/messages/1366671050@github.com.eml | 216 ++++++++++---------- tests/messages/example_attachment.eml | 112 +++++----- 11 files changed, 195 insertions(+), 195 deletions(-) diff --git a/src/Client.php b/src/Client.php index ceaae8f6..a79511c0 100755 --- a/src/Client.php +++ b/src/Client.php @@ -572,7 +572,7 @@ public function getFolderByPath($folder_path, bool $utf7 = false, bool $soft_fai * @throws ResponseException * @throws RuntimeException */ - public function getFolders(bool $hierarchical = true, string $parent_folder = null, bool $soft_fail = false): FolderCollection { + public function getFolders(bool $hierarchical = true, ?string $parent_folder = null, bool $soft_fail = false): FolderCollection { $this->checkConnection(); $folders = FolderCollection::make([]); @@ -618,7 +618,7 @@ public function getFolders(bool $hierarchical = true, string $parent_folder = nu * @throws RuntimeException * @throws ResponseException */ - public function getFoldersWithStatus(bool $hierarchical = true, string $parent_folder = null, bool $soft_fail = false): FolderCollection { + public function getFoldersWithStatus(bool $hierarchical = true, ?string $parent_folder = null, bool $soft_fail = false): FolderCollection { $this->checkConnection(); $folders = FolderCollection::make([]); @@ -793,7 +793,7 @@ public function getFolderPath(): ?string { * @throws RuntimeException * @throws ResponseException */ - public function Id(array $ids = null): array { + public function Id(?array $ids = null): array { $this->checkConnection(); return $this->connection->ID($ids)->validatedData(); } diff --git a/src/ClientManager.php b/src/ClientManager.php index a63f3a4e..6f66886b 100644 --- a/src/ClientManager.php +++ b/src/ClientManager.php @@ -74,7 +74,7 @@ public function make(array $config): Client { * @return Client * @throws Exceptions\MaskNotFoundException */ - public function account(string $name = null): Client { + public function account(?string $name = null): Client { $name = $name ?: $this->config->getDefaultAccount(); // If the connection has not been resolved we will resolve it now as all diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index b054d462..9272be91 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -67,7 +67,7 @@ public function __destruct() { * * @throws ConnectionFailedException */ - public function connect(string $host, int $port = null): bool { + public function connect(string $host, ?int $port = null): bool { $transport = 'tcp'; $encryption = ''; @@ -426,7 +426,7 @@ private function stringifyArray(array $arr): string { * @return Response * @throws RuntimeException */ - public function sendRequest(string $command, array $tokens = [], string &$tag = null): Response { + public function sendRequest(string $command, array $tokens = [], ?string &$tag = null): Response { if (!$tag) { $this->noun++; $tag = 'TAG' . $this->noun; @@ -1021,7 +1021,7 @@ public function folders(string $reference = '', string $folder = '*'): Response * @throws RuntimeException */ public function store( - array|string $flags, int $from, int $to = null, string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, string $item = null + array|string $flags, int $from, ?int $to = null, ?string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, ?string $item = null ): Response { $flags = $this->escapeList(is_array($flags) ? $flags : [$flags]); $set = $this->buildSet($from, $to); @@ -1061,7 +1061,7 @@ public function store( * @throws ImapServerErrorException * @throws RuntimeException */ - public function appendMessage(string $folder, string $message, array $flags = null, string $date = null): Response { + public function appendMessage(string $folder, string $message, ?array $flags = null, ?string $date = null): Response { $tokens = []; $tokens[] = $this->escapeString($folder); if ($flags !== null) { @@ -1091,7 +1091,7 @@ public function appendMessage(string $folder, string $message, array $flags = nu * @throws ImapServerErrorException * @throws RuntimeException */ - public function copyMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { + public function copyMessage(string $folder, $from, ?int $to = null, int|string $uid = IMAP::ST_UID): Response { $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("COPY", $uid); @@ -1137,7 +1137,7 @@ public function copyManyMessages(array $messages, string $folder, int|string $ui * @throws ImapServerErrorException * @throws RuntimeException */ - public function moveMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { + public function moveMessage(string $folder, $from, ?int $to = null, int|string $uid = IMAP::ST_UID): Response { $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("MOVE", $uid); diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index c6471a02..81b52f77 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -55,7 +55,7 @@ public function __destruct() { * @param string $host * @param int|null $port */ - public function connect(string $host, int $port = null): void { + public function connect(string $host, ?int $port = null): void { if ($this->encryption) { $encryption = strtolower($this->encryption); if ($encryption == "ssl") { @@ -360,7 +360,7 @@ public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response * * @return Response message number for given message or all messages as array */ - public function getUid(int $id = null): Response { + public function getUid(?int $id = null): Response { return $this->response()->wrap(function($response) use ($id) { /** @var Response $response */ if ($id === null) { @@ -455,7 +455,7 @@ public function folders(string $reference = '', string $folder = '*'): Response * * @return Response new flags if $silent is false, else true or false depending on success */ - public function store(array|string $flags, int $from, int $to = null, string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, string $item = null): Response { + public function store(array|string $flags, int $from, ?int $to = null, ?string $mode = null, bool $silent = true, int|string $uid = IMAP::ST_UID, ?string $item = null): Response { $flag = trim(is_array($flags) ? implode(" ", $flags) : $flags); return $this->response()->wrap(function($response) use ($mode, $from, $flag, $uid, $silent) { @@ -491,7 +491,7 @@ public function store(array|string $flags, int $from, int $to = null, string $mo * * @return Response */ - public function appendMessage(string $folder, string $message, array $flags = null, mixed $date = null): Response { + public function appendMessage(string $folder, string $message, ?array $flags = null, mixed $date = null): Response { return $this->response("imap_append")->wrap(function($response) use ($folder, $message, $flags, $date) { /** @var Response $response */ if ($date != null) { @@ -522,7 +522,7 @@ public function appendMessage(string $folder, string $message, array $flags = nu * * @return Response */ - public function copyMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { + public function copyMessage(string $folder, $from, ?int $to = null, int|string $uid = IMAP::ST_UID): Response { return $this->response("imap_mail_copy")->wrap(function($response) use ($from, $folder, $uid) { /** @var Response $response */ @@ -572,7 +572,7 @@ public function copyManyMessages(array $messages, string $folder, int|string $ui * * @return Response success */ - public function moveMessage(string $folder, $from, int $to = null, int|string $uid = IMAP::ST_UID): Response { + public function moveMessage(string $folder, $from, ?int $to = null, int|string $uid = IMAP::ST_UID): Response { return $this->response("imap_mail_move")->wrap(function($response) use ($from, $folder, $uid) { if (\imap_mail_move($this->stream, $from, $this->getAddress() . $folder, $uid ? IMAP::ST_UID : IMAP::NIL)) { return [ diff --git a/src/EncodingAliases.php b/src/EncodingAliases.php index 888fc7fa..8a3dc0fb 100644 --- a/src/EncodingAliases.php +++ b/src/EncodingAliases.php @@ -474,7 +474,7 @@ class EncodingAliases { * * @return string */ - public static function get(?string $encoding, string $fallback = null): string { + public static function get(?string $encoding, ?string $fallback = null): string { if (isset(self::$aliases[strtolower($encoding ?? '')])) { return self::$aliases[strtolower($encoding ?? '')]; } diff --git a/src/Folder.php b/src/Folder.php index 8eff19bc..dced7d6b 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -307,7 +307,7 @@ public function move(string $new_name, bool $expunge = true): array { * @throws MessageNotFoundException * @throws ResponseException */ - public function overview(string $sequence = null): array { + public function overview(?string $sequence = null): array { $this->client->openFolder($this->path); $sequence = $sequence === null ? "1:*" : $sequence; $uid = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN); @@ -329,7 +329,7 @@ public function overview(string $sequence = null): array { * @throws AuthFailedException * @throws ResponseException */ - public function appendMessage(string $message, array $options = null, Carbon|string $internal_date = null): array { + public function appendMessage(string $message, ?array $options = null, Carbon|string|null $internal_date = null): array { /** * Check if $internal_date is parsed. If it is null it should not be set. Otherwise, the message can't be stored. * If this parameter is set, it will set the INTERNALDATE on the appended message. The parameter should be a diff --git a/src/Message.php b/src/Message.php index 855373b8..b0dabb2d 100755 --- a/src/Message.php +++ b/src/Message.php @@ -222,7 +222,7 @@ class Message { * @throws RuntimeException * @throws ResponseException */ - public function __construct(int $uid, ?int $msglist, Client $client, int $fetch_options = null, bool $fetch_body = false, bool $fetch_flags = false, int $sequence = null) { + public function __construct(int $uid, ?int $msglist, Client $client, ?int $fetch_options = null, bool $fetch_body = false, bool $fetch_flags = false, ?int $sequence = null) { $this->boot($client->getConfig()); $default_mask = $client->getDefaultMessageMask(); @@ -329,7 +329,7 @@ public static function make(int $uid, ?int $msglist, Client $client, string $raw * @throws ResponseException * @throws RuntimeException */ - public static function fromFile(string $filename, Config $config = null): Message { + public static function fromFile(string $filename, ?Config $config = null): Message { $blob = file_get_contents($filename); if ($blob === false) { throw new RuntimeException("Unable to read file"); @@ -354,7 +354,7 @@ public static function fromFile(string $filename, Config $config = null): Messag * @throws ResponseException * @throws RuntimeException */ - public static function fromString(string $blob, Config $config = null): Message { + public static function fromString(string $blob, ?Config $config = null): Message { $reflection = new ReflectionClass(self::class); /** @var Message $instance */ $instance = $reflection->newInstanceWithoutConstructor(); @@ -386,7 +386,7 @@ public static function fromString(string $blob, Config $config = null): Message * @param ?Config $config * @throws DecoderNotFoundException */ - public function boot(Config $config = null): void { + public function boot(?Config $config = null): void { $this->attributes = []; $this->client = null; $this->config = $config ?? Config::make(); @@ -903,7 +903,7 @@ public function getFolder(): ?Folder { * @throws RuntimeException * @throws ResponseException */ - public function thread(Folder $sent_folder = null, MessageCollection &$thread = null, Folder $folder = null): MessageCollection { + public function thread(?Folder $sent_folder = null, ?MessageCollection &$thread = null, ?Folder $folder = null): MessageCollection { $thread = $thread ?: MessageCollection::make(); $folder = $folder ?: $this->getFolder(); $sent_folder = $sent_folder ?: $this->client->getFolderByPath($this->config->get("options.common_folders.sent", "INBOX/Sent")); @@ -1123,7 +1123,7 @@ protected function fetchNewMail(Folder $folder, int $next_uid, string $event, bo * @throws RuntimeException * @throws ResponseException */ - public function delete(bool $expunge = true, string $trash_path = null, bool $force_move = false): bool { + public function delete(bool $expunge = true, ?string $trash_path = null, bool $force_move = false): bool { $status = $this->setFlag("Deleted"); if ($force_move) { $trash_path = $trash_path === null ? $this->config["common_folders"]["trash"] : $trash_path; @@ -1398,7 +1398,7 @@ public function getStructure(): ?Structure { * @param null|Message $message * @return boolean */ - public function is(Message $message = null): bool { + public function is(?Message $message = null): bool { if (is_null($message)) { return false; } @@ -1605,7 +1605,7 @@ public function setUid(int $uid): Message { * * @return Message */ - public function setMsgn(int $msgn, int $msglist = null): Message { + public function setMsgn(int $msgn, ?int $msglist = null): Message { $this->msgn = $msgn; $this->msglist = $msglist; $this->uid = null; @@ -1636,7 +1636,7 @@ public function getSequenceId(): int { * @param $uid * @param int|null $msglist */ - public function setSequenceId($uid, int $msglist = null): void { + public function setSequenceId($uid, ?int $msglist = null): void { if ($this->getSequence() === IMAP::ST_UID) { $this->setUid($uid); $this->setMsglist($msglist); diff --git a/src/Part.php b/src/Part.php index 1dbf9439..3c55bdfb 100644 --- a/src/Part.php +++ b/src/Part.php @@ -153,7 +153,7 @@ class Part { * * @throws InvalidMessageDateException */ - public function __construct(string $raw_part, Config $config, Header $header = null, int $part_number = 0) { + public function __construct(string $raw_part, Config $config, ?Header $header = null, int $part_number = 0) { $this->raw = $raw_part; $this->config = $config; $this->header = $header; diff --git a/src/Query/WhereQuery.php b/src/Query/WhereQuery.php index e76cbe85..5636cf35 100755 --- a/src/Query/WhereQuery.php +++ b/src/Query/WhereQuery.php @@ -171,7 +171,7 @@ protected function push_search_criteria(string $criteria, mixed $value): void { * * @return $this */ - public function orWhere(Closure $closure = null): static { + public function orWhere(?Closure $closure = null): static { $this->query->push(['OR']); if ($closure !== null) $closure($this); @@ -183,7 +183,7 @@ public function orWhere(Closure $closure = null): static { * * @return $this */ - public function andWhere(Closure $closure = null): static { + public function andWhere(?Closure $closure = null): static { $this->query->push(['AND']); if ($closure !== null) $closure($this); diff --git a/tests/messages/1366671050@github.com.eml b/tests/messages/1366671050@github.com.eml index 91f51cf8..97cb7ad1 100644 --- a/tests/messages/1366671050@github.com.eml +++ b/tests/messages/1366671050@github.com.eml @@ -1,108 +1,108 @@ -Return-Path: -Delivered-To: someone@domain.tld -Received: from mx.domain.tld - by localhost with LMTP - id SABVMNfGqWP+PAAA0J78UA - (envelope-from ) - for ; Mon, 26 Dec 2022 17:07:51 +0100 -Received: from localhost (localhost [127.0.0.1]) - by mx.domain.tld (Postfix) with ESMTP id C3828140227 - for ; Mon, 26 Dec 2022 17:07:51 +0100 (CET) -X-Virus-Scanned: Debian amavisd-new at mx.domain.tld -X-Spam-Flag: NO -X-Spam-Score: -4.299 -X-Spam-Level: -X-Spam-Status: No, score=-4.299 required=6.31 tests=[BAYES_00=-1.9, - DKIMWL_WL_HIGH=-0.001, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, - DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, HTML_IMAGE_ONLY_16=1.092, - HTML_MESSAGE=0.001, MAILING_LIST_MULTI=-1, RCVD_IN_DNSWL_MED=-2.3, - RCVD_IN_MSPIKE_H2=-0.001, T_KAM_HTML_FONT_INVALID=0.01] - autolearn=ham autolearn_force=no -Received: from mx.domain.tld ([127.0.0.1]) - by localhost (mx.domain.tld [127.0.0.1]) (amavisd-new, port 10024) - with ESMTP id JcIS9RuNBTNx for ; - Mon, 26 Dec 2022 17:07:21 +0100 (CET) -Received: from smtp.github.com (out-26.smtp.github.com [192.30.252.209]) - (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) - (No client certificate requested) - by mx.domain.tld (Postfix) with ESMTPS id 6410B13FEB2 - for ; Mon, 26 Dec 2022 17:07:21 +0100 (CET) -Received: from github-lowworker-891b8d2.va3-iad.github.net (github-lowworker-891b8d2.va3-iad.github.net [10.48.109.104]) - by smtp.github.com (Postfix) with ESMTP id 176985E0200 - for ; Mon, 26 Dec 2022 08:07:14 -0800 (PST) -DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=github.com; - s=pf2014; t=1672070834; - bh=v91TPiLpM/cpUb4lgt2NMIUfM4HOIxCEWMR1+JTco+Q=; - h=Date:From:Reply-To:To:Cc:In-Reply-To:References:Subject:List-ID: - List-Archive:List-Post:List-Unsubscribe:From; - b=jW4Tac9IjWAPbEImyiYf1bzGLzY3ceohVbBg1V8BlpMTQ+o+yY3YB0eOe20hAsqZR - jrDjArx7rKQcslqBFL/b2B1C51rHuCBrz2cccLEERu9l/u0mTGCxTNtCRXHbCKbnR1 - VLWBeFLjATHth83kK6Kt7lkVuty+G3V1B6ZKPhCI= -Date: Mon, 26 Dec 2022 08:07:14 -0800 -From: Username -Reply-To: Webklex/php-imap -To: Webklex/php-imap -Cc: Subscribed -Message-ID: -In-Reply-To: -References: -Subject: Re: [Webklex/php-imap] Read all folders? (Issue #349) -Mime-Version: 1.0 -Content-Type: multipart/alternative; - boundary="--==_mimepart_63a9c6b293fe_65b5c71014155a"; - charset=UTF-8 -Content-Transfer-Encoding: 7bit -Precedence: list -X-GitHub-Sender: consigliere23 -X-GitHub-Recipient: Webklex -X-GitHub-Reason: subscribed -List-ID: Webklex/php-imap -List-Archive: https://github.com/Webklex/php-imap -List-Post: -List-Unsubscribe: , - -X-Auto-Response-Suppress: All -X-GitHub-Recipient-Address: someone@domain.tld - - -----==_mimepart_63a9c6b293fe_65b5c71014155a -Content-Type: text/plain; - charset=UTF-8 -Content-Transfer-Encoding: 7bit - -Any updates? - --- -Reply to this email directly or view it on GitHub: -https://github.com/Webklex/php-imap/issues/349#issuecomment-1365266070 -You are receiving this because you are subscribed to this thread. - -Message ID: -----==_mimepart_63a9c6b293fe_65b5c71014155a -Content-Type: text/html; - charset=UTF-8 -Content-Transfer-Encoding: 7bit - -

-

Any updates?

- -


Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <Webklex/php-imap/issues/349/1365266070@github.com>

- -----==_mimepart_63a9c6b293fe_65b5c71014155a-- +Return-Path: +Delivered-To: someone@domain.tld +Received: from mx.domain.tld + by localhost with LMTP + id SABVMNfGqWP+PAAA0J78UA + (envelope-from ) + for ; Mon, 26 Dec 2022 17:07:51 +0100 +Received: from localhost (localhost [127.0.0.1]) + by mx.domain.tld (Postfix) with ESMTP id C3828140227 + for ; Mon, 26 Dec 2022 17:07:51 +0100 (CET) +X-Virus-Scanned: Debian amavisd-new at mx.domain.tld +X-Spam-Flag: NO +X-Spam-Score: -4.299 +X-Spam-Level: +X-Spam-Status: No, score=-4.299 required=6.31 tests=[BAYES_00=-1.9, + DKIMWL_WL_HIGH=-0.001, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, + DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, HTML_IMAGE_ONLY_16=1.092, + HTML_MESSAGE=0.001, MAILING_LIST_MULTI=-1, RCVD_IN_DNSWL_MED=-2.3, + RCVD_IN_MSPIKE_H2=-0.001, T_KAM_HTML_FONT_INVALID=0.01] + autolearn=ham autolearn_force=no +Received: from mx.domain.tld ([127.0.0.1]) + by localhost (mx.domain.tld [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id JcIS9RuNBTNx for ; + Mon, 26 Dec 2022 17:07:21 +0100 (CET) +Received: from smtp.github.com (out-26.smtp.github.com [192.30.252.209]) + (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) + (No client certificate requested) + by mx.domain.tld (Postfix) with ESMTPS id 6410B13FEB2 + for ; Mon, 26 Dec 2022 17:07:21 +0100 (CET) +Received: from github-lowworker-891b8d2.va3-iad.github.net (github-lowworker-891b8d2.va3-iad.github.net [10.48.109.104]) + by smtp.github.com (Postfix) with ESMTP id 176985E0200 + for ; Mon, 26 Dec 2022 08:07:14 -0800 (PST) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=github.com; + s=pf2014; t=1672070834; + bh=v91TPiLpM/cpUb4lgt2NMIUfM4HOIxCEWMR1+JTco+Q=; + h=Date:From:Reply-To:To:Cc:In-Reply-To:References:Subject:List-ID: + List-Archive:List-Post:List-Unsubscribe:From; + b=jW4Tac9IjWAPbEImyiYf1bzGLzY3ceohVbBg1V8BlpMTQ+o+yY3YB0eOe20hAsqZR + jrDjArx7rKQcslqBFL/b2B1C51rHuCBrz2cccLEERu9l/u0mTGCxTNtCRXHbCKbnR1 + VLWBeFLjATHth83kK6Kt7lkVuty+G3V1B6ZKPhCI= +Date: Mon, 26 Dec 2022 08:07:14 -0800 +From: Username +Reply-To: Webklex/php-imap +To: Webklex/php-imap +Cc: Subscribed +Message-ID: +In-Reply-To: +References: +Subject: Re: [Webklex/php-imap] Read all folders? (Issue #349) +Mime-Version: 1.0 +Content-Type: multipart/alternative; + boundary="--==_mimepart_63a9c6b293fe_65b5c71014155a"; + charset=UTF-8 +Content-Transfer-Encoding: 7bit +Precedence: list +X-GitHub-Sender: consigliere23 +X-GitHub-Recipient: Webklex +X-GitHub-Reason: subscribed +List-ID: Webklex/php-imap +List-Archive: https://github.com/Webklex/php-imap +List-Post: +List-Unsubscribe: , + +X-Auto-Response-Suppress: All +X-GitHub-Recipient-Address: someone@domain.tld + + +----==_mimepart_63a9c6b293fe_65b5c71014155a +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: 7bit + +Any updates? + +-- +Reply to this email directly or view it on GitHub: +https://github.com/Webklex/php-imap/issues/349#issuecomment-1365266070 +You are receiving this because you are subscribed to this thread. + +Message ID: +----==_mimepart_63a9c6b293fe_65b5c71014155a +Content-Type: text/html; + charset=UTF-8 +Content-Transfer-Encoding: 7bit + +

+

Any updates?

+ +


Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <Webklex/php-imap/issues/349/1365266070@github.com>

+ +----==_mimepart_63a9c6b293fe_65b5c71014155a-- diff --git a/tests/messages/example_attachment.eml b/tests/messages/example_attachment.eml index a452ae79..3e929a34 100644 --- a/tests/messages/example_attachment.eml +++ b/tests/messages/example_attachment.eml @@ -1,56 +1,56 @@ -Return-Path: -Delivered-To: -Received: from mx.domain.tld - by localhost (Dovecot) with LMTP id T7mwLn3ddlvKWwAA0J78UA - for ; Fri, 17 Aug 2018 16:36:45 +0200 -Received: from localhost (localhost [127.0.0.1]) - by mx.domain.tld (Postfix) with ESMTP id B642913BA0BE2 - for ; Fri, 17 Aug 2018 16:36:45 +0200 (CEST) -X-Virus-Scanned: Debian amavisd-new at mx.domain.tld -X-Spam-Flag: NO -X-Spam-Score: 1.103 -X-Spam-Level: * -X-Spam-Status: No, score=1.103 required=6.31 tests=[ALL_TRUSTED=-1, - OBFU_TEXT_ATTACH=1, TRACKER_ID=1.102, TVD_SPACE_RATIO=0.001] - autolearn=no autolearn_force=no -Received: from mx.domain.tld ([127.0.0.1]) - by localhost (mx.domain.tld [127.0.0.1]) (amavisd-new, port 10024) - with ESMTP id L8E9vyX80d44 for ; - Fri, 17 Aug 2018 16:36:39 +0200 (CEST) -Received: from [127.0.0.1] (ip.dynamic.domain.tld [192.168.0.24]) - (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) - (No client certificate requested) - by mx.domain.tld (Postfix) with ESMTPSA id EF01E13BA0BD7 - for ; Fri, 17 Aug 2018 16:36:38 +0200 (CEST) -Sender: testsender -Message-ID: -Date: Fri, 17 Aug 2018 14:36:24 +0000 -Subject: ogqMVHhz7swLaq2PfSWsZj0k99w8wtMbrb4RuHdNg53i76B7icIIM0zIWpwGFtnk -From: testfrom -Reply-To: testreply_to -To: testnameto -Cc: testnamecc -MIME-Version: 1.0 -Content-Type: multipart/mixed; - boundary="_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_" -X-Priority: 1 (Highest) - - - - ---_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_ -Content-Type: text/plain; charset=utf-8 -Content-Transfer-Encoding: quoted-printable - -n1IaXFkbeqKyg4lYToaJ3u1Ond2EDrN3UWuiLFNjOLJEAabSYagYQaOHtV5QDlZE - ---_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_ -Content-Type: application/octet-stream; name=6mfFxiU5Yhv9WYJx.txt -Content-Transfer-Encoding: base64 -Content-Disposition: attachment; filename=6mfFxiU5Yhv9WYJx.txt - -em5rNTUxTVAzVFAzV1BwOUtsMWduTEVycldFZ2tKRkF0dmFLcWtUZ3JrM2RLSThkWDM4WVQ4QmFW -eFJjT0VSTg== - ---_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_-- - +Return-Path: +Delivered-To: +Received: from mx.domain.tld + by localhost (Dovecot) with LMTP id T7mwLn3ddlvKWwAA0J78UA + for ; Fri, 17 Aug 2018 16:36:45 +0200 +Received: from localhost (localhost [127.0.0.1]) + by mx.domain.tld (Postfix) with ESMTP id B642913BA0BE2 + for ; Fri, 17 Aug 2018 16:36:45 +0200 (CEST) +X-Virus-Scanned: Debian amavisd-new at mx.domain.tld +X-Spam-Flag: NO +X-Spam-Score: 1.103 +X-Spam-Level: * +X-Spam-Status: No, score=1.103 required=6.31 tests=[ALL_TRUSTED=-1, + OBFU_TEXT_ATTACH=1, TRACKER_ID=1.102, TVD_SPACE_RATIO=0.001] + autolearn=no autolearn_force=no +Received: from mx.domain.tld ([127.0.0.1]) + by localhost (mx.domain.tld [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id L8E9vyX80d44 for ; + Fri, 17 Aug 2018 16:36:39 +0200 (CEST) +Received: from [127.0.0.1] (ip.dynamic.domain.tld [192.168.0.24]) + (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) + (No client certificate requested) + by mx.domain.tld (Postfix) with ESMTPSA id EF01E13BA0BD7 + for ; Fri, 17 Aug 2018 16:36:38 +0200 (CEST) +Sender: testsender +Message-ID: +Date: Fri, 17 Aug 2018 14:36:24 +0000 +Subject: ogqMVHhz7swLaq2PfSWsZj0k99w8wtMbrb4RuHdNg53i76B7icIIM0zIWpwGFtnk +From: testfrom +Reply-To: testreply_to +To: testnameto +Cc: testnamecc +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_" +X-Priority: 1 (Highest) + + + + +--_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_ +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +n1IaXFkbeqKyg4lYToaJ3u1Ond2EDrN3UWuiLFNjOLJEAabSYagYQaOHtV5QDlZE + +--_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_ +Content-Type: application/octet-stream; name=6mfFxiU5Yhv9WYJx.txt +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=6mfFxiU5Yhv9WYJx.txt + +em5rNTUxTVAzVFAzV1BwOUtsMWduTEVycldFZ2tKRkF0dmFLcWtUZ3JrM2RLSThkWDM4WVQ4QmFW +eFJjT0VSTg== + +--_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_-- + From 861557cc7519922a5cc3b644ac1faa3cfdc9af7f Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 04:38:11 +0100 Subject: [PATCH 167/203] Filename sanitization optimized to remove any leftover dot prefix --- src/Attachment.php | 8 ++++++-- tests/AttachmentTest.php | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index 19b639f6..17d25efa 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -324,13 +324,17 @@ public function decodeName(?string $name): string { } // sanitize $name - // order of '..' is important $replaces = [ '/\\\\/' => '', '/[\/\0:]+/' => '', '/\.+/' => '.', ]; - return preg_replace(array_keys($replaces), array_values($replaces), $name); + $name_starts_with_dots = str_starts_with($name, '..'); + $name = preg_replace(array_keys($replaces), array_values($replaces), $name); + if($name_starts_with_dots) { + return substr($name, 1); + } + return $name; } return ""; } diff --git a/tests/AttachmentTest.php b/tests/AttachmentTest.php index 566ea658..c9ba2f17 100644 --- a/tests/AttachmentTest.php +++ b/tests/AttachmentTest.php @@ -28,7 +28,7 @@ public function testDecodeName(string $input, string $output): void public function decodeNameDataProvider(): array { return [ - ['../../../../../../../../../../../var/www/shell.php', '.varwwwshell.php'], + ['../../../../../../../../../../../var/www/shell.php', 'varwwwshell.php'], ['test..xml', 'test.xml'], [chr(0), ''], ['C:\\file.txt', 'Cfile.txt'], From 7484a0ed372e8741a70dc3b295c4b2a90debf06b Mon Sep 17 00:00:00 2001 From: grnsv <70471000+grnsv@users.noreply.github.com> Date: Fri, 17 Jan 2025 06:40:14 +0300 Subject: [PATCH 168/203] Fix decode filename (#535) --- src/Attachment.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Attachment.php b/src/Attachment.php index 17d25efa..e5a035bc 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -307,6 +307,7 @@ public function decodeName(?string $name): string { if (str_contains($name, "''")) { $parts = explode("''", $name); if (EncodingAliases::has($parts[0])) { + $encoding = $parts[0]; $name = implode("''", array_slice($parts, 1)); } } @@ -323,6 +324,10 @@ public function decodeName(?string $name): string { $name = urldecode($name); } + if (isset($encoding)) { + $name = EncodingAliases::convert($name, $encoding); + } + // sanitize $name $replaces = [ '/\\\\/' => '', From 490ff39941f531d09c3b61d061b8d362f1b6b23d Mon Sep 17 00:00:00 2001 From: grnsv <70471000+grnsv@users.noreply.github.com> Date: Fri, 17 Jan 2025 06:41:21 +0300 Subject: [PATCH 169/203] Fixed annotations (#536) --- src/Attachment.php | 24 ++++++++++++------------ src/Message.php | 32 ++++++++++++++++---------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index e5a035bc..2e0e5821 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -24,18 +24,18 @@ * * @package Webklex\PHPIMAP * - * @property integer part_number - * @property integer size - * @property string content - * @property string type - * @property string content_type - * @property string id - * @property string hash - * @property string name - * @property string description - * @property string filename - * @property ?string disposition - * @property string img_src + * @property integer $part_number + * @property integer $size + * @property string $content + * @property string $type + * @property string $content_type + * @property string $id + * @property string $hash + * @property string $name + * @property string $description + * @property string $filename + * @property ?string $disposition + * @property string $img_src * * @method integer getPartNumber() * @method integer setPartNumber(integer $part_number) diff --git a/src/Message.php b/src/Message.php index b0dabb2d..66193436 100755 --- a/src/Message.php +++ b/src/Message.php @@ -47,22 +47,22 @@ * * @package Webklex\PHPIMAP * - * @property integer msglist - * @property integer uid - * @property integer msgn - * @property integer size - * @property Attribute subject - * @property Attribute message_id - * @property Attribute message_no - * @property Attribute references - * @property Attribute date - * @property Attribute from - * @property Attribute to - * @property Attribute cc - * @property Attribute bcc - * @property Attribute reply_to - * @property Attribute in_reply_to - * @property Attribute sender + * @property integer $msglist + * @property integer $uid + * @property integer $msgn + * @property integer $size + * @property Attribute $subject + * @property Attribute $message_id + * @property Attribute $message_no + * @property Attribute $references + * @property Attribute $date + * @property Attribute $from + * @property Attribute $to + * @property Attribute $cc + * @property Attribute $bcc + * @property Attribute $reply_to + * @property Attribute $in_reply_to + * @property Attribute $sender * * @method integer getMsglist() * @method integer setMsglist($msglist) From 2d91d33cc100c9256f9623a779d803fa627b13d3 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 04:54:17 +0100 Subject: [PATCH 170/203] Fixture adjusted --- tests/fixtures/AttachmentLongFilenameTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures/AttachmentLongFilenameTest.php b/tests/fixtures/AttachmentLongFilenameTest.php index 650a2dcb..5a3e00ed 100644 --- a/tests/fixtures/AttachmentLongFilenameTest.php +++ b/tests/fixtures/AttachmentLongFilenameTest.php @@ -53,7 +53,7 @@ public function testFixture() : void { $attachment = $attachments[1]; self::assertInstanceOf(Attachment::class, $attachment); self::assertEquals('01_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); - self::assertEquals("f7b5181985862431bfc443d26e3af2371e20a0afd676eeb9b9595a26d42e0b73", hash("sha256", $attachment->filename)); + self::assertEquals("cebd34e48eaa06311da3d3130d5a9b465b096dc1094a6548f8c94c24ca52f34e", hash("sha256", $attachment->filename)); self::assertEquals('text', $attachment->type); self::assertEquals('txt', $attachment->getExtension()); self::assertEquals("text/plain", $attachment->content_type); From d760c9f6bbf60f0d7d6687672647919dc684de06 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 04:55:45 +0100 Subject: [PATCH 171/203] Changelog updated --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c85451bd..b83c9016 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Fix - Query - Chunked - Resolved infinite loop when start chunk > 1 #477 (thanks @NeekTheNook) - Attachment with symbols in filename #436 (thanks @nuernbergerA) - Ignore possible untagged lines after IDLE and DONE commands #445 (thanks @gazben) +- Fix Empty Child Folder Error #474 (thanks @bierpub) +- Filename sanitization improved #501 (thanks @neolip) +- `Client::getFolderPath()` return null if folder is not set #506 (thanks @arnolem) +- Fix implicit marking of parameters as nullable, deprecated in PHP 8.4 #518 (thanks @campbell-m) ### Added - IMAP STATUS command support added `Folder::status()` #424 (thanks @InterLinked1) @@ -21,6 +25,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Config handling moved into a new class `Config::class` to allow class serialization (sponsored by elb-BIT GmbH) - Support for Carbon 3 added #483 - Custom decoder support added +- Decoding filename with non-standard encoding #535 (thanks @grnsv) ### Breaking changes - The decoder config has been moved from `options.decoder` to `decoding` and contains now the `decoder` class to used as well as their decoding fallbacks From 352901f470a98506b6f277af78e0fff04b35d4ba Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 04:56:54 +0100 Subject: [PATCH 172/203] Alternatives added & discord link updated --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2eb4ef40..ec49921e 100755 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Discord: [discord.gg/rd4cN9h6][link-discord] - [Known issues](#known-issues) - [Support](#support) - [Features & pull requests](#features--pull-requests) +- [Alternatives & Different Flavors](#alternatives--different-flavors) - [Security](#security) - [Credits](#credits) - [License](#license) @@ -193,6 +194,14 @@ first, if you're planning to do bigger changes. Of course, you can also create a if you're just wishing a feature ;) +## Alternatives & Different Flavors +This library and especially the code flavor It's written in, is certainly not for everyone. If you are looking for a +different approach, you might want to check out the following libraries: +- [ddeboer/imap](https://github.com/ddeboer/imap) +- [barbushin/php-imap](https://github.com/barbushin/php-imap) +- [stevebauman/php-imap](https://github.com/stevebauman/php-imap) + + ## Change log Please see [CHANGELOG][link-changelog] for more information what has changed recently. @@ -226,7 +235,7 @@ The MIT License (MIT). Please see [License File][link-license] for more informat [link-changelog]: https://github.com/Webklex/php-imap/blob/master/CHANGELOG.md [link-hits]: https://hits.webklex.com [link-snyk]: https://snyk.io/vuln/composer:webklex%2Fphp-imap -[link-discord]: https://discord.gg/rd4cN9h6 +[link-discord]: https://discord.gg/vUHrbfbDr9 [ico-sponsor-feline]: https://cdn.feline.dk/public/feline.png From 0aa3b670d3518d53170dd74026b1667a341182bc Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 04:58:13 +0100 Subject: [PATCH 173/203] Release information added --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b83c9016..0e5a032e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Breaking changes +- NaN + +## [6.0.0] - 2025-01-17 +### Fixed - Fixed date issue if timezone is UT and a 2 digit year #429 (thanks @ferrisbuellers) - Make the space optional after a comma separator #437 (thanks @marc0adam) - Fix bug when multipart message getHTMLBody() method returns null #455 (thanks @michalkortas) From 45f03cfe4774b99989794e3e7eeea65414f28f81 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 16:10:24 +0100 Subject: [PATCH 174/203] Typo fixed --- src/config/imap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/imap.php b/src/config/imap.php index 7c184435..b628a0fd 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -167,7 +167,7 @@ /** * |-------------------------------------------------------------------------- - * | Available IMAP options + * | Available decoding options * |-------------------------------------------------------------------------- * | * | Available php imap config parameters are listed below From f5dd36838cb10973c7e7d6695967ec6a84ef4e17 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 16:27:49 +0100 Subject: [PATCH 175/203] Filename sanitization made optional --- CHANGELOG.md | 2 +- src/Attachment.php | 35 +++++++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e5a032e..2f972ef9 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Filename sanitization is now optional (enabled via default) ### Added - NaN diff --git a/src/Attachment.php b/src/Attachment.php index 2e0e5821..223de45e 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -328,17 +328,10 @@ public function decodeName(?string $name): string { $name = EncodingAliases::convert($name, $encoding); } - // sanitize $name - $replaces = [ - '/\\\\/' => '', - '/[\/\0:]+/' => '', - '/\.+/' => '.', - ]; - $name_starts_with_dots = str_starts_with($name, '..'); - $name = preg_replace(array_keys($replaces), array_values($replaces), $name); - if($name_starts_with_dots) { - return substr($name, 1); + if($this->config->get('security.sanitize_filenames', true)) { + $name = $this->sanitizeName($name); } + return $name; } return ""; @@ -497,4 +490,26 @@ public function setDecoder(DecoderInterface $decoder): static { $this->decoder = $decoder; return $this; } + + /** + * Sanitize a given name to prevent common attacks + * !!IMPORTANT!! Do not rely on this method alone - this is just the bare minimum. Additional measures should be taken + * to ensure that the file is safe to use. + * @param string $name + * + * @return string + */ + private function sanitizeName(string $name): string { + $replaces = [ + '/\\\\/' => '', + '/[\/\0:]+/' => '', + '/\.+/' => '.', + ]; + $name_starts_with_dots = str_starts_with($name, '..'); + $name = preg_replace(array_keys($replaces), array_values($replaces), $name); + if($name_starts_with_dots) { + return substr($name, 1); + } + return $name; + } } From e07b9660366cfcc0e87f7fb56369cac22bae259a Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 17:30:36 +0100 Subject: [PATCH 176/203] Spoofing detection added #40 --- CHANGELOG.md | 2 +- .../SpoofingAttemptDetectedException.php | 24 ++++++ src/Header.php | 30 +++++++- tests/issues/Issue40Test.php | 74 +++++++++++++++++++ tests/messages/issue-40.eml | 39 ++++++++++ 5 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 src/Exceptions/SpoofingAttemptDetectedException.php create mode 100644 tests/issues/Issue40Test.php create mode 100644 tests/messages/issue-40.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f972ef9..68b0d1e1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Filename sanitization is now optional (enabled via default) ### Added -- NaN +- Spoofing detection added #40 ### Breaking changes - NaN diff --git a/src/Exceptions/SpoofingAttemptDetectedException.php b/src/Exceptions/SpoofingAttemptDetectedException.php new file mode 100644 index 00000000..f3771817 --- /dev/null +++ b/src/Exceptions/SpoofingAttemptDetectedException.php @@ -0,0 +1,24 @@ +rfc822_parse_headers($this->raw); @@ -230,6 +232,11 @@ protected function parse(): void { $this->extractHeaderExtensions(); $this->findPriority(); + + if($this->config->get('security.detect_spoofing', true)) { + // Detect spoofing + $this->detectSpoofing(); + } } /** @@ -413,7 +420,7 @@ private function decodeAddresses($values): array { * @param object $header */ private function extractAddresses(object $header): void { - foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'sender'] as $key) { + foreach (['from', 'to', 'cc', 'bcc', 'reply_to', 'sender', 'return_path', 'envelope_from', 'envelope_to', 'delivered_to'] as $key) { if (property_exists($header, $key)) { $this->set($key, $this->parseAddresses($header->$key)); } @@ -760,4 +767,25 @@ public function setDecoder(DecoderInterface $decoder): static { return $this; } + /** + * Detect spoofing by checking the from, reply_to, return_path, sender and envelope_from headers + * @throws SpoofingAttemptDetectedException + */ + private function detectSpoofing(): void { + $header_keys = ["from", "reply_to", "return_path", "sender", "envelope_from"]; + $potential_senders = []; + foreach($header_keys as $key) { + $header = $this->get($key); + foreach ($header->toArray() as $address) { + $potential_senders[] = $address->mailbox . "@" . $address->host; + } + } + if(count($potential_senders) > 1) { + $this->set("spoofed", true); + if($this->config->get('security.detect_spoofing_exception', false)) { + throw new SpoofingAttemptDetectedException("Potential spoofing detected. Message ID: " . $this->get("message_id") . " Senders: " . implode(", ", $potential_senders)); + } + } + } + } diff --git a/tests/issues/Issue40Test.php b/tests/issues/Issue40Test.php new file mode 100644 index 00000000..7288484c --- /dev/null +++ b/tests/issues/Issue40Test.php @@ -0,0 +1,74 @@ +getFixture("issue-40.eml"); + + self::assertSame("Zly from", (string)$message->subject); + self::assertSame([ + 'personal' => '', + 'mailbox' => 'faked_sender', + 'host' => 'sender_domain.pl', + 'mail' => 'faked_sender@sender_domain.pl', + 'full' => 'faked_sender@sender_domain.pl', + ], $message->from->first()->toArray()); + self::assertSame([ + 'personal' => '', + 'mailbox' => 'real_sender', + 'host' => 'sender_domain.pl', + 'mail' => 'real_sender@sender_domain.pl', + 'full' => ' ', + ], (array)$message->return_path->first()); + self::assertSame(true, $message->spoofed->first()); + + $config = $message->getConfig(); + self::assertSame(false, $config->get("security.detect_spoofing_exception")); + $config->set("security.detect_spoofing_exception", true); + self::assertSame(true, $config->get("security.detect_spoofing_exception")); + + $this->expectException(SpoofingAttemptDetectedException::class); + $this->getFixture("issue-40.eml", $config); + } + +} \ No newline at end of file diff --git a/tests/messages/issue-40.eml b/tests/messages/issue-40.eml new file mode 100644 index 00000000..52e7ff9a --- /dev/null +++ b/tests/messages/issue-40.eml @@ -0,0 +1,39 @@ +Return-Path: +Delivered-To: receipent@receipent_domain.pl +Received: from h2.server.pl +\tby h2.server.pl with LMTP +\tid 4IDTIEUkm18ZSSkA87l24w +\t(envelope-from ) +\tfor ; Thu, 29 Oct 2020 21:21:25 +0100 +Return-path: +Envelope-to: receipent@receipent_domain.pl +Delivery-date: Thu, 29 Oct 2020 21:21:25 +0100 +Received: from sender_domain.pl ([server ip]) +\tby h2.server.pl with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 +\t(Exim 4.94) +\t(envelope-from ) +\tid 1kYEQG-00BPgD-S0 +\tfor receipent@receipent_domain.pl; Thu, 29 Oct 2020 21:21:25 +0100 +Received: by sender_domain.pl (Postfix, from userid 1000) +\tid 57DADAB; Thu, 29 Oct 2020 21:21:23 +0100 (CET) +DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=sender_domain.pl; s=default; +\tt=1604002883; bh=CsZufJouWdjY/W12No6MSSMwbp0VaS8EOMGg9WptEaI=; +\th=From:To:Subject:Date; +\tb=v0NAncnNT/w+gInANxAkMt20ktM4LZquuwlokUmLpPyO3++8dy112olu63Dkn9L2E +\t GwfHGqW+8f7g494UK6asUKqTx8fHxlEJbHqAiEV5QrlynSeZDFXsKvGDW8XNMFBKop +\t sAjvp8NTUiNcA4MTbFaZ7RX15A/9d9QVEynU8MaNP2ZYKnq9J/JXgUjjMnx+FiULqf +\t xJN/5rjwHRx7f6JQoXXUxuck6Zh4tSDiLLnDFasrSxed6sTNfnZMAggCyb1++estNk +\t q6HNBwp85Az3ELo10RbBF/WM2FhxxFz1khncRtCyLXLUZ2lzhjan765KXpeYg7FUa9 +\t zItPWVTaTzTEg== +From: faked_sender@sender_domain.pl +To: receipent@receipent_domain.pl +Subject: Zly from +Message-Id: <20201029202123.57DADAB@sender_domain.pl> +Date: Thu, 29 Oct 2020 21:21:01 +0100 (CET) +Forward-Confirmed-ReverseDNS: Reverse and forward lookup success on server ip, -10 Spam score +SPFCheck: Server passes SPF test, -30 Spam score +X-DKIM: signer='sender_domain.pl' status='pass' reason='' +DKIMCheck: Server passes DKIM test, -20 Spam score +X-Spam-Score: -0.2 (/) + +Test message From 916e273d102c6e4b8f10363a500d8caa6ab94111 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 17:32:22 +0100 Subject: [PATCH 177/203] Address parsing improved and extended to include more cases --- CHANGELOG.md | 1 + src/Header.php | 30 ++++++++++++++++++- tests/HeaderTest.php | 7 +++-- tests/MessageTest.php | 6 ++-- tests/fixtures/BccTest.php | 10 +++++-- tests/fixtures/ExampleBounceTest.php | 8 ++++- tests/fixtures/UndefinedCharsetHeaderTest.php | 14 ++++++--- 7 files changed, 62 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b0d1e1..bba62dc1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed - Filename sanitization is now optional (enabled via default) +- Address parsing improved and extended to include more cases ### Added - Spoofing detection added #40 diff --git a/src/Header.php b/src/Header.php index c294d7f6..604eb896 100644 --- a/src/Header.php +++ b/src/Header.php @@ -437,7 +437,35 @@ private function parseAddresses($list): array { $addresses = []; if (is_array($list) === false) { - return $addresses; + if(is_string($list)) { + // $list = "" + if (preg_match( + '/^(?:(?P.+)\s)?(?(name)<|[^\s]+?)(?(name)>|>?)$/', + $list, + $matches + )) { + $name = trim(rtrim($matches["name"])); + $email = trim(rtrim($matches["email"])); + list($mailbox, $host) = array_pad(explode("@", $email), 2, null); + if($mailbox === ">") { // Fix trailing ">" in malformed mailboxes + $mailbox = ""; + } + if($name === "" && $mailbox === "" && $host === "") { + return $addresses; + } + $list = [ + (object)[ + "personal" => $name, + "mailbox" => $mailbox, + "host" => $host, + ] + ]; + }else{ + return $addresses; + } + }else{ + return $addresses; + } } foreach ($list as $item) { diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index 767f4388..7c27944f 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -50,7 +50,8 @@ public function testHeaderParsing(): void { $header = new Header($raw_header, $this->config); $subject = $header->get("subject"); - $returnPath = $header->get("Return-Path"); + $returnPath = $header->get("return_path"); + var_dump($returnPath); /** @var Carbon $date */ $date = $header->get("date")->first(); /** @var Address $from */ @@ -69,7 +70,7 @@ public function testHeaderParsing(): void { self::assertInstanceOf(Attribute::class, $subject); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", $subject->toString()); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", (string)$header->subject); - self::assertSame("", $returnPath->toString()); + self::assertSame("noreply@github.com", $returnPath->toString()); self::assertSame("return_path", $returnPath->getName()); self::assertSame("-4.299", (string)$header->get("X-Spam-Score")); self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$header->get("Message-ID")); @@ -91,7 +92,7 @@ public function testHeaderParsing(): void { self::assertInstanceOf(Carbon::class, $date); self::assertSame("2022-12-26 08:07:14 GMT-0800", $date->format("Y-m-d H:i:s T")); - self::assertSame(50, count($header->getAttributes())); + self::assertSame(51, count($header->getAttributes())); } public function testRfc822ParseHeaders() { diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 72e32f5e..0ce55cd0 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -120,7 +120,7 @@ public function testMessage(): void { self::assertInstanceOf(Attribute::class, $subject); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", $subject->toString()); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", (string)$message->subject); - self::assertSame("", $returnPath->toString()); + self::assertSame("noreply@github.com", $returnPath->toString()); self::assertSame("return_path", $returnPath->getName()); self::assertSame("-4.299", (string)$message->get("X-Spam-Score")); self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$message->get("Message-ID")); @@ -182,7 +182,7 @@ public function testLoadMessageFromFile(): void { self::assertInstanceOf(Attribute::class, $subject); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", $subject->toString()); self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", (string)$message->subject); - self::assertSame("", $returnPath->toString()); + self::assertSame("noreply@github.com", $returnPath->toString()); self::assertSame("return_path", $returnPath->getName()); self::assertSame("-4.299", (string)$message->get("X-Spam-Score")); self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$message->get("Message-ID")); @@ -201,7 +201,7 @@ public function testLoadMessageFromFile(): void { self::assertInstanceOf(Attribute::class, $subject); self::assertSame("ogqMVHhz7swLaq2PfSWsZj0k99w8wtMbrb4RuHdNg53i76B7icIIM0zIWpwGFtnk", $subject->toString()); self::assertSame("ogqMVHhz7swLaq2PfSWsZj0k99w8wtMbrb4RuHdNg53i76B7icIIM0zIWpwGFtnk", (string)$message->subject); - self::assertSame("", $returnPath->toString()); + self::assertSame("someone@domain.tld", $returnPath->toString()); self::assertSame("return_path", $returnPath->getName()); self::assertSame("1.103", (string)$message->get("X-Spam-Score")); self::assertSame("d3a5e91963cb805cee975687d5acb1c6@swift.generated", (string)$message->get("Message-ID")); diff --git a/tests/fixtures/BccTest.php b/tests/fixtures/BccTest.php index 8de14f62..be8ef72e 100644 --- a/tests/fixtures/BccTest.php +++ b/tests/fixtures/BccTest.php @@ -24,11 +24,17 @@ class BccTest extends FixtureTestCase { * * @return void */ - public function testFixture() : void { + public function testFixture(): void { $message = $this->getFixture("bcc.eml"); self::assertEquals("test", $message->subject); - self::assertEquals("", $message->return_path); + self::assertSame([ + 'personal' => '', + 'mailbox' => 'return-path', + 'host' => 'here.com', + 'mail' => 'return-path@here.com', + 'full' => 'return-path@here.com', + ], $message->return_path->first()->toArray()); self::assertEquals("1.0", $message->mime_version); self::assertEquals("text/plain", $message->content_type); self::assertEquals("Hi!", $message->getTextBody()); diff --git a/tests/fixtures/ExampleBounceTest.php b/tests/fixtures/ExampleBounceTest.php index c92d35c6..8153518d 100644 --- a/tests/fixtures/ExampleBounceTest.php +++ b/tests/fixtures/ExampleBounceTest.php @@ -29,7 +29,13 @@ class ExampleBounceTest extends FixtureTestCase { public function testFixture(): void { $message = $this->getFixture("example_bounce.eml"); - self::assertEquals("<>", $message->return_path); + self::assertEquals([ + 'personal' => '', + 'mailbox' => '', + 'host' => '', + 'mail' => '', + 'full' => '', + ], (array)$message->return_path->first()); self::assertEquals([ 0 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>); Thu, 02 Mar 2023 05:27:29 +0100', 1 => 'from somewhere06.your-server.de ([1b21:2f8:e0a:50e4::2]) by somewhere.your-server.de with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384 (Exim 4.94.2) id 1pXaXR-0006xQ-BN for demo@foo.de; Thu, 02 Mar 2023 05:27:29 +0100', diff --git a/tests/fixtures/UndefinedCharsetHeaderTest.php b/tests/fixtures/UndefinedCharsetHeaderTest.php index b2f9d8fb..f7a51cb0 100644 --- a/tests/fixtures/UndefinedCharsetHeaderTest.php +++ b/tests/fixtures/UndefinedCharsetHeaderTest.php @@ -26,7 +26,7 @@ class UndefinedCharsetHeaderTest extends FixtureTestCase { * * @return void */ - public function testFixture() : void { + public function testFixture(): void { $message = $this->getFixture("undefined_charset_header.eml"); self::assertEquals("", $message->get("x-real-to")); @@ -34,10 +34,16 @@ public function testFixture() : void { self::assertEquals("Mon, 27 Feb 2017 13:21:44 +0930", $message->get("Resent-Date")); self::assertEquals("", $message->get("Resent-From")); self::assertEquals("BlaBla", $message->get("X-Stored-In")); - self::assertEquals("", $message->get("Return-Path")); + self::assertSame([ + 'personal' => '', + 'mailbox' => 'info', + 'host' => 'bla.bla', + 'mail' => 'info@bla.bla', + 'full' => 'info@bla.bla', + ], $message->get("Return-Path")->first()->toArray()); self::assertEquals([ - 'from by bla.bla (CommuniGate Pro RULE 6.1.13) with RULE id 14057804; Mon, 27 Feb 2017 13:21:44 +0930', - ], $message->get("Received")->all()); + 'from by bla.bla (CommuniGate Pro RULE 6.1.13) with RULE id 14057804; Mon, 27 Feb 2017 13:21:44 +0930', + ], $message->get("Received")->all()); self::assertEquals(")", $message->getHTMLBody()); self::assertFalse($message->hasTextBody()); self::assertEquals("2017-02-27 03:51:29", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); From cafda4fc8514e552c4aebc49cc636c8debdde11c Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 17:32:51 +0100 Subject: [PATCH 178/203] Security configuration options added --- CHANGELOG.md | 1 + src/config/imap.php | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bba62dc1..beb11276 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Address parsing improved and extended to include more cases ### Added +- Security configuration options added - Spoofing detection added #40 ### Breaking changes diff --git a/src/config/imap.php b/src/config/imap.php index b628a0fd..8e39fe8e 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -35,6 +35,32 @@ */ 'default' => 'default', + /* + |-------------------------------------------------------------------------- + | Security options + |-------------------------------------------------------------------------- + | + | You can enable or disable certain security features here by setting them to true or false to enable or disable + | them. + | -detect_spoofing: + | Detect spoofing attempts by checking the message sender against the message headers. + | Default TRUE + | -detect_spoofing_exception: + | Throw an exception if a spoofing attempt is detected. + | Default FALSE + | -sanitize_filenames: + | Sanitize attachment filenames by removing any unwanted and potentially dangerous characters. This is not a + | 100% secure solution, but it should help to prevent some common attacks. Please sanitize the filenames + | again if you need a more secure solution. + | Default TRUE + | + */ + 'security' => [ + "detect_spoofing" => true, + "detect_spoofing_exception" => false, + "sanitize_filenames" => true, + ], + /* |-------------------------------------------------------------------------- | Available accounts From 512f9f5e2fb273c4dd7a07986fe1818d36b7430f Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 18:01:28 +0100 Subject: [PATCH 179/203] RFC4315 MOVE fallback added #123 (thanks @freescout-help-desk) --- CHANGELOG.md | 1 + src/Connection/Protocols/ImapProtocol.php | 42 +++++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index beb11276..7f197ffc 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Added - Security configuration options added - Spoofing detection added #40 +- RFC4315 MOVE fallback added #123 (thanks @freescout-help-desk) ### Breaking changes - NaN diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 9272be91..da7483b8 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -1141,7 +1141,25 @@ public function moveMessage(string $folder, $from, ?int $to = null, int|string $ $set = $this->buildSet($from, $to); $command = $this->buildUIDCommand("MOVE", $uid); - return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); + $result = $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); + // RFC4315 fallback to COPY, STORE and EXPUNGE. + // Required for cases where MOVE isn't supported by the server. So we copy the message to the target folder, + // mark the original message as deleted and expunge the mailbox. + // See the following links for more information: + // - https://github.com/freescout-help-desk/freescout/issues/4313 + // - https://github.com/Webklex/php-imap/issues/123 + if (!$result->boolean()) { + $result = $this->copyMessage($folder, $from, $to, $uid); + if (!$result->boolean()) { + return $result; + } + $result = $this->store(['\Deleted'], $from, $to, null, true, $uid); + if (!$result->boolean()) { + return $result; + } + return $this->expunge(); + } + return $result; } /** @@ -1163,7 +1181,27 @@ public function moveManyMessages(array $messages, string $folder, int|string $ui $set = implode(',', $messages); $tokens = [$set, $this->escapeString($folder)]; - return $this->requestAndResponse($command, $tokens, true); + $result = $this->requestAndResponse($command, $tokens, true); + // RFC4315 fallback to COPY, STORE and EXPUNGE. + // Required for cases where MOVE isn't supported by the server. So we copy the message to the target folder, + // mark the original message as deleted and expunge the mailbox. + // See the following links for more information: + // - https://github.com/freescout-help-desk/freescout/issues/4313 + // - https://github.com/Webklex/php-imap/issues/123 + if (!$result->boolean()) { + $result = $this->copyManyMessages($messages, $folder, $uid); + if (!$result->boolean()) { + return $result; + } + foreach ($messages as $message) { + $result = $this->store(['\Deleted'], $message, $message, null, true, $uid); + if (!$result->boolean()) { + return $result; + } + } + return $this->expunge(); + } + return $result; } /** From 0a9b263eb4e29c2822cf7d68bec27a9af33ced2f Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 19:36:00 +0100 Subject: [PATCH 180/203] Boundary parsing fixed and improved to support more formats #544 --- CHANGELOG.md | 1 + src/Structure.php | 5 +- tests/HeaderTest.php | 1 - tests/fixtures/BooleanDecodedContentTest.php | 2 +- ...lWithoutContentDispositionEmbeddedTest.php | 4 +- ...ddedEmailWithoutContentDispositionTest.php | 8 +- .../MultipleHtmlPartsAndAttachmentsTest.php | 4 +- .../MultipleNestedAttachmentsTest.php | 4 +- .../NestesEmbeddedWithAttachmentTest.php | 4 +- tests/fixtures/PecTest.php | 6 +- tests/issues/Issue544Test.php | 61 +++++ tests/messages/issue-544.eml | 220 ++++++++++++++++++ 12 files changed, 302 insertions(+), 18 deletions(-) create mode 100644 tests/issues/Issue544Test.php create mode 100644 tests/messages/issue-544.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f197ffc..c532bb97 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ### Fixed - Filename sanitization is now optional (enabled via default) - Address parsing improved and extended to include more cases +- Boundary parsing fixed and improved to support more formats #544 ### Added - Security configuration options added diff --git a/src/Structure.php b/src/Structure.php index 4907c542..11f4cd66 100644 --- a/src/Structure.php +++ b/src/Structure.php @@ -132,7 +132,10 @@ private function parsePart(string $context, int $part_number = 0): array { * @throws InvalidMessageDateException */ private function detectParts(string $boundary, string $context, int $part_number = 0): array { - $base_parts = explode( $boundary, $context); + $base_parts = explode( "--".$boundary, $context); + if(count($base_parts) == 0) { + $base_parts = explode($boundary, $context); + } $final_parts = []; foreach($base_parts as $ctx) { $ctx = substr($ctx, 2); diff --git a/tests/HeaderTest.php b/tests/HeaderTest.php index 7c27944f..0ec60c6c 100644 --- a/tests/HeaderTest.php +++ b/tests/HeaderTest.php @@ -51,7 +51,6 @@ public function testHeaderParsing(): void { $header = new Header($raw_header, $this->config); $subject = $header->get("subject"); $returnPath = $header->get("return_path"); - var_dump($returnPath); /** @var Carbon $date */ $date = $header->get("date")->first(); /** @var Address $from */ diff --git a/tests/fixtures/BooleanDecodedContentTest.php b/tests/fixtures/BooleanDecodedContentTest.php index e49229a4..5d15fe57 100644 --- a/tests/fixtures/BooleanDecodedContentTest.php +++ b/tests/fixtures/BooleanDecodedContentTest.php @@ -48,7 +48,7 @@ public function testFixture() : void { self::assertEquals("application/pdf", $attachment->content_type); self::assertEquals("1c449aaab4f509012fa5eaa180fd017eb7724ccacabdffc1c6066d3756dcde5c", hash("sha256", $attachment->content)); self::assertEquals(53, $attachment->size); - self::assertEquals(3, $attachment->part_number); + self::assertEquals(2, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); } diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php index a620936b..387b0de0 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php @@ -55,7 +55,7 @@ public function testFixture() : void { self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); self::assertEquals(40, $attachment->size); - self::assertEquals(3, $attachment->part_number); + self::assertEquals(2, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -67,7 +67,7 @@ public function testFixture() : void { self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); self::assertEquals(40, $attachment->size); - self::assertEquals(4, $attachment->part_number); + self::assertEquals(3, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); } diff --git a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php index 36287a6b..2ec5a4fa 100644 --- a/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php +++ b/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -54,7 +54,7 @@ public function testFixture() : void { self::assertEquals("image/jpeg", $attachment->content_type); self::assertEquals("6b7fa434f92a8b80aab02d9bf1a12e49ffcae424e4013a1c4f68b67e3d2bbcd0", hash("sha256", $attachment->content)); self::assertEquals(96, $attachment->size); - self::assertEquals(3, $attachment->part_number); + self::assertEquals(2, $attachment->part_number); self::assertEquals("inline", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -66,7 +66,7 @@ public function testFixture() : void { self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("2476c8b91a93c6b2fe1bfff593cb55956c2fe8e7ca6de9ad2dc9d101efe7a867", hash("sha256", $attachment->content)); self::assertEquals(2073, $attachment->size); - self::assertEquals(5, $attachment->part_number); + self::assertEquals(3, $attachment->part_number); self::assertNull($attachment->disposition); self::assertNotEmpty($attachment->id); @@ -78,7 +78,7 @@ public function testFixture() : void { self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); self::assertEquals(40, $attachment->size); - self::assertEquals(6, $attachment->part_number); + self::assertEquals(4, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -90,7 +90,7 @@ public function testFixture() : void { self::assertEquals("application/x-zip-compressed", $attachment->content_type); self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); self::assertEquals(40, $attachment->size); - self::assertEquals(7, $attachment->part_number); + self::assertEquals(5, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); } diff --git a/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php b/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php index c08c03e0..48bc0172 100644 --- a/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php +++ b/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php @@ -57,7 +57,7 @@ public function testFixture() : void { self::assertEquals("application/pdf", $attachment->content_type); self::assertEquals("c162adf19e0f67e26ef0b7f791b33a60b2c23b175560a505dc7f9ec490206e49", hash("sha256", $attachment->content)); self::assertEquals(4814, $attachment->size); - self::assertEquals(4, $attachment->part_number); + self::assertEquals(2, $attachment->part_number); self::assertEquals("inline", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -69,7 +69,7 @@ public function testFixture() : void { self::assertEquals("application/pdf", $attachment->content_type); self::assertEquals("a337b37e9d3edb172a249639919f0eee3d344db352046d15f8f9887e55855a25", hash("sha256", $attachment->content)); self::assertEquals(5090, $attachment->size); - self::assertEquals(6, $attachment->part_number); + self::assertEquals(4, $attachment->part_number); self::assertEquals("inline", $attachment->disposition); self::assertNotEmpty($attachment->id); } diff --git a/tests/fixtures/MultipleNestedAttachmentsTest.php b/tests/fixtures/MultipleNestedAttachmentsTest.php index 543c0e21..a3cba097 100644 --- a/tests/fixtures/MultipleNestedAttachmentsTest.php +++ b/tests/fixtures/MultipleNestedAttachmentsTest.php @@ -50,7 +50,7 @@ public function testFixture() : void { self::assertEquals("image/png", $attachment->content_type); self::assertEquals("e0e99b0bd6d5ea3ced99add53cc98b6f8eea6eae8ddd773fd06f3489289385fb", hash("sha256", $attachment->content)); self::assertEquals(114, $attachment->size); - self::assertEquals(5, $attachment->part_number); + self::assertEquals(3, $attachment->part_number); self::assertEquals("inline", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -62,7 +62,7 @@ public function testFixture() : void { self::assertEquals("image/png", $attachment->content_type); self::assertEquals("e0e99b0bd6d5ea3ced99add53cc98b6f8eea6eae8ddd773fd06f3489289385fb", hash("sha256", $attachment->content)); self::assertEquals(114, $attachment->size); - self::assertEquals(8, $attachment->part_number); + self::assertEquals(4, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); } diff --git a/tests/fixtures/NestesEmbeddedWithAttachmentTest.php b/tests/fixtures/NestesEmbeddedWithAttachmentTest.php index c5c8c561..e1ff57ea 100644 --- a/tests/fixtures/NestesEmbeddedWithAttachmentTest.php +++ b/tests/fixtures/NestesEmbeddedWithAttachmentTest.php @@ -50,7 +50,7 @@ public function testFixture() : void { self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("From: from@there.com\r\nTo: to@here.com\r\nSubject: FIRST\r\nDate: Sat, 28 Apr 2018 14:37:16 -0400\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"----=_NextPart_000_222_000\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextPart_000_222_000\r\nContent-Type: multipart/alternative;\r\n boundary=\"----=_NextPart_000_222_111\"\r\n\r\n\r\n------=_NextPart_000_222_111\r\nContent-Type: text/plain;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nPlease respond directly to this email to update your RMA\r\n\r\n\r\n2018-04-17T11:04:03-04:00\r\n------=_NextPart_000_222_111\r\nContent-Type: text/html;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n\r\n\r\n
Please respond directly to this =\r\nemail to=20\r\nupdate your RMA
\r\n\r\n------=_NextPart_000_222_111--\r\n\r\n------=_NextPart_000_222_000\r\nContent-Type: image/png;\r\n name=\"chrome.png\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment;\r\n filename=\"chrome.png\"\r\n\r\niVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0Xk\r\nNTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8\r\nkkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwalju\r\nZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklH\r\npEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZF\r\no0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzv\r\nvhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGd\r\nyKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWf\r\nwtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+u\r\nSi6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm\r\n5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUA\r\nLabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxh\r\neD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T\r\n5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7\r\ns7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3\r\na/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU\r\n9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RH\r\niTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZN\r\nJ3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSu\r\nQmCC\r\n\r\n------=_NextPart_000_222_000--", $attachment->content); self::assertEquals(2535, $attachment->size); - self::assertEquals(5, $attachment->part_number); + self::assertEquals(3, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -62,7 +62,7 @@ public function testFixture() : void { self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("From: from@there.com\r\nTo: to@here.com\r\nSubject: SECOND\r\nDate: Sat, 28 Apr 2018 13:37:30 -0400\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative;\r\n boundary=\"----=_NextPart_000_333_000\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextPart_000_333_000\r\nContent-Type: text/plain;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nT whom it may concern:\r\n------=_NextPart_000_333_000\r\nContent-Type: text/html;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n\r\n\r\n
T whom it may concern:
\r\n\r\n\r\n------=_NextPart_000_333_000--", $attachment->content); self::assertEquals(631, $attachment->size); - self::assertEquals(6, $attachment->part_number); + self::assertEquals(4, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); } diff --git a/tests/fixtures/PecTest.php b/tests/fixtures/PecTest.php index 28ecb27b..341ad185 100644 --- a/tests/fixtures/PecTest.php +++ b/tests/fixtures/PecTest.php @@ -51,7 +51,7 @@ public function testFixture() : void { self::assertEquals("application/xml", $attachment->content_type); self::assertEquals("", $attachment->content); self::assertEquals(8, $attachment->size); - self::assertEquals(4, $attachment->part_number); + self::assertEquals(3, $attachment->part_number); self::assertEquals("inline", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -63,7 +63,7 @@ public function testFixture() : void { self::assertEquals("message/rfc822", $attachment->content_type); self::assertEquals("To: test@example.com\r\nFrom: test@example.com\r\nSubject: test-subject\r\nDate: Mon, 2 Oct 2017 12:13:50 +0200\r\nContent-Type: text/plain; charset=iso-8859-15; format=flowed\r\nContent-Transfer-Encoding: 7bit\r\n\r\ntest-content", $attachment->content); self::assertEquals(216, $attachment->size); - self::assertEquals(5, $attachment->part_number); + self::assertEquals(4, $attachment->part_number); self::assertEquals("inline", $attachment->disposition); self::assertNotEmpty($attachment->id); @@ -75,7 +75,7 @@ public function testFixture() : void { self::assertEquals("application/x-pkcs7-signature", $attachment->content_type); self::assertEquals("1", $attachment->content); self::assertEquals(4, $attachment->size); - self::assertEquals(7, $attachment->part_number); + self::assertEquals(5, $attachment->part_number); self::assertEquals("attachment", $attachment->disposition); self::assertNotEmpty($attachment->id); } diff --git a/tests/issues/Issue544Test.php b/tests/issues/Issue544Test.php new file mode 100644 index 00000000..b8ee0e7e --- /dev/null +++ b/tests/issues/Issue544Test.php @@ -0,0 +1,61 @@ +getFixture("issue-544.eml"); + + self::assertSame("Test bad boundary", (string)$message->subject); + + $attachments = $message->getAttachments(); + + self::assertSame(1, $attachments->count()); + + /** @var Attachment $attachment */ + $attachment = $attachments->first(); + self::assertSame("file.pdf", $attachment->name); + self::assertSame("file.pdf", $attachment->filename); + self::assertStringStartsWith("%PDF-1.4", $attachment->content); + self::assertStringEndsWith("%%EOF\n", $attachment->content); + self::assertSame(14938, $attachment->size); + } +} \ No newline at end of file diff --git a/tests/messages/issue-544.eml b/tests/messages/issue-544.eml new file mode 100644 index 00000000..96e2c48c --- /dev/null +++ b/tests/messages/issue-544.eml @@ -0,0 +1,220 @@ +Sender: "test@mail.com" +From: "test@mail.com" +Subject: Test bad boundary +To: "test_1@mail.com" +Cc: +Bcc: +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="-" + +This is a multi-part message in MIME format. + +--- +Content-Type: text/plain + +This message may contain an attachment in a PDF format. + +Special Comments +- +See attached document + +--- +Content-Type: application/pdf; name="file.pdf" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; filename="file.pdf" +Content-MD5: MLGn6wT7mIo/SUBWQ/mmng== + +JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PC9UaXRsZSAoZmlsZS0xLnBkZikKL1Byb2R1Y2VyIChT +a2lhL1BERiBtMTE5IEdvb2dsZSBEb2NzIFJlbmRlcmVyKT4+CmVuZG9iagozIDAgb2JqCjw8L2Nh +IDEKL0JNIC9Ob3JtYWw+PgplbmRvYmoKNSAwIG9iago8PC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCi9M +ZW5ndGggMTkzPj4gc3RyZWFtCnicdZBNDgIhDIX3PUUvILZDoZAYFxrHtYYb+JeYuHC8fyIwo2NM +oASa99FHgZFyLDgvGjs8PeAJRl1VP3sWGUsc9zgmww2We4u3FxQe2COT8zhc4AqHPwftysweVKXs +MSazxybBshdkMb4MxXQFnvsyojGw2oipmCzYmk7Vig2YzrgikrjGdAcxpBzVaa6ZwLaCfN55lmhn +0LdAqCAYdsyk4QuYpjtCIC/uB0irghtdOduoEG2B8YG7lD/3DS1VWPgKZW5kc3RyZWFtCmVuZG9i +agoyIDAgb2JqCjw8L1R5cGUgL1BhZ2UKL1Jlc291cmNlcyA8PC9Qcm9jU2V0IFsvUERGIC9UZXh0 +IC9JbWFnZUIgL0ltYWdlQyAvSW1hZ2VJXQovRXh0R1N0YXRlIDw8L0czIDMgMCBSPj4KL0ZvbnQg +PDwvRjQgNCAwIFI+Pj4+Ci9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgov +U3RydWN0UGFyZW50cyAwCi9QYXJlbnQgNiAwIFI+PgplbmRvYmoKNiAwIG9iago8PC9UeXBlIC9Q +YWdlcwovQ291bnQgMQovS2lkcyBbMiAwIFJdPj4KZW5kb2JqCjcgMCBvYmoKPDwvVHlwZSAvQ2F0 +YWxvZwovUGFnZXMgNiAwIFI+PgplbmRvYmoKOCAwIG9iago8PC9MZW5ndGgxIDE3ODUyCi9GaWx0 +ZXIgL0ZsYXRlRGVjb2RlCi9MZW5ndGggODc4OD4+IHN0cmVhbQp4nO16C3wURdbvqerueSWTzOT9 +mGQ6DAmSAQJ5kBAimZAHaowECJggSAJEAogEAoiPlbgughGF9VNUdAXf+GQSIgbUJauuu6IIq6gr +PoiIiroI+ikqSPr7V80EQtR75d7vPvz9tpvzr1NVp05Vn3PqdE0HYkQUCVBp6JiS0jI2n80g4nFo +zR9TOXbCssvWX0SkiHrLmAkTR9v+bF5FxPyoDx07ISNzqefpNMhXoV47qaSiuvKWOd8SZZ9F5Lx1 +xry6RraXf4X+dvTPnLFkkX6v6+0viEwHQFWXNM6a9+oVNeuI7HmoXzarrqmR4sgK/QWQd8y69IpL +Si/57HmiAZgvdEnDzHlL93yTeTsWvJHIck5Dfd3MrqiXoI91QX54AxoisqxJqGON1L9h3qKl/ZZo +DiLtVrRVXjp/Rl32j8Ma8DwPoP/ReXVLG7VN9mb0nY+6flndvPrY2mHvwxiVaCtpnN+0yEinteAv +Ff2NC+sbU9+q2E6UeIQo5M9oU8hCnJzEDAO8sGUVfU0F9Ccyo91BGTQJ2h6HrIa6QvIyBgidP3Nh +vHlU9wVU7KBjm45d6ZAtp11VssVGQ3BrdQvrppM+44qFl5I+a2H9XNIb6qcvJP3SukWXkX5KJ2nx +6+4qOfz2tPCCby2JFtl830cD0kX5SuXILcc2nZjlIEsoqtaTM4oyNMhzPE2gzSGelaJwMxpD5wLP +x81oLG5GE3CLcYqykq3BE1u0dVoWhiUGSuUfdAmPsGg8xKRycanSgr2uirEXjCUfDaYntDe6x7Es +8yjW5hPGNWDFNO0Z4QVS5YoCI6OCVo9Ca6WIGdyifQgNo0toNl1KjfSEGA2dw2jmqRbjo173jJ/Y +mYJ2Hk1TSJEWHhSwsLRtlFwDQS/DSk7xrBeP9c2tXwjZ3hjUK+Q4IodJCgs+kYtipL4oaddzjXeF +pY0GuZro/8mdhzgT9zv/N252kB9SHhS3ukrcWmHgNo2UazXTZOEVFfuY5lBzkGew5pIgz/HMDUFe +gb/PCvJqLxmNEiAV4E3giIpoIfxXBw9WYB9MonrUm9Ayn0S050ifD0V/hWyZT4voCvi6Hn3n0jy0 +z4LsZUAdsaD30qbTeEjNosXg69B6eu2U3COQzMQMw3DrWEGD1P3T2YpRWwheYB3aAyscIue8NDjf +bMzQgL6m4OxN8mmWAGfSENPPhOOZXPxRKuIjaFtPXW2iff9bCv8/uvAsZaByPF9kTxv7G63sxV/X +R36Fto3iQQnawxSvpuENQ8anoIOi7J5tHBT9ouSfY0BHkIg20hNsNj1B2+l5dgSjNtFWaqe/UyyV +0N10Nd1KKxCZk9FyA3w4HhFbQreyeKMdWf9eRO69tBOyF9I18EQMizM+o2W0XHkDo5aTnfohGioR +GTex843FyDL71OsoF7n0MmpkzUa1cbNxi/EAPUhblb8bJygEu2EG7p3Gl9o/jfcQwVPoNrqT9rFb +rE9hB12IfbZV+RNiaJ0yVWXGLOMYVpBCl2MNKmJ0J+vkXmivp09ZHLtaKYaW+w2/8SKkXDQVsbiO +trEcNoanaFOMCmMnctFgWgqtd1IbbcHdQc/RXhaqHTEeMI5QPA3CrloGe7zGOpXuE9d2F8JiGqw0 +kEagZz79mf5Gu5mH/YXP10K1TM2nXWnsQW4bRhOx2ocx8hP2Hb8G9zLlJbXMGI09vpz+KKxNf6UP +WQLLYGPZJD6Qz+f3KAuRKQfJnSdy+A10B7R/wLxsCw/lu5T71cfU46ak7i4jDB5Jo7vw/v0Ls+NJ +ddbEfs/eYh/xYj6N38X3K7eqj6ivm+vw1BcjK9xEj9F3LILlsXHsItbArmYr2B/ZnWwn240sV8Sr ++Fx+WGlQFijPqaNxT1Cb1Ou067UbTQe7q7tf7P5H93dGpnE9jUM8XIvV30b34Mm20i6ZKffRfqax +EBaGW2cpbCK7Cvc17CZ2H9vIHmHtmGU3288+Y1+zb9lxjsTITTyRp/B+uD18Ib+c38rv5rtw7+b/ +4j8osUo/xavkKAVKjTIfq1qhrMH9lPKhmqDuUg3YOVNbq63XNmqPac9rR0yh5t9byPLqj/efSD/x +QTd1r+xe293W3W58iHdGPGLKRW6cVsYhT9UhVy/FueRBxPkbLBS2S2DpbBQ7H5aZxuawBWwpLPkH +to49KNf+JHsWVnqbHcaa7dwl1zyE5/DRfCzui3k9X8DX8Ft4O3+LH1PMSogSrkQr6coYZapSryxS +rlDWKn7lVeV9Zb9yVPkRt6HaVLfaT01TveoYdZq6WL1H/VT9VJuivaJ9bLKZ5pmuN3WYvjIPN48y +V5rHmaeaV5u3mPdYahGdL9BT9HTvfc+6lGuVUuUpuplnqfH8Nf4a4nkazVQqOCKVb2Qr+e9YO++v +LTWN5CPZBXRETYOtX+Lr+VE+Uqlg5WwCzeHDAtpMUeqjKArUF+iQ+iye7TVoXmoKZdfww6ZQasNr +ewTm/KsyVPUqr9BeZR8zq/fSu6qNxbJD/GGlElHwnDpKq6YU5W56UlnAfkdP8VIcMY5bViGOL2CP +Ii9UsUz2vYKTJL8AUZSrfIRcNpf/kw5hH6+k29lMdRbdTFnsavqUHsKuGKhdZko3RbOX+Wy1hUey +duLqI3i6Eaw/U7Qo+gObqqwzHebv4G22S7XRB8rjWP0u/qRSoR7RxrMG7IDf0fW0wLiWrtCq1dfZ +LFLYJEpVu5DdrlYy1RSUy5BVpiCnbcHu3oY8UKRUoCUOkXM+4mIiMsQ63HcgT6iIoNnY4xcii71G +7aYq3kGztDCGrINM/Er3eJpsPER3GrPoMuMWGox8sMK4Gho30se0mjay5d1X4b2ZjJ3zATtfK+O7 +tDJjMG/h7/AJfO3p/oW1U1kcfY77SVRG4WzYor6N82ehscp4E9F9FjLsnTSdzqMDeMovMcM5Sidl +dV/AW40ypRHPu4/GGQ8bbmajBuNSnF+fpQfNGtWZvfCxn72O572K6vl4Y5FS3z0bdlgNK/hgrcXI +Pzf4iidWFfkKR51dMDJ/RF5uTnZW5rChGUMGD/KmDzxrQFpqf0+/FN2dnORKTIiPi42JjoqMcDrC +w+yhITarxWzSVIUzGlTqKavV/Wm1fjXNc845g0XdU4eGul4NtX4dTWWny/j1Wimmny7pg+QlfSR9 +AUnfSUnm0AuoYPAgvdSj+3eWePQONnlcNfibSjw1uv+Q5Cskv0bydvApKRigl8Y1lOh+VquX+suW +NLSU1pZAXWuIrdhTXG8bPIhabSFgQ8D5Yz2NrSx2FJMMjy3Nb8WJ145F+RM8JaX+eE+JWIFfSS2t +m+mvHFddWpKYklIzeJCfFc/wTPeTZ7Q/3CtFqFhO4zcV+81yGn22eBq6UW8d1NmyqsNB02u9oTM9 +M+umVPuVuhoxh9OLeUv8sVceiDtVhfKI4uoVvXsTlZbSuNm6qLa0rND9G8ZV9+5NEVhTAx0Yy1PL +alvKMPUqGLF8go7Z+PKaaj9bjil18STiqQLPV+8pFS21c3S/1TPa09AypxauSWjx0/grUtoSEnxb +jS5KKNVbqqo9Kf7CRE9NXYmrNYpaxl+xOd6nx5/eM3hQq8MZMGxrWHiQCbX3ZupP9klOiguufPxJ +yzKxIs+5CAi/PkPHSqo9eKY8AfV51DIjD2K4ahhG+WfCI7P91uLaFke+aBfj/Vqqw6O3fEuIAM+h +f53eUhdsMaU6viXBijg5GWro7+H9Xq8/PV2EiLkYPsUaR8l6zuBBSzq4x9Po0FHAfFQJ29bV5GfA +/CkpwsE3dvhoOir+5nHVgbpO0xPbyJfhrfHzWtHT2dMTPVH0NPf0nBxe60Ekt8sTdrTfknbyX7gj +JrK0Id/PYv4H3fWB/vIJnvJxk6v10pbaoG3Lq06rBfrzTvYFOX9kcbWSyIMcT1RkL4JyyklhUakO +9aup+GeSQT2zw2xBVMoWppf5HbXnBLDGlpLyKwd1GEfEKFmcGhZcpj/fe3p95Gn105YX2qJgwXhV +lldNbmmxndaHUAtMeG6wQMRTVXWKXuynidiZqfjXYXTmCapJ9PtgsmIhgPgLNAWrpwkmBvkaXCI6 +Bw8qQ6JraSnz6GUttS11HUbzdI/u8LRs5c/z51saS2t7AqfD2HZjor9sVQ1s1cDysSk4jW71sJXj +Wn1s5YTJ1VsdRPrKquo2znhx7eia1v7oq96qE/lkKxetolFUdFGhcoaHbOMWKZ+41UfULHtV2SDr +MzoYyTZLTxujGR080OboaeNoUwNtPtkmLpFjiquqe0eP3JI1g+ULT3yDqw6xWPr+IlJwPjmDX1DB +y2RS5WcU8SUFw0Ot1p/Raz5zvWazIj/NiK8wGG632frqVf879IaHhPQR0KC3r21+xWWxqCf1Yrgj +NLSPgNDb1za/4rJaVfFRVerFcKfd3kfApP0v6tV6640MC/sZvX1t8yuukBCT1Cu+RWF4tMPRR8Bs +OvVB8Ez0hprED8MevXEREX0ELCJKzlxvmN0s9YrQhwkSo6L6CFjhzfAz1xsebpZKhV4MT4qJ6SNg +E1Fy5nodDosMXBH6IvHExfURCIE3+9rmV1xOp+2kXidRSnz8f4/eiF56MTzV5eojEGoTnyXP+IqO +OqUXw9N1vY9AGIIs9sz1xsbaxQdb8U8MH+Lx9BEIR3/CmetNSAg7qRfDM9PS+gg40d/XNr/icrnC +5H4SWwrDhw8c2EcgAtGXfOZ6k5PD5X4SWwrD8wcN6iMQKaLvzPXquuOkXgwvzszsIxATiSg5c72p +qZFyo4mtiuHleXl9BOJFlJy53vT0aLkhnCSHTxg1qo+AS0TJmesdMiRObgixpTB8SmlpHwE3oiTr +zPVmZSWKP2PIvxFg+Mzy8j4CKYiSvrb5FVdeXhJ8I/+J4VupSjlrc1qce/ezykDqAnFlYJs3yb1V +GaAktY10+zoUz+aI6MzwosGKjrNLhkQdOB+0CbRdEX+DmaYki78HAZeBmkGbQNtBu0HInkDRq4Pm +g9aDukSPkqS42nS3o2iAEo+x8TiBhCuxdBhkgBRyAzNAY0HTQKtB60EmKSda5oOWgbaDjsgenxLb +dksW1h7bdqMsNs+5NFNW6wLVKVNldfOFNYGyYlygLDk3IJYfEBuWHWgeMjpQDhgUKCNSM5tFabNn +dhbFKDF4yBgsvBHI+IsUzhi5aYMSTX4QV0zBFp8Ssbl/Wub67YpKTOEKo5nkNjoV1mZ3ZhbZuMEP +I5Dc/Et+KNDDD20Oc2auLzqP76dNoO0ghe/H/SH/kJbxLmFzYCFoPWg7aBfoMMjEu3Dvw/0B/4DC ++fuUASoETQOtB20HHQaZ+ftAB39P/M6RKPhCEOfvAR38XTzWu8BwvhfcXr4XS3ujLXdE5lbJeDOC +jDs1yMQmBpmImMwO/nrbDwMRUWnwNCLqGaUfjaIspV9b6jB3hxLXVjDb3cE/2qx73RuKhvI95AeJ +o+gezLyHdFAlqBbUCDKBewvcW9QMWgPaAPKDEGVAB0jnO0Cvgt6ioSAfqBJk4bvbME0H39WWNtpd +FMNf43/D68HNd/K/y/JV/pIsX+F/leXLKJNR7uAvtSW7qSgE/YQxDpQOlBno1/hfNvePcBtFTr4d +tnMDM0CFoLGgaaDVIBPfzvu1zXRHQMkztAOvfzdvo89k+RDdZyHfHLcvrRgBqAtIyz8bHGC9vj6N ++9LW3omqgLSbbwEnIO0Pq8AJSLvyWnAC0i5dAk5A2sw54ASkTZ4GTkDa2CpwgA5+z9P9B7hzx85l +elE4vxxWuhxWuhxWupxUfrm46QdVrO2utvR0WGydzzsw3d28jTU/y5rHs+b7WHM9a76GNV/LmgtY +88Ws2cuaXaw5mTX7WPMzLA+maGa+9tOqI3xxrHkHa36CNTex5jTWnMqa+7NmneX6OnhK27lZsiiV +xeYiselQnj0K2Secp8CiKYj5FOSE7cBdIEPWfBDS+wWE45NF2W9zemGgPiQ/c37ROfwFDHwBbniB +9oFUOOgFhNELUPICFIQDC0HTQJ2gwyADZIJ0Pyx8tcRwYAaoEDQNtAx0GGSSyzkM4jQ/uMRNcmEZ +wUWPFTX+Am7xIT+Fp/iSHC6H13GOstrFwpPZ2GQjmeeSPFtGOC3ODmbf8p39++/sZC2y8pv5akqC +I9YEy9VtPyS5O9gdbWnPuIui2e2UrCLq2AhKY6ko86hJ1nPIZRFlNrn4Yygz21yTMCy8LW2QexsL +E6O2uH9wHXB/5urgYA+6nnG/rXeorM39Jloe2+Le47rB/XJGhwUtz6Z1MBTbdCm61ZXnfmKHFL0W +Heva3NeIYov7d64x7rku2VEf6Li4CTVfuHt82mT3OdBX4pru9jVB5xZ3oetid0FAKkeM2eIeiiV4 +A2w6FjvQJSf1JEuFE3M7WINvkHmtudo81jzcnGkeZE4xu81J5kRzlCXC4rCEWUItNovFYrKoFm4h +S1SH0eXzir+vR5nkf54Qv3EZqZJ3cJJ/Xpd/gufMwuk88kcq5bx8wmhW7u+cQeXTdf/RCZ4OZhs3 +2a95RjN/RDmVV43253nLO8zGeH+ut9xvrryoupWxm2vQ6ucr8Wu/qrqDGaJpeaL4vriVGHMuvylR +lGctv6mmhuJilhTGFUaMco4oK/kZqA2i99QVdxqf5F9bPqHa/2hSjT9TMEZSTbn/P8QHyK3sa3ak +tGQr+0oUNdVblVHs69Lxol0ZVVJTU97BJkk50tlXkEPEfCXlLHgxCznSLckBuXUBuVSMh1x/UUDO +aqVUKZdqtUo5lQm51qb+pSWt/ftLmVidmqRMU6zeW2ZHKmRSU6VMTDPtkDI7YpqFjH+UFHG5IJLs +kiIsgVxSxMUSpMikUyIZQZEbTorcIGdS2CkZV0DG3tUjY++CjPfXXvWjvV62eWTNjCni422tp7Qe +VOu/cUlDnL95uq63zqgJftVNq50+o0GUdfX+Gk99iX+Gp0RvHTnlZ7qniO6RnpJWnBOrqlun+OpL +2kb6RpZ66kpqNo+pzM49ba4bTs6VXfkzyiqFsmwx15jcn+nOFd1jxFy5Yq5cMdcY3xg5F8kYr6xu +tdDomuIpgXIzD7EhXmsTU2pGxzgaR8ngHZkSd03iNpxWNlKIt8Yf6hntt4NE1+CiwUWiC3tKdIWJ +L/TBrrhrRqYkbmMbg10ONDs9o8m7aHHTYoornV0S+NeEC02LFguDB9Db9EsX+kr9vrqSpkX4VeBP +n1DuLxw3ubrVbEZrrXgkf35PW0hIaYfRGWgcgsZ80agoJwVFW4Fos1qDgj/1/+JgWSx2QTN/ZjPz +JbNF1FSj+JPLqzhSQVXwU+g2nKXE66GpBg/YxLysqUdHcNleLwXqJJ65hxYtDnJBWywKloGRGNLU +Y5KTlzCW96TFFkGhyFwkvnto4gsQfkOnOFOcqQDxX4p+1JXOH30aHSdd7RRproh18Dl8HiQH+eIb +eaPCK1gF58xDPEFrhEC82nhTnPcCx4Gpjk8oo+LQsKG0gE2NzEmJLuIDWcdTTwkt2wAraCe0pPri +eAHZeME0mk/LaBOpG9C/Qb33jjiv4+jUqYeoECqycrKit+3cuVOM3YcFH9c68et5r6/EFhoaOnoi +SbSFhYSAl2iz2O3gJSo+uzN7rrqMr+Z3WtTHVWYlk8YVq8ZCOdthI7jPZ0vxZA8lJn6XIOG3Oxx8 +IpjPfc7wcHCu0FBgmN0uW4/44sPDTRPFdz2BdjswIVTz2cOzNaErTOjSmK75NK7Fh2xjBWw5BUyy +wIuHCnoClYKKEwVUWBg7gjlHDBvKptJULwt0pnicJpM5Z/jw3Cx+vL3ojarb92csUq8adbX7yTE7 +puGVU2YcVPZp2/BbNIlt911t46o91Z5tL7FrOVE5rgt5lW181ATXLD5Tq7fOiKp1dbr3aG9Gvh// +ceTHUYdjv4j/OKnLbbhj3G5vQkFMQUJ5QqN7jds8hPe3D4nJ5zn2cl5qL4s613WhbZJ9lv1j06cx +x9g3YQ4WrYSFOMIp0RVidpIt2qWExHUY37cLOwvmaeGIuCxhxa+flu5IdYb3CID5pl0IgPnaN0B0 +h6c6HLudzOH0OWudzU7V7QsJ4RPdPmFqZ4RwgxODAm5wmsLCgHGyT2gICQkxTXSGORwmUf+yXfjD +GZgswPhqxWzORREyDiJklETI+Ijob3aImtkherabd5n3mQ2z6jYX4lygmJPFKsxxwu3mZDGfOVTM +ZQ4Vms0JYiJzfHJ2pfDqNyfd6a04BOZEr902dUGBQ7Q5TngLDmCvFR4qLBDkHOGMgMdpKlswlRak +5Jg8/dLScrIjhmdlxsQ6s5wsKiYrc3hOdpqnn0nJq39x2ZuL5+y5rnZtxuYT+uOLlzy48aql915/ +z6rj969nSsu4Ih52rIxHvLrjLy/tffVF8f293DioJqujKBrRcY8v1k2uaD5RmapNtU4MqVfmavOt +9SGW6A7jQI+pDvjGCy7JJXBAxDvasaijCeqwiPz4Ya6iiIqEIte4iCnx4111EfMS6lxLTUujj/Kj +cQ6KYeH22NjKmNqYRvyEdYWvcWxwcIdDTXTZzLSNP0rM6JS7icmdIVzlYIzdFulSQ2KxlWQ4gPm6 +XXgFzPdbhENiffYO4712YXm78KxYFZjPpYvtQpV1QHq2387sCW7UNqemZYvy6WTsPDdzx3QYP/qm +CEUxWQ6LmMIhve6QceDob/b1T8/u8bWMCuFZoN7L7y7p9zDpd5f0eIz0Pvye28vvcLK3Qvj8ANoQ +A0cXiDYZCXD3ianoKDwUMSJjasGJBQUMbh8R0bPXcTpYsJDFmuB9cjooK5OcUeaUGOF6lpI2QDr/ +4m2Dvtz6WfdhFvXemyyM/XjQ1rZ8xqoTe/m40LxJN1z9CJsUe387czOFhbKzuj/o/sGhb9rWwG67 +vrjhIZEtIxEOzdobFMsG+pKjrCw8PiN+aDxSd/xdoXfbH7FbEuxn2f3xnfFqvDCrL8GdnWSxK6Hh +LhuL5t6oSFUxkW19FIsyIqUNI31qrGRipTFjpfliU1X8OLmFiX3fuXlYXrZMql6XO3sNsXif2L3x +Pjt2L0XJ1HqWTKv9xH6mQcG0iv0s02yUsLhIvu0iWsB80i7TbYdx7GmZce+Pi3+WbaMUOspsyKve +o703nNdb4PimwFEgd90h76GpyK8FBQUnsO1GiCRbfIUvyuE0Wc0mi4mbHNaIRHKawhPxyvWmX3st +82I/LsxyenKycrJzh2M7xpqFG6Kjs6I9zrb16yMTrlty/pTEvMzxJbt2KetWLZibXXZhxJ9sZbXT +V/14CXbeSvE/zZGX8SZlD/viuU2YSpFokmiWiO3wowx56mG0HkYVwZskOB4ibKtINEk0S8TgE3Lj +UA+j9TAYfMKXJDi8EMS7T6JJolminFm+HHsYrYeRM+cLzjpcbISx1jXWDVa/tdO6z3rEaiar29po +bbauDzZ1WQ2rzW1lxMwqXqcmRfh8sJz1GvxO0kyqzWRO1Uhdr25Q/Wqn2qWaOtUjKidVV3ejpqoi +WYuAAHPMFyvCQVVFEKg2Mb8qQ0HtCQUw3TITyHXaRCyoF1jGVMadeqci4S4skO9U+Nsrd5sgsd8W +LviFU7rXG4nThYKsu7K9vV39Yteu49Fq2vG9YvdcB8gV3mQVPntvX57mP5+zr7dO85DP2dcfp/nA +FyKdIC0uzg+bc/PkOWJzdk6gHDosUPZLDZwvUqNjs8M1t7Ze26epYwFHNMWtNWrNmqHhVzsOU0pq +YCPmBjdiNKJ5PbFOOiJ+rOq0m7pwtusxvthZviS5F6XxSRo/uA8twU0YsDwYQ+ZgOukCukA93QXC +B9iF0gvC8KLW98qCta9r17YdKxNvqxXds9UUvK0iKJmt9S0KdQx2nO0od6iFul/nbn1gqCcpMzoz +aXRSo75Gt+TH5ieeF3teYo3lotApsVMS51jmhs52zIudm9ipvxH1ftz7CW8kH4g6kNylG3qMR/U6 +vNE5ar6jTD3PMdnxccgXSd2OEGcYXlYuk5mZYlxhIRQW3/MGiu85osSL44NbeCa+/24bc9h8tlpb +s03V5QFFlwcUG3KTL0SkMltcsH6sXVjQJt5Zwnw2oU5kLzAHfTnCXrZFLDKLZwWPJIHDSOBgkkrU +ydgatoH52RGmulkhG4u8LtOB8A5ziEmYQ8zAHGIZTJ5KIHFUbhApGiOmY6FiKhYhfMbi3WNy41jv +N5bcJhUO8db65oDjxKnWwFsLJ5TCQ84RwRMKZGlBpDMrWqTDmJjoKC5OKwOcSq8zyooH8m9pWLl7 +zuJ9V01ePcT50JKljz28qKm1e7b2XMu4cauMO+7vPn7j+fknjisP7HzxlTdf2fF24C8JCnaWuDRF +YRxvjzjtXyGd9L3FIAtZjG6yktU4gZO+Tf5/5RBgKIUC7WQHhkkMpzCgg8KBTuCPiCMnMJIigFEU +CYwGHqcYigLGUjQwDniM4ikWfALFg0+kBKBLYhIlApPJZfxAbok6JQFTyA3sRzrQA/ye+lMKMJX6 +AdOA39EA8gDPov7AgZQGTJfopQHGURpEZwEHSxxC6cAM8gKH0mDgMOC3lElDgFmUAcymocY3lCNx +OA0D5lIWMI+yjf+kERLzKQc4UmIBDQeeTbnAUZQHLKQRxtfko3xgEY0EjqYCYDHwKyqhs4GlNApY +RoXGERpDPuA5VAQ8l0YDz5NYTsXA86kEWIHfH4fpAoljaQywks4BjqNzjS9pvMQJdB6wCmfRQzSR +KoCTJF5IFwCraazxL6qhSuBk4CG6iMaBn0ITgFOpCnixxGk00fiCamkSsI4uBE4Hfk4zqAY4kyYD +6+ki4CU0xfiMZklsoKnA2XSxcZDmUC34uRIvpTrgPJqO9stoBnC+xEaaaXxKC6geuJBmAZskLqIG +4xNaTLOBS2gO8HLgx7SU5gKvoHnAK+ky4FUSr6b5wN9RI/AaWmAcwI9agc3UBLyWFgF/T4sN8f9w +lwD/IHE5XW7sp+tpKXAFXQFcSVcCb6CrjA+pha4G3ki/Q8sq4Id0E10DvJmWAVfTtcA1wC76I/0e +eAtdB/wP+oOxj26VeBstB66lFcDbaSV67wDuozvpBuA6ajE+oLvoRuDdtAr4J4n30M3A9bQauIHW +AO8Fvk/30R+B99MtwAfoP4AP0q3Ge/QQ3Wa8Sw/TWuBGuh34iMRH6Q7gY3Qn8HG6C/iExCfpbuAm ++hPQT/cAW4F7qY3WAzfTBmA73We8Q0/R/cY/aYvEp+kBYAc9CNxKDwG3SXyGNgKfpUeMt+k5ehT4 +Z4nb6TFgJz0O/As9AXyengS+QJuMt+hF8gP/Sq3Gm/SSxL9RG/DvtNnYQy9TO3AHPQV8hbYAX6Wn +gTvx/ttDr9FW4C6Ju2kb8B/0LPB1es54g94Avk576M/AN2k78C3qNP5Bb0v8Jz0PfIdeAO6lF4Hv +SnyP/gp8n14CfkB/M3bTPold9LKxiz6kHcD99ArwI4kH6FXgx7QT+Am9BvyUdhuv0UGJn9E/gJ/T +68ZO+oLeAP5L4iHaA/yS3jJepcP0NvCIxK/on8Cv6R3gf9Je4DcSv6X3jFfoKL0P/I4+AH4P3EE/ +0D7gMeoCHqcPgT9KPEEfGS9TNx0AGvQx8N85/f98Tv/qN57Tv/jVOf2zX8jpn/0kpx/8hZz+6U9y ++ie/IqcfOJnTF56W0z/6hZz+kczpH/0kp++XOX1/r5y+X+b0/TKn7++V0z/8SU7vkjm9S+b0rt9g +Tn/n/1FO3/PvnP7vnP6by+m/9XP6bzen/9I5/d85/d85/edz+t9/+zn9vwCXmHoeCmVuZHN0cmVh +bQplbmRvYmoKOSAwIG9iago8PC9UeXBlIC9Gb250RGVzY3JpcHRvcgovRm9udE5hbWUgL0FBQUFB +QStBcmlhbE1UCi9GbGFncyA0Ci9Bc2NlbnQgOTA1LjI3MzQ0Ci9EZXNjZW50IC0yMTEuOTE0MDYK +L1N0ZW1WIDQ1Ljg5ODQzOAovQ2FwSGVpZ2h0IDcxNS44MjAzMQovSXRhbGljQW5nbGUgMAovRm9u +dEJCb3ggWy02NjQuNTUwNzggLTMyNC43MDcwMyAyMDAwIDEwMDUuODU5MzhdCi9Gb250RmlsZTIg +OCAwIFI+PgplbmRvYmoKMTAgMCBvYmoKPDwvVHlwZSAvRm9udAovRm9udERlc2NyaXB0b3IgOSAw +IFIKL0Jhc2VGb250IC9BQUFBQUErQXJpYWxNVAovU3VidHlwZSAvQ0lERm9udFR5cGUyCi9DSURU +b0dJRE1hcCAvSWRlbnRpdHkKL0NJRFN5c3RlbUluZm8gPDwvUmVnaXN0cnkgKEFkb2JlKQovT3Jk +ZXJpbmcgKElkZW50aXR5KQovU3VwcGxlbWVudCAwPj4KL1cgWzE2IFszMzMuMDA3ODEgMjc3Ljgz +MjAzXSAyMCA3MiA1NTYuMTUyMzQgNzMgWzI3Ny44MzIwM10gNzYgNzkgMjIyLjE2Nzk3IDgzIFs1 +NTYuMTUyMzRdXQovRFcgNzUwPj4KZW5kb2JqCjExIDAgb2JqCjw8L0ZpbHRlciAvRmxhdGVEZWNv +ZGUKL0xlbmd0aCAyNzE+PiBzdHJlYW0KeJxdkdtqxCAQhu99irncXizRbNplIQSWLAu56IGmfQCj +k1RojBhzkbevhzSFCiofM//vzJjVza3RykH2ZifRooNeaWlxnhYrEDoclCYsB6mE2yieYuSGZF7c +rrPDsdH9RMoSIHv30dnZFQ5XOXX4QLJXK9EqPcDhs249t4sx3ziidkBJVYHE3js9c/PCR4Qsyo6N +9HHl1qPX/GV8rAYhj8xSNWKSOBsu0HI9ICmpXxWUd78qglr+ixdJ1fXii9uQzQqfTemJVYGKOtLT +JdE9UR3p8RTpTKPv5pD/+u3PMxrTGItXfktO50jFJRkWm0UShSrDNPcRiMVa330ceWw7NKw07r9i +JhNUYf8AtiGI+QplbmRzdHJlYW0KZW5kb2JqCjQgMCBvYmoKPDwvVHlwZSAvRm9udAovU3VidHlw +ZSAvVHlwZTAKL0Jhc2VGb250IC9BQUFBQUErQXJpYWxNVAovRW5jb2RpbmcgL0lkZW50aXR5LUgK +L0Rlc2NlbmRhbnRGb250cyBbMTAgMCBSXQovVG9Vbmljb2RlIDExIDAgUj4+CmVuZG9iagp4cmVm +CjAgMTIKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDE1IDAwMDAwIG4gCjAwMDAwMDA0MDEg +MDAwMDAgbiAKMDAwMDAwMDEwMSAwMDAwMCBuIAowMDAwMDEwNDU5IDAwMDAwIG4gCjAwMDAwMDAx +MzggMDAwMDAgbiAKMDAwMDAwMDYwOSAwMDAwMCBuIAowMDAwMDAwNjY0IDAwMDAwIG4gCjAwMDAw +MDA3MTEgMDAwMDAgbiAKMDAwMDAwOTU4NSAwMDAwMCBuIAowMDAwMDA5ODE5IDAwMDAwIG4gCjAw +MDAwMTAxMTcgMDAwMDAgbiAKdHJhaWxlcgo8PC9TaXplIDEyCi9Sb290IDcgMCBSCi9JbmZvIDEg +MCBSPj4Kc3RhcnR4cmVmCjEwNTk4CiUlRU9GCg== + +----- From d4df579fbbe22bb5eca10b7bb3c0192b1f9a5bf7 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 20:03:00 +0100 Subject: [PATCH 181/203] Content fetching RFC standard support added #510 --- CHANGELOG.md | 1 + src/Client.php | 8 ++++++++ src/Connection/Protocols/ImapProtocol.php | 4 +++- src/Message.php | 2 +- src/Query/Query.php | 2 +- src/config/imap.php | 1 + 6 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c532bb97..94f94701 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Security configuration options added - Spoofing detection added #40 - RFC4315 MOVE fallback added #123 (thanks @freescout-help-desk) +- Content fetching RFC standard support added #510 (thanks @ybizeul) ### Breaking changes - NaN diff --git a/src/Client.php b/src/Client.php index a79511c0..0dd15a26 100755 --- a/src/Client.php +++ b/src/Client.php @@ -127,6 +127,13 @@ class Client { */ public array $extensions; + /** + * Account rfc. + * + * @var string + */ + public string $rfc; + /** * Account authentication method. * @@ -168,6 +175,7 @@ class Client { 'validate_cert' => true, 'username' => '', 'password' => '', + 'rfc' => 'RFC822', 'authentication' => null, "extensions" => [], 'proxy' => [ diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index da7483b8..17bc0e32 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -877,7 +877,9 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in * @throws RuntimeException */ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["$rfc.TEXT"], is_array($uids) ? $uids : [$uids], null, $uid); + $rfc = $rfc ?? "RFC822"; + $item = $rfc === "BODY" ? "BODY[TEXT]" : "$rfc.TEXT"; + return $this->fetch([$item], is_array($uids) ? $uids : [$uids], null, $uid); } /** diff --git a/src/Message.php b/src/Message.php index 66193436..7ca0097b 100755 --- a/src/Message.php +++ b/src/Message.php @@ -639,7 +639,7 @@ public function parseBody(): Message { $sequence_id = $this->getSequenceId(); try { - $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence)->validatedData(); + $contents = $this->client->getConnection()->content([$sequence_id], $this->client->rfc, $this->sequence)->validatedData(); } catch (Exceptions\RuntimeException $e) { throw new MessageContentFetchingException("failed to fetch content", 0, $e); } diff --git a/src/Query/Query.php b/src/Query/Query.php index cade729d..f1aea3ac 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -243,7 +243,7 @@ protected function fetch(Collection $available_messages): array { $contents = []; if ($this->getFetchBody()) { - $contents = $this->client->getConnection()->content($uids, "RFC822", $this->sequence)->validatedData(); + $contents = $this->client->getConnection()->content($uids, $this->client->rfc, $this->sequence)->validatedData(); } return [ diff --git a/src/config/imap.php b/src/config/imap.php index 8e39fe8e..407c53bd 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -81,6 +81,7 @@ 'username' => 'root@example.com', 'password' => '', 'authentication' => null, + 'rfc' => 'RFC822', // If you are using iCloud, you might want to set this to 'BODY' 'proxy' => [ 'socket' => null, 'request_fulluri' => false, From beb82e25a5f6cd0ab00afe9f56171f52b5b60abf Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 20:16:08 +0100 Subject: [PATCH 182/203] Support unescaped dates inside the search conditions #542 --- CHANGELOG.md | 1 + src/Query/Query.php | 5 ++++- src/config/imap.php | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94f94701..d664fe6f 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Spoofing detection added #40 - RFC4315 MOVE fallback added #123 (thanks @freescout-help-desk) - Content fetching RFC standard support added #510 (thanks @ybizeul) +- Support unescaped dates inside the search conditions #542 ### Breaking changes - NaN diff --git a/src/Query/Query.php b/src/Query/Query.php index f1aea3ac..e65a5a9d 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -164,7 +164,10 @@ public function generate_query(): string { if ($statement[1] === null) { $query .= $statement[0]; } else { - if (is_numeric($statement[1])) { + if (is_numeric($statement[1]) || ( + ($statement[0] === 'SINCE' || $statement[0] === 'BEFORE') && + $this->client->getConfig()->get('options.unescaped_search_dates', false) + )) { $query .= $statement[0] . ' ' . $statement[1]; } else { $query .= $statement[0] . ' "' . $statement[1] . '"'; diff --git a/src/config/imap.php b/src/config/imap.php index 407c53bd..abf8e6e6 100644 --- a/src/config/imap.php +++ b/src/config/imap.php @@ -174,6 +174,7 @@ 'soft_fail' => false, 'rfc822' => true, 'debug' => false, + 'unescaped_search_dates' => false, 'uid_cache' => true, // 'fallback_date' => "01.01.1970 00:00:00", 'boundary' => '/boundary=(.*?(?=;)|(.*))/i', From 4289d41f80822b0c800cfa2a2c42a9d5a4fa67b0 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 21:31:08 +0100 Subject: [PATCH 183/203] clone method looses account configuration #521 --- CHANGELOG.md | 1 + src/Client.php | 2 +- tests/ClientTest.php | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d664fe6f..b598ce50 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - RFC4315 MOVE fallback added #123 (thanks @freescout-help-desk) - Content fetching RFC standard support added #510 (thanks @ybizeul) - Support unescaped dates inside the search conditions #542 +- `Client::clone()` looses account configuration #521 (thanks @netpok) ### Breaking changes - NaN diff --git a/src/Client.php b/src/Client.php index 0dd15a26..441d0ab9 100755 --- a/src/Client.php +++ b/src/Client.php @@ -224,7 +224,7 @@ public function clone(): Client { $client->default_account_config = $this->default_account_config; $config = $this->getAccountConfig(); foreach($config as $key => $value) { - $client->setAccountConfig($key, $this->default_account_config); + $client->setAccountConfig($key, $config); } $client->default_message_mask = $this->default_message_mask; $client->default_attachment_mask = $this->default_message_mask; diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 315f3c42..73a01be7 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -83,6 +83,40 @@ public function testClient(): void { self::assertArrayHasKey("new", $this->client->getDefaultEvents("message")); } + /** + * @throws MaskNotFoundException + */ + public function testClientClone(): void { + $config = Config::make([ + "accounts" => [ + "default" => [ + 'host' => 'example.com', + 'port' => 993, + 'protocol' => 'imap', //might also use imap, [pop3 or nntp (untested)] + 'encryption' => 'ssl', // Supported: false, 'ssl', 'tls' + 'validate_cert' => true, + 'username' => 'root@example.com', + 'password' => 'foo', + 'authentication' => null, + 'rfc' => 'RFC822', // If you are using iCloud, you might want to set this to 'BODY' + 'proxy' => [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ], + "timeout" => 30, + "extensions" => [] + ]] + ]); + $client = new Client($config); + $clone = $client->clone(); + self::assertInstanceOf(Client::class, $clone); + self::assertSame($client->getConfig(), $clone->getConfig()); + self::assertSame($client->getAccountConfig(), $clone->getAccountConfig()); + self::assertSame($client->host, $clone->host); + } + public function testClientLogout(): void { $this->createNewProtocolMockup(); From e527bf3c8b7e8e73c21797b2ee8cea66bba03edf Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 22:20:42 +0100 Subject: [PATCH 184/203] Decode partially encoded address names #511 --- CHANGELOG.md | 1 + src/Decoder/HeaderDecoder.php | 4 +-- src/Header.php | 23 ++++++++++------ tests/issues/Issue511Test.php | 50 +++++++++++++++++++++++++++++++++++ tests/messages/issue-511.eml | 5 ++++ 5 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 tests/issues/Issue511Test.php create mode 100644 tests/messages/issue-511.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index b598ce50..7e02ee70 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Filename sanitization is now optional (enabled via default) - Address parsing improved and extended to include more cases - Boundary parsing fixed and improved to support more formats #544 +- Decode partially encoded address names #511 ### Added - Security configuration options added diff --git a/src/Decoder/HeaderDecoder.php b/src/Decoder/HeaderDecoder.php index c9aa33d0..3a98aa6c 100644 --- a/src/Decoder/HeaderDecoder.php +++ b/src/Decoder/HeaderDecoder.php @@ -68,11 +68,11 @@ public function getEncoding(object|string $structure): string { if (property_exists($structure, 'parameters')) { foreach ($structure->parameters as $parameter) { if (strtolower($parameter->attribute) == "charset") { - return EncodingAliases::get($parameter->value, $this->fallback_encoding); + return EncodingAliases::get($parameter->value == "default" ? EncodingAliases::detectEncoding($parameter->value) : $parameter->value, $this->fallback_encoding); } } } elseif (property_exists($structure, 'charset')) { - return EncodingAliases::get($structure->charset, $this->fallback_encoding); + return EncodingAliases::get($structure->charset == "default" ? EncodingAliases::detectEncoding($structure->charset) : $structure->charset, $this->fallback_encoding); } elseif (is_string($structure) === true) { $result = mb_detect_encoding($structure); return $result === false ? $this->fallback_encoding : $result; diff --git a/src/Header.php b/src/Header.php index 604eb896..3d909302 100644 --- a/src/Header.php +++ b/src/Header.php @@ -480,16 +480,23 @@ private function parseAddresses($list): array { if (!property_exists($address, 'personal')) { $address->personal = false; } else { - $personalParts = $this->decoder->mimeHeaderDecode($address->personal); - - $address->personal = ''; - foreach ($personalParts as $p) { - $address->personal .= $this->decoder->convertEncoding($p->text, $this->decoder->getEncoding($p)); - } + $personal_slices = explode(" ", $address->personal); + $address->personal = ""; + foreach ($personal_slices as $slice) { + $personalParts = $this->decoder->mimeHeaderDecode($slice); + + $personal = ''; + foreach ($personalParts as $p) { + $personal .= $this->decoder->convertEncoding($p->text, $this->decoder->getEncoding($p)); + } - if (str_starts_with($address->personal, "'")) { - $address->personal = str_replace("'", "", $address->personal); + if (str_starts_with($personal, "'")) { + $personal = str_replace("'", "", $personal); + } + $personal = $this->decoder->decode($personal); + $address->personal .= $personal . " "; } + $address->personal = trim(rtrim($address->personal)); } if ($address->host == ".SYNTAX-ERROR.") { diff --git a/tests/issues/Issue511Test.php b/tests/issues/Issue511Test.php new file mode 100644 index 00000000..4782bace --- /dev/null +++ b/tests/issues/Issue511Test.php @@ -0,0 +1,50 @@ +getFixture("issue-511.eml"); + self::assertSame("RE: [EXTERNAL] Re: Lorem Ipsum /40 one", (string)$message->subject); + self::assertSame("COMPANYNAME | usługi ", (string)$message->from->first()); + } +} \ No newline at end of file diff --git a/tests/messages/issue-511.eml b/tests/messages/issue-511.eml new file mode 100644 index 00000000..7cd61faa --- /dev/null +++ b/tests/messages/issue-511.eml @@ -0,0 +1,5 @@ +From: COMPANYNAME | =?iso-8859-2?q?us=B3ugi?= +To: receipent@receipent_domain.tld +Subject: =?utf-8?B?UkU6IFtFWFRFUk5BTF0gUmU6IExvcmVtIElwc3VtIC8=?= =?utf-8?Q?40_one?= + +Test message From 0276983f8104793a5676e1bf1aec151007cfc710 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 22:50:36 +0100 Subject: [PATCH 185/203] dev-master version bumped #493 #496 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4956e549..334275fa 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "6.0-dev" } }, "minimum-stability": "dev", From ec2dcb863a9713189ef1caf80ecf6b49af2256fe Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 17 Jan 2025 23:57:00 +0100 Subject: [PATCH 186/203] Compatibility table updated --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ec49921e..da47d048 100755 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Discord: [discord.gg/rd4cN9h6][link-discord] ## Compatibility | Version | PHP 5.6 | PHP 7 | PHP 8 | |:--------|:-------:|:-----:|:-----:| +| v6.x | / | / | X | | v5.x | / | / | X | | v4.x | / | X | X | | v3.x | / | X | / | From d21e00cf4158c23e087f6b7da2425bd8fd46755a Mon Sep 17 00:00:00 2001 From: webklex Date: Sun, 19 Jan 2025 21:17:05 +0100 Subject: [PATCH 187/203] Event dispatching simplified (thanks @stevebauman) --- src/Client.php | 6 ++---- src/Folder.php | 9 +++------ src/Message.php | 15 +++++---------- src/Traits/HasEvents.php | 9 +++++++++ 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Client.php b/src/Client.php index 441d0ab9..e8d971bc 100755 --- a/src/Client.php +++ b/src/Client.php @@ -723,8 +723,7 @@ public function createFolder(string $folder_path, bool $expunge = true, bool $ut $folder = $this->getFolderByPath($folder_path, true); if($status && $folder) { - $event = $this->getEvent("folder", "new"); - $event::dispatch($folder); + $this->dispatch("folder", "new", $folder); } return $folder; @@ -755,8 +754,7 @@ public function deleteFolder(string $folder_path, bool $expunge = true): array { $status = $this->getConnection()->deleteFolder($folder->path)->validatedData(); if ($expunge) $this->expunge(); - $event = $this->getEvent("folder", "deleted"); - $event::dispatch($folder); + $this->dispatch("folder", "deleted", $folder); return $status; } diff --git a/src/Folder.php b/src/Folder.php index dced7d6b..a6d8ee58 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -287,8 +287,7 @@ public function move(string $new_name, bool $expunge = true): array { if ($expunge) $this->client->expunge(); $folder = $this->client->getFolder($new_name); - $event = $this->getEvent("folder", "moved"); - $event::dispatch($this, $folder); + $this->dispatch("folder", "moved", $this, $folder); return $status; } @@ -383,8 +382,7 @@ public function delete(bool $expunge = true): array { if ($expunge) $this->client->expunge(); - $event = $this->getEvent("folder", "deleted"); - $event::dispatch($this); + $this->dispatch("folder", "deleted", $this); return $status; } @@ -494,8 +492,7 @@ public function idle(callable $callback, int $timeout = 300): void { $message->setSequence($sequence); $callback($message); - $event = $this->getEvent("message", "new"); - $event::dispatch($message); + $this->dispatch("message", "new", $message); } } } diff --git a/src/Message.php b/src/Message.php index 7ca0097b..3f4fa90b 100755 --- a/src/Message.php +++ b/src/Message.php @@ -1096,8 +1096,7 @@ protected function fetchNewMail(Folder $folder, int $next_uid, string $event, bo } $message = $folder->query()->getMessage($sequence_id, null, $this->sequence); - $event = $this->getEvent("message", $event); - $event::dispatch($this, $message); + $this->dispatch("message", $event, $this, $message); return $message; } @@ -1131,8 +1130,7 @@ public function delete(bool $expunge = true, ?string $trash_path = null, bool $f } if ($expunge) $this->client->expunge(); - $event = $this->getEvent("message", "deleted"); - $event::dispatch($this); + $this->dispatch("message", "deleted", $this); return $status; } @@ -1155,8 +1153,7 @@ public function restore(bool $expunge = true): bool { $status = $this->unsetFlag("Deleted"); if ($expunge) $this->client->expunge(); - $event = $this->getEvent("message", "restored"); - $event::dispatch($this); + $this->dispatch("message", "restored", $this); return $status; } @@ -1186,8 +1183,7 @@ public function setFlag(array|string $flag): bool { } $this->parseFlags(); - $event = $this->getEvent("flag", "new"); - $event::dispatch($this, $flag); + $this->dispatch("flag", "new", $this, $flag); return (bool)$status; } @@ -1218,8 +1214,7 @@ public function unsetFlag(array|string $flag): bool { } $this->parseFlags(); - $event = $this->getEvent("flag", "deleted"); - $event::dispatch($this, $flag); + $this->dispatch("flag", "deleted", $this, $flag); return (bool)$status; } diff --git a/src/Traits/HasEvents.php b/src/Traits/HasEvents.php index 3b6902ed..6dc382b7 100644 --- a/src/Traits/HasEvents.php +++ b/src/Traits/HasEvents.php @@ -74,4 +74,13 @@ public function getEvents(): array { return $this->events; } + /** + * Dispatch a specific event. + * @throws EventNotFoundException + */ + public function dispatch(string $section, string $event, mixed ...$args): void { + $event = $this->getEvent($section, $event); + $event::dispatch(...$args); + } + } \ No newline at end of file From dae454840765236c88fdf71ef9eb0321ee62e894 Mon Sep 17 00:00:00 2001 From: webklex Date: Sun, 19 Jan 2025 21:21:59 +0100 Subject: [PATCH 188/203] Enforce RFC822 parsing if enabled #462 --- CHANGELOG.md | 1 + src/Address.php | 18 ++++++++++ src/Header.php | 52 ++++++++++++++++++++++++++--- tests/fixtures/EmailAddressTest.php | 24 +++++++++++-- tests/fixtures/ReferencesTest.php | 6 ++-- tests/issues/Issue462Test.php | 48 ++++++++++++++++++++++++++ tests/messages/issue-462.eml | 5 +++ 7 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 tests/issues/Issue462Test.php create mode 100644 tests/messages/issue-462.eml diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e02ee70..51337ca8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Address parsing improved and extended to include more cases - Boundary parsing fixed and improved to support more formats #544 - Decode partially encoded address names #511 +- Enforce RFC822 parsing if enabled #462 ### Added - Security configuration options added diff --git a/src/Address.php b/src/Address.php index b45c72de..87c6479e 100644 --- a/src/Address.php +++ b/src/Address.php @@ -43,6 +43,24 @@ public function __construct(object $object) { if (property_exists($object, "host")){ $this->host = $object->host ?? ''; } if (property_exists($object, "mail")){ $this->mail = $object->mail ?? ''; } if (property_exists($object, "full")){ $this->full = $object->full ?? ''; } + $this->boot(); + } + + /** + * Boot the address + */ + private function boot(): void { + if($this->mail === "" && $this->mailbox !== "" && $this->host !== ""){ + $this->mail = $this->mailbox . "@" . $this->host; + }elseif($this->mail === "" && $this->mailbox !== ""){ + $this->mail = $this->mailbox; + } + + if($this->full === "" && $this->mail !== "" && $this->personal !== ""){ + $this->full = $this->personal . " <" . $this->mail . ">"; + }elseif($this->full === "" && $this->mail !== ""){ + $this->full = $this->mail; + } } diff --git a/src/Header.php b/src/Header.php index 3d909302..47c1668a 100644 --- a/src/Header.php +++ b/src/Header.php @@ -408,6 +408,24 @@ private function decodeAddresses($values): array { "mailbox" => $mailbox, "host" => $host, ]; + }elseif (preg_match( + '/^((?P.+)<)(?P[^<]+?)>$/', + $split_address, + $matches + )) { + $name = trim(rtrim($matches["name"])); + if(str_starts_with($name, "\"") && str_ends_with($name, "\"")) { + $name = substr($name, 1, -1); + }elseif(str_starts_with($name, "'") && str_ends_with($name, "'")) { + $name = substr($name, 1, -1); + } + $email = trim(rtrim($matches["email"])); + list($mailbox, $host) = array_pad(explode("@", $email), 2, null); + $addresses[] = (object)[ + "personal" => $name, + "mailbox" => $mailbox, + "host" => $host, + ]; } } } @@ -438,7 +456,6 @@ private function parseAddresses($list): array { if (is_array($list) === false) { if(is_string($list)) { - // $list = "" if (preg_match( '/^(?:(?P.+)\s)?(?(name)<|[^\s]+?)(?(name)>|>?)$/', $list, @@ -460,6 +477,32 @@ private function parseAddresses($list): array { "host" => $host, ] ]; + }elseif (preg_match( + '/^((?P.+)<)(?P[^<]+?)>$/', + $list, + $matches + )) { + $name = trim(rtrim($matches["name"])); + $email = trim(rtrim($matches["email"])); + if(str_starts_with($name, "\"") && str_ends_with($name, "\"")) { + $name = substr($name, 1, -1); + }elseif(str_starts_with($name, "'") && str_ends_with($name, "'")) { + $name = substr($name, 1, -1); + } + list($mailbox, $host) = array_pad(explode("@", $email), 2, null); + if($mailbox === ">") { // Fix trailing ">" in malformed mailboxes + $mailbox = ""; + } + if($name === "" && $mailbox === "" && $host === "") { + return $addresses; + } + $list = [ + (object)[ + "personal" => $name, + "mailbox" => $mailbox, + "host" => $host, + ] + ]; }else{ return $addresses; } @@ -501,14 +544,15 @@ private function parseAddresses($list): array { if ($address->host == ".SYNTAX-ERROR.") { $address->host = ""; + }elseif ($address->host == "UNKNOWN") { + $address->host = ""; } if ($address->mailbox == "UNEXPECTED_DATA_AFTER_ADDRESS") { $address->mailbox = ""; + }elseif ($address->mailbox == "MISSING_MAILBOX_TERMINATOR") { + $address->mailbox = ""; } - $address->mail = ($address->mailbox && $address->host) ? $address->mailbox . '@' . $address->host : false; - $address->full = ($address->personal) ? $address->personal . ' <' . $address->mail . '>' : $address->mail; - $addresses[] = new Address($address); } diff --git a/tests/fixtures/EmailAddressTest.php b/tests/fixtures/EmailAddressTest.php index d4e403d7..0f87cf6a 100644 --- a/tests/fixtures/EmailAddressTest.php +++ b/tests/fixtures/EmailAddressTest.php @@ -12,6 +12,16 @@ namespace Tests\fixtures; +use Webklex\PHPIMAP\Exceptions\AuthFailedException; +use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; +use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; +use Webklex\PHPIMAP\Exceptions\ImapServerErrorException; +use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException; +use Webklex\PHPIMAP\Exceptions\MaskNotFoundException; +use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException; +use Webklex\PHPIMAP\Exceptions\ResponseException; +use Webklex\PHPIMAP\Exceptions\RuntimeException; + /** * Class EmailAddressTest * @@ -23,6 +33,16 @@ class EmailAddressTest extends FixtureTestCase { * Test the fixture email_address.eml * * @return void + * @throws \ReflectionException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws RuntimeException */ public function testFixture() : void { $message = $this->getFixture("email_address.eml"); @@ -32,8 +52,8 @@ public function testFixture() : void { self::assertEquals("Hi\r\nHow are you?", $message->getTextBody()); self::assertFalse($message->hasHTMLBody()); self::assertFalse($message->date->first()); - self::assertEquals("no_host@UNKNOWN", (string)$message->from); + self::assertEquals("no_host", (string)$message->from); self::assertEquals("", $message->to); - self::assertEquals("This one: is \"right\" , No-address@UNKNOWN", $message->cc); + self::assertEquals("This one: is \"right\" , No-address", (string)$message->cc); } } \ No newline at end of file diff --git a/tests/fixtures/ReferencesTest.php b/tests/fixtures/ReferencesTest.php index 18473d61..c86aa96f 100644 --- a/tests/fixtures/ReferencesTest.php +++ b/tests/fixtures/ReferencesTest.php @@ -34,8 +34,8 @@ public function testFixture() : void { self::assertEquals("b9e87bd5e661a645ed6e3b832828fcc5@example.com", $message->in_reply_to); self::assertEquals("", $message->from->first()->personal); - self::assertEquals("UNKNOWN", $message->from->first()->host); - self::assertEquals("no_host@UNKNOWN", $message->from->first()->mail); + self::assertEquals("", $message->from->first()->host); + self::assertEquals("no_host", $message->from->first()->mail); self::assertFalse($message->to->first()); self::assertEquals([ @@ -45,7 +45,7 @@ public function testFixture() : void { self::assertEquals([ 'This one: is "right" ', - 'No-address@UNKNOWN' + 'No-address' ], $message->cc->map(function($address){ /** @var \Webklex\PHPIMAP\Address $address */ return $address->full; diff --git a/tests/issues/Issue462Test.php b/tests/issues/Issue462Test.php new file mode 100644 index 00000000..e7fc2126 --- /dev/null +++ b/tests/issues/Issue462Test.php @@ -0,0 +1,48 @@ +set('options.rfc822', false); + $message = $this->getFixture("issue-462.eml", $config); + self::assertSame("Undeliverable: Some subject", (string)$message->subject); + self::assertSame("postmaster@ ", (string)$message->from->first()); + } +} \ No newline at end of file diff --git a/tests/messages/issue-462.eml b/tests/messages/issue-462.eml new file mode 100644 index 00000000..d13b3cf1 --- /dev/null +++ b/tests/messages/issue-462.eml @@ -0,0 +1,5 @@ +From: "postmaster@" +To: receipent@receipent_domain.tld +Subject: Undeliverable: Some subject + +Test message From dedff6f679829d4aadfa0b17f150923b2f8dd1fe Mon Sep 17 00:00:00 2001 From: webklex Date: Sun, 19 Jan 2025 21:23:12 +0100 Subject: [PATCH 189/203] Release information added --- CHANGELOG.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51337ca8..205133e3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed +- NaN + +### Added +- NaN + +### Breaking changes +- NaN + +## [6.1.0] - 2025-01-19 +### Fixed - Filename sanitization is now optional (enabled via default) - Address parsing improved and extended to include more cases - Boundary parsing fixed and improved to support more formats #544 @@ -20,9 +30,6 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Support unescaped dates inside the search conditions #542 - `Client::clone()` looses account configuration #521 (thanks @netpok) -### Breaking changes -- NaN - ## [6.0.0] - 2025-01-17 ### Fixed - Fixed date issue if timezone is UT and a 2 digit year #429 (thanks @ferrisbuellers) From b9d8b1ba8751fb5fcb6fcb0f115feb5eda858cd6 Mon Sep 17 00:00:00 2001 From: Laurent Le Moine Date: Tue, 28 Jan 2025 13:03:00 +0100 Subject: [PATCH 190/203] SSL stream context options added #238 #546 Co-authored-by: LE MOINE Laurent --- src/Client.php | 14 ++++++++++- src/Connection/Protocols/Protocol.php | 36 +++++++++++++++++++++++++++ tests/ImapProtocolTest.php | 12 +++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index e8d971bc..19749a32 100755 --- a/src/Client.php +++ b/src/Client.php @@ -100,6 +100,16 @@ class Client { 'password' => null, ]; + + /** + * SSL stream context options + * + * @see https://www.php.net/manual/en/context.ssl.php for possible options + * + * @var array + */ + protected array $ssl_options = []; + /** * Connection timeout * @var int $timeout @@ -184,7 +194,8 @@ class Client { 'username' => null, 'password' => null, ], - "timeout" => 30 + 'ssl_options' => [], + "timeout" => 30, ]; /** @@ -436,6 +447,7 @@ public function connect(): Client { $this->connection = new ImapProtocol($this->config, $this->validate_cert, $this->encryption); $this->connection->setConnectionTimeout($this->timeout); $this->connection->setProxy($this->proxy); + $this->connection->setSslOptions($this->ssl_options); }else{ if (extension_loaded('imap') === false) { throw new ConnectionFailedException("connection setup failed", 0, new ProtocolNotSupportedException($protocol." is an unsupported protocol")); diff --git a/src/Connection/Protocols/Protocol.php b/src/Connection/Protocols/Protocol.php index a3886b55..a1404b4b 100644 --- a/src/Connection/Protocols/Protocol.php +++ b/src/Connection/Protocols/Protocol.php @@ -71,6 +71,15 @@ abstract class Protocol implements ProtocolInterface { 'password' => null, ]; + /** + * SSL stream context options + * + * @see https://www.php.net/manual/en/context.ssl.php for possible options + * + * @var array + */ + protected array $ssl_options = []; + /** * Cache for uid of active folder. * @@ -162,6 +171,28 @@ public function getProxy(): array { return $this->proxy; } + /** + * Set SSL context options settings + * @var array $options + * + * @return Protocol + */ + public function setSslOptions(array $options): Protocol + { + $this->ssl_options = $options; + + return $this; + } + + /** + * Get the current SSL context options settings + * + * @return array + */ + public function getSslOptions(): array { + return $this->ssl_options; + } + /** * Prepare socket options * @return array @@ -175,6 +206,11 @@ private function defaultSocketOptions(string $transport): array { 'verify_peer_name' => $this->getCertValidation(), 'verify_peer' => $this->getCertValidation(), ]; + + if (count($this->ssl_options)) { + /* Get the ssl context options from the config, but prioritize the 'validate_cert' config over the ssl context options */ + $options["ssl"] = array_replace($this->ssl_options, $options["ssl"]); + } } if ($this->proxy["socket"] != null) { diff --git a/tests/ImapProtocolTest.php b/tests/ImapProtocolTest.php index 78716548..30f3a200 100644 --- a/tests/ImapProtocolTest.php +++ b/tests/ImapProtocolTest.php @@ -48,5 +48,17 @@ public function testImapProtocol(): void { self::assertSame(true, $protocol->getCertValidation()); self::assertSame("ssl", $protocol->getEncryption()); + + $protocol->setSslOptions([ + 'verify_peer' => true, + 'cafile' => '/dummy/path/for/testing', + 'peer_fingerprint' => ['md5' => 40], + ]); + + self::assertSame([ + 'verify_peer' => true, + 'cafile' => '/dummy/path/for/testing', + 'peer_fingerprint' => ['md5' => 40], + ], $protocol->getSslOptions()); } } \ No newline at end of file From 35bad8556032e07aa046b0036e398b43eae044d7 Mon Sep 17 00:00:00 2001 From: webklex Date: Tue, 28 Jan 2025 13:03:30 +0100 Subject: [PATCH 191/203] Changelog updated --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 205133e3..bcf9a0f8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - NaN ### Added -- NaN +- SSL stream context options added #238 #546 (thanks @llemoine) ### Breaking changes - NaN From f8d5f34c3f854a9c10bcaba0bdeec29fdccc510c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= <39156286+zeddmaster@users.noreply.github.com> Date: Fri, 25 Apr 2025 08:49:46 +0300 Subject: [PATCH 192/203] When using the chunk function, some messages do not have an element with index 0 #552 #553 Co-authored-by: Dmitry Chizh --- src/Connection/Protocols/ImapProtocol.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 17bc0e32..455a5bae 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -770,6 +770,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in if (is_array($from) && count($from) > 1) { $set = implode(',', $from); } elseif (is_array($from) && count($from) === 1) { + $from = array_values($from); $set = $from[0] . ':' . $from[0]; } elseif ($to === null) { $set = $from . ':' . $from; From 88f28e4d766a52db8da42366a5364f13fcd8a9c2 Mon Sep 17 00:00:00 2001 From: Rene Date: Fri, 25 Apr 2025 07:52:13 +0200 Subject: [PATCH 193/203] Copy/Move Message with utf7 folder path (#559) Messages can be moved/copied to a folder with umlauts. Co-authored-by: Rene Rudnick --- src/Message.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Message.php b/src/Message.php index 3f4fa90b..9d15a7b8 100755 --- a/src/Message.php +++ b/src/Message.php @@ -983,6 +983,7 @@ protected function fetchThreadByMessageId(MessageCollection &$thread, string $me * Copy the current Messages to a mailbox * @param string $folder_path * @param boolean $expunge + * @param bool $utf7 * * @return null|Message * @throws AuthFailedException @@ -999,7 +1000,7 @@ protected function fetchThreadByMessageId(MessageCollection &$thread, string $me * @throws RuntimeException * @throws ResponseException */ - public function copy(string $folder_path, bool $expunge = false): ?Message { + public function copy(string $folder_path, bool $expunge = false, bool $utf7 = false): ?Message { $this->client->openFolder($folder_path); $status = $this->client->getConnection()->examineFolder($folder_path)->validatedData(); @@ -1010,7 +1011,7 @@ public function copy(string $folder_path, bool $expunge = false): ?Message { } /** @var Folder $folder */ - $folder = $this->client->getFolderByPath($folder_path); + $folder = $this->client->getFolderByPath($folder_path, $utf7); $this->client->openFolder($this->folder_path); if ($this->client->getConnection()->copyMessage($folder->path, $this->getSequenceId(), null, $this->sequence)->validatedData()) { @@ -1025,6 +1026,7 @@ public function copy(string $folder_path, bool $expunge = false): ?Message { * Move the current Messages to a mailbox * @param string $folder_path * @param boolean $expunge + * @param bool $utf7 * * @return Message|null * @throws AuthFailedException @@ -1041,7 +1043,7 @@ public function copy(string $folder_path, bool $expunge = false): ?Message { * @throws RuntimeException * @throws ResponseException */ - public function move(string $folder_path, bool $expunge = false): ?Message { + public function move(string $folder_path, bool $expunge = false, bool $utf7 = false): ?Message { $this->client->openFolder($folder_path); $status = $this->client->getConnection()->examineFolder($folder_path)->validatedData(); @@ -1052,7 +1054,7 @@ public function move(string $folder_path, bool $expunge = false): ?Message { } /** @var Folder $folder */ - $folder = $this->client->getFolderByPath($folder_path); + $folder = $this->client->getFolderByPath($folder_path, $utf7); $this->client->openFolder($this->folder_path); if ($this->client->getConnection()->moveMessage($folder->path, $this->getSequenceId(), null, $this->sequence)->validatedData()) { From d1e916b397233a7ea11f612ca1a0e9040c755f87 Mon Sep 17 00:00:00 2001 From: rskrzypczak Date: Fri, 25 Apr 2025 07:53:50 +0200 Subject: [PATCH 194/203] fix #560 Get folders list in hierarchical order (#561) * fix #552 Get folders list in hierarchical order * fix #560 Get folders list in hierarchical order --- src/Client.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index 19749a32..5eb26ff2 100755 --- a/src/Client.php +++ b/src/Client.php @@ -604,7 +604,7 @@ public function getFolders(bool $hierarchical = true, ?string $parent_folder = n $folder = new Folder($this, $folder_name, $item["delimiter"], $item["flags"]); if ($hierarchical && $folder->hasChildren()) { - $pattern = $folder->full_name.$folder->delimiter.'%'; + $pattern = $folder->path.$folder->delimiter.'%'; $children = $this->getFolders(true, $pattern, true); $folder->setChildren($children); @@ -650,7 +650,7 @@ public function getFoldersWithStatus(bool $hierarchical = true, ?string $parent_ $folder = new Folder($this, $folder_name, $item["delimiter"], $item["flags"]); if ($hierarchical && $folder->hasChildren()) { - $pattern = $folder->full_name.$folder->delimiter.'%'; + $pattern = $folder->path.$folder->delimiter.'%'; $children = $this->getFoldersWithStatus(true, $pattern, true); $folder->setChildren($children); From 0a0a968c208b786a461be7a75fae62d1a39b7852 Mon Sep 17 00:00:00 2001 From: Roberto Guido Date: Fri, 25 Apr 2025 07:55:08 +0200 Subject: [PATCH 195/203] Public search() method for Query (#565) --- src/Query/Query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Query/Query.php b/src/Query/Query.php index e65a5a9d..bed64a95 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -193,7 +193,7 @@ public function generate_query(): string { * @throws ImapServerErrorException * @throws ResponseException */ - protected function search(): Collection { + public function search(): Collection { $this->generate_query(); try { From 700485f710d1ff4f0630ecb15d010010ba9a023f Mon Sep 17 00:00:00 2001 From: Steffen Weber Date: Fri, 25 Apr 2025 07:56:39 +0200 Subject: [PATCH 196/203] Fix remaining implicit marking of parameters as nullable (PHP 8.4) (#566) --- src/Attachment.php | 2 +- src/Decoder/Decoder.php | 2 +- src/Decoder/DecoderInterface.php | 2 +- src/Decoder/HeaderDecoder.php | 2 +- src/Decoder/MessageDecoder.php | 2 +- tests/fixtures/FixtureTestCase.php | 2 +- tests/live/LegacyTest.php | 4 ++-- tests/live/LiveMailboxTestCase.php | 2 +- tests/live/QueryTest.php | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Attachment.php b/src/Attachment.php index 223de45e..e2ce8128 100755 --- a/src/Attachment.php +++ b/src/Attachment.php @@ -462,7 +462,7 @@ public function setConfig(Config $config): Attachment { * @return mixed * @throws MaskNotFoundException */ - public function mask(string $mask = null): mixed { + public function mask(?string $mask = null): mixed { $mask = $mask !== null ? $mask : $this->mask; if (class_exists($mask)) { return new $mask($this); diff --git a/src/Decoder/Decoder.php b/src/Decoder/Decoder.php index 1df68c5e..23b4dc42 100644 --- a/src/Decoder/Decoder.php +++ b/src/Decoder/Decoder.php @@ -54,7 +54,7 @@ public function __construct( * @param string|null $encoding * @return mixed */ - public function decode(array|string|null $value, string $encoding = null): mixed { + public function decode(array|string|null $value, ?string $encoding = null): mixed { return $value; } diff --git a/src/Decoder/DecoderInterface.php b/src/Decoder/DecoderInterface.php index c44f2118..e4874602 100644 --- a/src/Decoder/DecoderInterface.php +++ b/src/Decoder/DecoderInterface.php @@ -33,7 +33,7 @@ public function __construct(array $options = [], string $fallback_encoding = 'UT * @param string|null $encoding * @return string|array|null */ - public function decode(array|string|null $value, string $encoding = null): mixed; + public function decode(array|string|null $value, ?string $encoding = null): mixed; public function mimeHeaderDecode(string $text): array; diff --git a/src/Decoder/HeaderDecoder.php b/src/Decoder/HeaderDecoder.php index 3a98aa6c..9a8a71b2 100644 --- a/src/Decoder/HeaderDecoder.php +++ b/src/Decoder/HeaderDecoder.php @@ -21,7 +21,7 @@ */ class HeaderDecoder extends Decoder { - public function decode(array|string|null $value, string $encoding = null): mixed { + public function decode(array|string|null $value, ?string $encoding = null): mixed { if (is_array($value)) { return $this->decodeHeaderArray($value); } diff --git a/src/Decoder/MessageDecoder.php b/src/Decoder/MessageDecoder.php index 5d4178bc..4a2cff2f 100644 --- a/src/Decoder/MessageDecoder.php +++ b/src/Decoder/MessageDecoder.php @@ -23,7 +23,7 @@ */ class MessageDecoder extends Decoder { - public function decode(array|string|null $value, string $encoding = null): mixed { + public function decode(array|string|null $value, ?string $encoding = null): mixed { if(is_array($value)) { return array_map(function($item){ return $this->decode($item); diff --git a/tests/fixtures/FixtureTestCase.php b/tests/fixtures/FixtureTestCase.php index 00f31157..cb64a9a6 100644 --- a/tests/fixtures/FixtureTestCase.php +++ b/tests/fixtures/FixtureTestCase.php @@ -84,7 +84,7 @@ final public function __construct(?string $name = null, array $data = [], $dataN * @throws ResponseException * @throws RuntimeException */ - final public function getFixture(string $template, Config $config = null) : Message { + final public function getFixture(string $template, ?Config $config = null) : Message { $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", $template]); $message = Message::fromFile($filename, $config); self::assertInstanceOf(Message::class, $message); diff --git a/tests/live/LegacyTest.php b/tests/live/LegacyTest.php index 61e2ee09..3e385eb0 100644 --- a/tests/live/LegacyTest.php +++ b/tests/live/LegacyTest.php @@ -236,7 +236,7 @@ final protected function appendMessageTemplate(Folder $folder, string $template) * @throws ResponseException * @throws RuntimeException */ - final protected function deleteFolder(Folder $folder = null): bool { + final protected function deleteFolder(?Folder $folder = null): bool { $response = $folder?->delete(false); if (is_array($response)) { $valid_response = false; @@ -423,7 +423,7 @@ public function testQueryWhereCriteria(): void { * @throws ResponseException * @throws RuntimeException */ - protected function assertWhereSearchCriteria(Folder $folder, string $criteria, Carbon|string $value = null, bool $date = false): void { + protected function assertWhereSearchCriteria(Folder $folder, string $criteria, Carbon|string|null $value = null, bool $date = false): void { $query = $folder->query()->where($criteria, $value); self::assertInstanceOf(WhereQuery::class, $query); diff --git a/tests/live/LiveMailboxTestCase.php b/tests/live/LiveMailboxTestCase.php index c59e6773..d0f1b680 100644 --- a/tests/live/LiveMailboxTestCase.php +++ b/tests/live/LiveMailboxTestCase.php @@ -200,7 +200,7 @@ final protected function appendMessageTemplate(Folder $folder, string $template) * @throws ResponseException * @throws RuntimeException */ - final protected function deleteFolder(Folder $folder = null): bool { + final protected function deleteFolder(?Folder $folder = null): bool { $response = $folder?->delete(false); if (is_array($response)) { $valid_response = false; diff --git a/tests/live/QueryTest.php b/tests/live/QueryTest.php index 34cd77a4..194d804e 100644 --- a/tests/live/QueryTest.php +++ b/tests/live/QueryTest.php @@ -231,7 +231,7 @@ public function testQueryWhereCriteria(): void { * @throws ResponseException * @throws RuntimeException */ - protected function assertWhereSearchCriteria(Folder $folder, string $criteria, Carbon|string $value = null, bool $date = false): void { + protected function assertWhereSearchCriteria(Folder $folder, string $criteria, Carbon|string|null $value = null, bool $date = false): void { $query = $folder->query()->where($criteria, $value); self::assertInstanceOf(WhereQuery::class, $query); From fb10c94e4127ae9b02b8c96d64f82d41858d6170 Mon Sep 17 00:00:00 2001 From: smajti1 Date: Fri, 25 Apr 2025 07:57:39 +0200 Subject: [PATCH 197/203] Fix case sensitivity of folder attribute parsing (\NoSelect, \NoInferiors) #469 (#571) * Fix case sensitivity of folder attribute parsing (\NoSelect, \NoInferiors) #469 * changelog update --- CHANGELOG.md | 2 +- src/Folder.php | 4 ++-- tests/issues/Issue469Test.php | 42 +++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 tests/issues/Issue469Test.php diff --git a/CHANGELOG.md b/CHANGELOG.md index bcf9a0f8..7159d0af 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Fix case sensitivity of Folder attribute parsing (\NoSelect, \NoInferiors) #469 (thanks @smajti1) ### Added - SSL stream context options added #238 #546 (thanks @llemoine) diff --git a/src/Folder.php b/src/Folder.php index a6d8ee58..45d2931d 100755 --- a/src/Folder.php +++ b/src/Folder.php @@ -259,8 +259,8 @@ protected function getSimpleName($delimiter, $full_name): string|bool { * @param $attributes */ protected function parseAttributes($attributes): void { - $this->no_inferiors = in_array('\NoInferiors', $attributes); - $this->no_select = in_array('\NoSelect', $attributes); + $this->no_inferiors = in_array('\NoInferiors', $attributes, true) || \in_array('\Noinferiors', $attributes, true); + $this->no_select = in_array('\NoSelect', $attributes, true) || \in_array('\Noselect', $attributes, true); $this->marked = in_array('\Marked', $attributes); $this->referral = in_array('\Referral', $attributes); $this->has_children = in_array('\HasChildren', $attributes); diff --git a/tests/issues/Issue469Test.php b/tests/issues/Issue469Test.php new file mode 100644 index 00000000..13bd841b --- /dev/null +++ b/tests/issues/Issue469Test.php @@ -0,0 +1,42 @@ +createStub(Client::class); + $folder_name = '[Gmail]'; + $delimiter = '/'; + + $attributes = [ + '\NoInferiors', + '\NoSelect', + ]; + $folder = new Folder($client, $folder_name, $delimiter, $attributes); + + $attributes_lowercase = [ + '\Noinferiors', + '\Noselect', + ]; + $folder_lowercase = new Folder($client, $folder_name, $delimiter, $attributes_lowercase); + + self::assertSame( + $folder->no_inferiors, + $folder_lowercase->no_inferiors, + 'The parsed "\NoInferiors" attribute does not match the parsed "\Noinferiors" attribute' + ); + self::assertSame( + $folder->no_select, + $folder_lowercase->no_select, + 'The parsed "\NoSelect" attribute does not match the parsed "\Noselect" attribute' + ); + } +} \ No newline at end of file From 0e75d7e77407edd325e93918e47baa5fae534b5e Mon Sep 17 00:00:00 2001 From: pierement Date: Fri, 25 Apr 2025 07:58:33 +0200 Subject: [PATCH 198/203] Fix error on getUid(null) with 0 results (#499) (#573) Co-authored-by: Wiebe Haanstra --- src/Connection/Protocols/ImapProtocol.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 455a5bae..ed1dac6e 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -940,7 +940,7 @@ public function getUid(?int $id = null): Response { $uids = $this->uid_cache; if ($id == null) { - return Response::empty($this->debug)->setResult($uids); + return Response::empty($this->debug)->setResult($uids)->setCanBeEmpty(true); } foreach ($uids as $k => $v) { From c00d9db35fe0c6232d455625e823d9a744531bae Mon Sep 17 00:00:00 2001 From: lm-cmxkonzepte <45796141+lm-cmxkonzepte@users.noreply.github.com> Date: Fri, 25 Apr 2025 07:59:51 +0200 Subject: [PATCH 199/203] Fix Date parsing on non-standard format from Aqua Mail (#575) fixes issue #574 --- src/Header.php | 5 +++-- tests/fixtures/DateTemplateTest.php | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Header.php b/src/Header.php index 47c1668a..d18e98a1 100644 --- a/src/Header.php +++ b/src/Header.php @@ -673,6 +673,7 @@ private function read_attribute(string $raw_attribute): array { * | Thu, 31 May 2018 18:15:00 +0800 (added by) | Non-standard details added by the | Unknown * | | mail server | * | Sat, 31 Aug 2013 20:08:23 +0580 | Invalid timezone | PHPMailer bug https://sourceforge.net/p/phpmailer/mailman/message/6132703/ + * | Mi., 23 Apr. 2025 09:48:37 +0200 (MESZ) | Non-standard localized format | Aqua Mail S/MIME implementation * * Please report any new invalid timestamps to [#45](https://github.com/Webklex/php-imap/issues) * @@ -720,8 +721,8 @@ private function parseDate(object $header): void { case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}[\,]\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0: $date = str_replace(',', '', $date); break; - // match case for: Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)/Di., 15 Feb. 2022 06:52:44 +0100 (MEZ) - case preg_match('/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \([A-Z]{3,4}\))\/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \([A-Z]{3,4}\))+$/i', $date) > 0: + // match case for: Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)/Di., 15 Feb. 2022 06:52:44 +0100 (MEZ) and Mi., 23 Apr. 2025 09:48:37 +0200 (MESZ) + case preg_match('/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \([A-Z]{3,4}\))(\/([A-Z]{2,3}\.\,\ [0-9]{1,2}\ [A-Z]{2,3}\.\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4}\ \([A-Z]{3,4}\))+)?$/i', $date) > 0: $dates = explode('/', $date); $date = array_shift($dates); $array = explode(',', $date); diff --git a/tests/fixtures/DateTemplateTest.php b/tests/fixtures/DateTemplateTest.php index 9c43ae8b..77dfd7bb 100644 --- a/tests/fixtures/DateTemplateTest.php +++ b/tests/fixtures/DateTemplateTest.php @@ -51,6 +51,7 @@ class DateTemplateTest extends FixtureTestCase { "Thur, 16 Mar 2023 15:33:07 +0400" => "2023-03-16 11:33:07", "fr., 25 nov. 2022 06:27:14 +0100/fr., 25 nov. 2022 06:27:14 +0100" => "2022-11-25 05:27:14", "Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)/Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)" => "2022-02-15 05:52:44", + "Mi., 23 Apr. 2025 09:48:37 +0200 (MESZ)" => "2025-04-23 07:48:37", ]; /** From 6b8ef85d621bbbaf52741b00cca8e9237e2b2e05 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 25 Apr 2025 08:02:37 +0200 Subject: [PATCH 200/203] Release information added --- CHANGELOG.md | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7159d0af..97935023 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,28 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- Fix case sensitivity of Folder attribute parsing (\NoSelect, \NoInferiors) #469 (thanks @smajti1) +- NaN ### Added -- SSL stream context options added #238 #546 (thanks @llemoine) +- NaN ### Breaking changes - NaN +## [6.2.0] - 2025-04-25 +### Fixed +- When using the chunk function, some messages do not have an element with index 0 #552 #553 (thanks @zeddmaster) +- Get folders list in hierarchical order #560 #561 (thanks @rskrzypczak) +- Fix remaining implicit marking of parameters as nullable (PHP 8.4) #566 (thanks @steffenweber) +- Fix case sensitivity of folder attribute parsing (\NoSelect, \NoInferiors) #469 #571 (thanks @smajti1) +- Fix error on getUid(null) with 0 results (#499) #573 (thanks @pierement) +- Fix Date parsing on non-standard format from Aqua Mail #574 #575 (thanks @lm-cmxkonzepte) + +### Added +- SSL stream context options added #238 #546 (thanks @llemoine) +- Support copy/move Message with utf7 folder path #559 (thanks @loc4l) +- Public `Query::search()` method #565 (Thanks @madbob) + ## [6.1.0] - 2025-01-19 ### Fixed - Filename sanitization is now optional (enabled via default) @@ -65,12 +79,12 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - `Attachment::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$attachment->setOptions` instead. - `Header::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$header->setOptions` instead. - All protocol constructors now require a `Config::class` instance -- The `Client::class` constructor now require a `Config::class` instance -- The `Part::class` constructor now require a `Config::class` instance -- The `Header::class` constructor now require a `Config::class` instance -- The `Message::fromFile` method now requires a `Config::class` instance -- The `Message::fromString` method now requires a `Config::class` instance -- The `Message::boot` method now requires a `Config::class` instance +- The `Client::class` constructor now require a `Config::class` instance +- The `Part::class` constructor now require a `Config::class` instance +- The `Header::class` constructor now require a `Config::class` instance +- The `Message::fromFile` method now requires a `Config::class` instance +- The `Message::fromString` method now requires a `Config::class` instance +- The `Message::boot` method now requires a `Config::class` instance - The `Message::decode` method has been removed. Use `Message::getDecoder()->decode($str)` instead. - The `Message::getEncoding` method has been removed. Use `Message::getDecoder()->getEncoding($str)` instead. - The `Message::convertEncoding` method has been removed. Use `Message::getDecoder()->convertEncoding()` instead. @@ -366,7 +380,7 @@ If you have any questions, please feel welcome to join this issue: https://githu - Extend date parsing error message #173 - Fixed 'Where' method replaces the content with uppercase #148 - Don't surround numeric search values with quotes -- Context added to `InvalidWhereQueryCriteriaException` +- Context added to `InvalidWhereQueryCriteriaException` - Redundant `stream_set_timeout()` removed ### Added @@ -537,7 +551,7 @@ If you have any questions, please feel welcome to join this issue: https://githu - Alias `Message::removeFlag()` for `Message::unsetFlag()` added - Alias `Message::flags()` for `Message::getFlags()` added - New Exception `MessageFlagException::class` added -- New method `Message::setSequenceId($id)` added +- New method `Message::setSequenceId($id)` added - Optional Header attributizion option added ### Affected Classes @@ -549,7 +563,7 @@ If you have any questions, please feel welcome to join this issue: https://githu - [Attribute::class](src/Attribute.php) ### Breaking changes -- Stringified message headers are now separated by ", " instead of " ". +- Stringified message headers are now separated by ", " instead of " ". - All message header values such as subject, message_id, from, to, etc now consists of an `Àttribute::class` instance (should behave the same way as before, but might cause some problem in certain edge cases) - The formal address object "from", "to", etc now consists of an `Address::class` instance (should behave the same way as before, but might cause some problem in certain edge cases) - When fetching or manipulating message flags a `MessageFlagException::class` exception can be thrown if a runtime error occurs @@ -559,12 +573,12 @@ If you have any questions, please feel welcome to join this issue: https://githu ## [2.3.1] - 2020-12-30 ### Fixed -- Missing RFC attributes added +- Missing RFC attributes added - Set the message sequence when idling - Missing UID commands added #64 ### Added -- Get a message by its message number +- Get a message by its message number - Get a message by its uid #72 #66 #63 ### Affected Classes @@ -585,7 +599,7 @@ If you have any questions, please feel welcome to join this issue: https://githu - `Message::getTextBody()` fallback value fixed ### Added -- Proxy support added +- Proxy support added - Flexible disposition support added #58 - New `options.message_key` option `uid` added - Protocol UID support added @@ -919,7 +933,7 @@ If you have any questions, please feel welcome to join this issue: https://githu - Imap client timeout can be modified and read #186 - Decoder config options added #175 - Message search criteria "NOT" added #181 -- Invalid message date exception added +- Invalid message date exception added - Blade examples ### Breaking changes From 15d01425cf04e22b6523f40983c7fdbb682a85a2 Mon Sep 17 00:00:00 2001 From: webklex Date: Fri, 25 Apr 2025 08:28:02 +0200 Subject: [PATCH 201/203] List of alternatives updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da47d048..fa6cd7a1 100755 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ This library and especially the code flavor It's written in, is certainly not fo different approach, you might want to check out the following libraries: - [ddeboer/imap](https://github.com/ddeboer/imap) - [barbushin/php-imap](https://github.com/barbushin/php-imap) -- [stevebauman/php-imap](https://github.com/stevebauman/php-imap) +- [DirectoryTree/ImapEngine](https://github.com/DirectoryTree/ImapEngine) ## Change log From 56f3b6b3c6c2c0dfcc07f0a2a2b0f322299fcbf3 Mon Sep 17 00:00:00 2001 From: wurst-hans <56444979+wurst-hans@users.noreply.github.com> Date: Wed, 7 May 2025 18:23:47 +0200 Subject: [PATCH 202/203] Reading whole lines from stream (#576) Replaced char-by-char retrieval from stream via fread() with fgets() to get full line from stream until EOF or newline. This speeds up data transfer by factor 10. --- src/Connection/Protocols/ImapProtocol.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index ed1dac6e..6871d57f 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -135,14 +135,10 @@ protected function enableStartTls(): void { * @throws RuntimeException */ public function nextLine(Response $response): string { - $line = ""; - while (($next_char = fread($this->stream, 1)) !== false && !in_array($next_char, ["", "\n"])) { - $line .= $next_char; - } - if ($line === "" && ($next_char === false || $next_char === "")) { + $line = fgets($this->stream); + if ($line === false || $line === '') { throw new RuntimeException('empty response'); } - $line .= "\n"; $response->addResponse($line); if ($this->debug) echo "<< " . $line; return $line; From 5656386a3597dacf93f8887d2016ed3cea2adbb7 Mon Sep 17 00:00:00 2001 From: webklex Date: Wed, 7 May 2025 18:27:06 +0200 Subject: [PATCH 203/203] Changelog updated --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97935023..438ee606 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip ## [UNRELEASED] ### Fixed -- NaN +- Reading whole lines from stream #576 (thanks @wurst-hans) ### Added - NaN