From bc38f1cfb07b0671a87cbb19326acf033e11fd3d Mon Sep 17 00:00:00 2001 From: dhairyasquad73 Date: Wed, 29 Oct 2025 19:43:06 +0530 Subject: [PATCH 1/4] fix(imap): use message header date for timestamps Signed-off-by: dhairyasquad73 --- lib/IMAP/ImapMessageFetcher.php | 27 +++++- tests/Unit/IMAP/ImapMessageFetcherTest.php | 107 +++++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/IMAP/ImapMessageFetcherTest.php diff --git a/lib/IMAP/ImapMessageFetcher.php b/lib/IMAP/ImapMessageFetcher.php index e9e6bba5e9..1111eb8276 100644 --- a/lib/IMAP/ImapMessageFetcher.php +++ b/lib/IMAP/ImapMessageFetcher.php @@ -12,6 +12,7 @@ use Horde_Imap_Client_Base; use Horde_Imap_Client_Data_Envelope; use Horde_Imap_Client_Data_Fetch; +use Horde_Imap_Client_DateTime; use Horde_Imap_Client_Exception; use Horde_Imap_Client_Exception_NoSupportExtension; use Horde_Imap_Client_Fetch_Query; @@ -63,6 +64,7 @@ class ImapMessageFetcher { private bool $isOneClickUnsubscribe = false; private ?string $unsubscribeMailto = null; private bool $isPgpMimeEncrypted = false; + private ?Horde_Imap_Client_DateTime $messageDate = null; public function __construct( int $uid, @@ -263,7 +265,7 @@ public function fetchMessage(?Horde_Imap_Client_Data_Fetch $fetch = null): IMAPM $this->inlineAttachments, $this->hasAnyAttachment, $this->scheduling, - $fetch->getImapDate(), + $this->messageDate ?? $fetch->getImapDate(), $this->rawReferences, $this->dispositionNotificationTo, $this->hasDkimSignature, @@ -531,6 +533,8 @@ private function parseHeaders(Horde_Imap_Client_Data_Fetch $fetch): void { $dkimSignatureHeader = $parsedHeaders->getHeader('dkim-signature'); $this->hasDkimSignature = $dkimSignatureHeader !== null; + $this->messageDate = $this->resolveMessageDate($fetch, $parsedHeaders); + if ($this->runPhishingCheck) { $this->phishingDetails = $this->phishingDetectionService->checkHeadersForPhishing($parsedHeaders, $this->hasHtmlMessage, $this->htmlMessage); } @@ -560,4 +564,25 @@ private function parseHeaders(Horde_Imap_Client_Data_Fetch $fetch): void { } } } + + private function resolveMessageDate(Horde_Imap_Client_Data_Fetch $fetch, Horde_Mime_Headers $parsedHeaders): Horde_Imap_Client_DateTime { + $dateHeader = $parsedHeaders->getHeader('Date'); + if ($dateHeader !== null) { + $dateValue = $dateHeader->value ?? null; + if (!empty($dateValue)) { + try { + return new Horde_Imap_Client_DateTime($dateValue); + } catch (\Throwable $e) { + // Ignore invalid header value and fall back to the internal date + } + } + } + + $internalDate = $fetch->getImapDate(); + if ($internalDate instanceof Horde_Imap_Client_DateTime) { + return $internalDate; + } + + return new Horde_Imap_Client_DateTime('now'); + } } diff --git a/tests/Unit/IMAP/ImapMessageFetcherTest.php b/tests/Unit/IMAP/ImapMessageFetcherTest.php new file mode 100644 index 0000000000..a7bd45a26d --- /dev/null +++ b/tests/Unit/IMAP/ImapMessageFetcherTest.php @@ -0,0 +1,107 @@ +htmlService = $this->createMock(Html::class); + $this->smimeService = $this->createMock(SmimeService::class); + $this->converter = $this->createMock(Converter::class); + $this->phishingDetectionService = $this->createMock(PhishingDetectionService::class); + $this->client = $this->createMock(Horde_Imap_Client_Base::class); + + $this->fetcher = new ImapMessageFetcher( + 42, + 'INBOX', + $this->client, + 'user', + $this->htmlService, + $this->smimeService, + $this->converter, + $this->phishingDetectionService, + ); + } + + private function invokeResolveMessageDate(Horde_Imap_Client_Data_Fetch $fetch, Horde_Mime_Headers $headers): Horde_Imap_Client_DateTime { + $method = new ReflectionMethod(ImapMessageFetcher::class, 'resolveMessageDate'); + $method->setAccessible(true); + /** @var Horde_Imap_Client_DateTime $result */ + $result = $method->invoke($this->fetcher, $fetch, $headers); + return $result; + } + + public function testResolveMessageDatePrefersHeader(): void { + $fetch = $this->createMock(Horde_Imap_Client_Data_Fetch::class); + $fetch->method('getImapDate') + ->willReturn(new Horde_Imap_Client_DateTime('2025-10-20 10:00:00 +0000')); + $headers = Horde_Mime_Headers::parseHeaders("Date: Mon, 01 Jan 2001 12:00:00 +0000\r\n"); + + $result = $this->invokeResolveMessageDate($fetch, $headers); + + self::assertSame('2001-01-01T12:00:00+00:00', $result->format('c')); + } + + public function testResolveMessageDateFallsBackToInternalWithoutHeader(): void { + $internal = new Horde_Imap_Client_DateTime('2025-10-20 10:00:00 +0000'); + $fetch = $this->createMock(Horde_Imap_Client_Data_Fetch::class); + $fetch->method('getImapDate')->willReturn($internal); + $headers = Horde_Mime_Headers::parseHeaders(''); + + $result = $this->invokeResolveMessageDate($fetch, $headers); + + self::assertSame($internal->format('c'), $result->format('c')); + } + + public function testResolveMessageDateFallsBackToInternalOnInvalidHeader(): void { + $internal = new Horde_Imap_Client_DateTime('2025-10-20 10:00:00 +0000'); + $fetch = $this->createMock(Horde_Imap_Client_Data_Fetch::class); + $fetch->method('getImapDate')->willReturn($internal); + $headers = Horde_Mime_Headers::parseHeaders("Date: not-a-valid-date\r\n"); + + $result = $this->invokeResolveMessageDate($fetch, $headers); + + self::assertSame($internal->format('c'), $result->format('c')); + } + + public function testResolveMessageDateFallsBackToNowWhenNoDateAvailable(): void { + $fetch = $this->createMock(Horde_Imap_Client_Data_Fetch::class); + $fetch->method('getImapDate')->willReturn(null); + $headers = Horde_Mime_Headers::parseHeaders(''); + + $before = time(); + $result = $this->invokeResolveMessageDate($fetch, $headers); + $after = time(); + + self::assertGreaterThanOrEqual($before, $result->getTimestamp()); + self::assertLessThanOrEqual($after, $result->getTimestamp()); + } +} From a01527f0d09683de25779084e543beeaf342cd14 Mon Sep 17 00:00:00 2001 From: dhairya Date: Fri, 31 Oct 2025 21:55:58 +0530 Subject: [PATCH 2/4] Update lib/IMAP/ImapMessageFetcher.php Co-authored-by: Daniel Signed-off-by: dhairya --- lib/IMAP/ImapMessageFetcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/IMAP/ImapMessageFetcher.php b/lib/IMAP/ImapMessageFetcher.php index 1111eb8276..6eb5e1be9f 100644 --- a/lib/IMAP/ImapMessageFetcher.php +++ b/lib/IMAP/ImapMessageFetcher.php @@ -583,6 +583,6 @@ private function resolveMessageDate(Horde_Imap_Client_Data_Fetch $fetch, Horde_M return $internalDate; } - return new Horde_Imap_Client_DateTime('now'); + return new Horde_Imap_Client_DateTime(); } } From 3a7a6e7bc678b930812920f159d32c787300f2c8 Mon Sep 17 00:00:00 2001 From: dhairya Date: Fri, 31 Oct 2025 21:56:05 +0530 Subject: [PATCH 3/4] Update lib/IMAP/ImapMessageFetcher.php Co-authored-by: Daniel Signed-off-by: dhairya --- lib/IMAP/ImapMessageFetcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/IMAP/ImapMessageFetcher.php b/lib/IMAP/ImapMessageFetcher.php index 6eb5e1be9f..263cae4227 100644 --- a/lib/IMAP/ImapMessageFetcher.php +++ b/lib/IMAP/ImapMessageFetcher.php @@ -567,7 +567,7 @@ private function parseHeaders(Horde_Imap_Client_Data_Fetch $fetch): void { private function resolveMessageDate(Horde_Imap_Client_Data_Fetch $fetch, Horde_Mime_Headers $parsedHeaders): Horde_Imap_Client_DateTime { $dateHeader = $parsedHeaders->getHeader('Date'); - if ($dateHeader !== null) { + if ($dateHeader instanceof \Horde_Mime_Headers_Date) { $dateValue = $dateHeader->value ?? null; if (!empty($dateValue)) { try { From 21697ee72ca14b505b38adeb067ec1e6bd42676c Mon Sep 17 00:00:00 2001 From: dhairyasquad73 Date: Fri, 31 Oct 2025 22:07:12 +0530 Subject: [PATCH 4/4] fix: Handle invalid date headers in ImapMessageFetcher --- lib/IMAP/ImapMessageFetcher.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/IMAP/ImapMessageFetcher.php b/lib/IMAP/ImapMessageFetcher.php index 1111eb8276..9ae8a9727c 100644 --- a/lib/IMAP/ImapMessageFetcher.php +++ b/lib/IMAP/ImapMessageFetcher.php @@ -571,8 +571,11 @@ private function resolveMessageDate(Horde_Imap_Client_Data_Fetch $fetch, Horde_M $dateValue = $dateHeader->value ?? null; if (!empty($dateValue)) { try { - return new Horde_Imap_Client_DateTime($dateValue); - } catch (\Throwable $e) { + $date = new Horde_Imap_Client_DateTime($dateValue); + if ($date->getTimestamp() > 0) { + return $date; + } + } catch (\Throwable) { // Ignore invalid header value and fall back to the internal date } }