diff --git a/src/Connection/Protocols/ImapProtocol.php b/src/Connection/Protocols/ImapProtocol.php index 6871d57f..b2d248b4 100644 --- a/src/Connection/Protocols/ImapProtocol.php +++ b/src/Connection/Protocols/ImapProtocol.php @@ -815,9 +815,23 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in // if we only want one item we return that one directly if (count($items) == 1) { - if ($tokens[2][0] == $items[0]) { + $expectedItem = (string) $items[0]; + $itemMatches = static function($actual, $expected): bool { + $actualNorm = strtoupper((string) $actual); + $expectedNorm = strtoupper((string) $expected); + if ($actualNorm === $expectedNorm) { + return true; + } + // IMAP may answer BODY[...] even when request used BODY.PEEK[...]. + if (str_replace('.PEEK', '', $actualNorm) === str_replace('.PEEK', '', $expectedNorm)) { + return true; + } + return false; + }; + + if ($itemMatches($tokens[2][0] ?? '', $expectedItem)) { $data = $tokens[2][1]; - } elseif ($uid === IMAP::ST_UID && $tokens[2][2] == $items[0]) { + } elseif ($uid === IMAP::ST_UID && $itemMatches($tokens[2][2] ?? '', $expectedItem)) { $data = $tokens[2][3]; } else { $expectedResponse = 0; @@ -825,7 +839,7 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in $count = count($tokens[2]); // we start with 2, because 0 was already checked for ($i = 2; $i < $count; $i += 2) { - if ($tokens[2][$i] != $items[0]) { + if (!$itemMatches($tokens[2][$i] ?? '', $expectedItem)) { continue; } $data = $tokens[2][$i + 1]; @@ -869,13 +883,19 @@ public function fetch(array|string $items, array|int $from, mixed $to = null, in * @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 * message numbers instead. + * @param bool $peek true to fetch body using PEEK (do not set \Seen) * * @return Response * @throws RuntimeException */ - public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { + public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID, bool $peek = false): Response { $rfc = $rfc ?? "RFC822"; - $item = $rfc === "BODY" ? "BODY[TEXT]" : "$rfc.TEXT"; + if ($peek && ($rfc === "RFC822" || $rfc === "BODY")) { + // BODY.PEEK keeps the message unread at protocol level. + $item = "BODY.PEEK[TEXT]"; + } else { + $item = $rfc === "BODY" ? "BODY[TEXT]" : "$rfc.TEXT"; + } return $this->fetch([$item], is_array($uids) ? $uids : [$uids], null, $uid); } diff --git a/src/Connection/Protocols/LegacyProtocol.php b/src/Connection/Protocols/LegacyProtocol.php index 81b52f77..9d077958 100644 --- a/src/Connection/Protocols/LegacyProtocol.php +++ b/src/Connection/Protocols/LegacyProtocol.php @@ -252,18 +252,23 @@ public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', * @param int|array $uids * @param string $rfc * @param int|string $uid set to IMAP::ST_UID if you pass message unique identifiers instead of numbers. + * @param bool $peek true to fetch body without setting \Seen * * @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) { + public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID, bool $peek = false): Response { + return $this->response()->wrap(function($response) use ($uids, $uid, $peek) { /** @var Response $response */ $result = []; $uids = is_array($uids) ? $uids : [$uids]; + $fetchOptions = ($uid === IMAP::ST_UID ? IMAP::ST_UID : IMAP::NIL); + if ($peek) { + $fetchOptions |= IMAP::FT_PEEK; + } foreach ($uids as $id) { $response->addCommand("imap_fetchbody"); - $result[$id] = \imap_fetchbody($this->stream, $id, "", $uid === IMAP::ST_UID ? IMAP::ST_UID : IMAP::NIL); + $result[$id] = \imap_fetchbody($this->stream, $id, "", $fetchOptions); } return $result; diff --git a/src/Connection/Protocols/ProtocolInterface.php b/src/Connection/Protocols/ProtocolInterface.php index eb6d7c9d..6679f215 100644 --- a/src/Connection/Protocols/ProtocolInterface.php +++ b/src/Connection/Protocols/ProtocolInterface.php @@ -134,11 +134,12 @@ public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', * @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 * message numbers instead. + * @param bool $peek true to fetch body using a non-seen variant when supported * * @return Response * @throws RuntimeException */ - public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response; + public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID, bool $peek = false): Response; /** * Fetch message headers diff --git a/src/Message.php b/src/Message.php index 9d15a7b8..ea909cce 100755 --- a/src/Message.php +++ b/src/Message.php @@ -638,8 +638,9 @@ public function parseBody(): Message { $this->client->openFolder($this->folder_path); $sequence_id = $this->getSequenceId(); + $usePeek = ($this->fetch_options === IMAP::FT_PEEK); try { - $contents = $this->client->getConnection()->content([$sequence_id], $this->client->rfc, $this->sequence)->validatedData(); + $contents = $this->client->getConnection()->content([$sequence_id], $this->client->rfc, $this->sequence, $usePeek)->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 bed64a95..ce22e9ce 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -246,7 +246,8 @@ protected function fetch(Collection $available_messages): array { $contents = []; if ($this->getFetchBody()) { - $contents = $this->client->getConnection()->content($uids, $this->client->rfc, $this->sequence)->validatedData(); + $usePeek = ($this->getFetchOptions() === IMAP::FT_PEEK); + $contents = $this->client->getConnection()->content($uids, $this->client->rfc, $this->sequence, $usePeek)->validatedData(); } return [