From b59877c95511fce368013be4727e378210098771 Mon Sep 17 00:00:00 2001 From: Wolfgang Klinger Date: Thu, 1 Oct 2026 19:14:27 +0200 Subject: [PATCH 1/2] Add regression test for grabbing URLs from single-part emails mail-mime-parser returns null for a missing text or HTML part, which used to be passed to preg_match_all() and raised a deprecation notice. The null guard landed with #130; this test keeps it from regressing. Resolves: #138 --- tests/unit/Module/MailCatcherTest.php | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/unit/Module/MailCatcherTest.php b/tests/unit/Module/MailCatcherTest.php index 8655f02..7530e93 100644 --- a/tests/unit/Module/MailCatcherTest.php +++ b/tests/unit/Module/MailCatcherTest.php @@ -482,6 +482,28 @@ public function testSeeInLastEmailRecipientFail() $mailcatcher->seeInLastEmailRecipient('userB@example.com'); } + public function testGrabUrlsFromSinglePartEmailsRaisesNoDeprecation() + { + $mailcatcher = new MailCatcherTest_TestClass(); + $sourceDirectory = dirname(__DIR__, 3) . '/src/'; + set_error_handler(static function (int $severity, string $message, string $file, int $line) use ($sourceDirectory): bool { + if (!str_starts_with($file, $sourceDirectory)) { + return false; + } + throw new \ErrorException($message, 0, $severity, $file, $line); + }); + + try { + $mailcatcher->setLastMessage(new Email(1, [], '', $this->mimeSource('text/plain', 'Visit https://example.com/a'))); + $this->assertEquals(['https://example.com/a'], $mailcatcher->grabUrlsFromLastEmail()); + + $mailcatcher->setLastMessage(new Email(1, [], '', $this->mimeSource('text/html', 'B'))); + $this->assertEquals(['https://example.com/b'], $mailcatcher->grabUrlsFromLastEmail()); + } finally { + restore_error_handler(); + } + } + private function mailcatcherWithMessages(array $messages, Email ...$emails): MailCatcherTest_TestClass { $mailcatcher = new MailCatcherTest_TestClass(); @@ -506,6 +528,17 @@ private function message( 'recipients' => $recipients, ]; } + + private function mimeSource(string $contentType, string $body): string + { + return "From: sender@example.com\r\n" + . "To: user@example.com\r\n" + . "Subject: Test\r\n" + . "MIME-Version: 1.0\r\n" + . "Content-Type: {$contentType}; charset=UTF-8\r\n" + . "\r\n" + . $body; + } } class MailCatcherTest_TestClass extends MailCatcher From 45df7150b918b52b42912cdae740d0a6ff88583e Mon Sep 17 00:00:00 2001 From: Wolfgang Klinger Date: Thu, 1 Oct 2026 11:24:42 +0200 Subject: [PATCH 2/2] Decode HTML entities in URLs grabbed from the HTML part The URL regex runs over raw HTML markup, so an href written as "?id=1&token=abc" came back with the literal "&" and the second query parameter was lost when the URL was opened. Resolves: #137 --- src/Module/MailCatcher.php | 7 ++++++- tests/acceptance/MailcatcherCest.php | 8 ++++++++ tests/unit/Module/MailCatcherTest.php | 11 +++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Module/MailCatcher.php b/src/Module/MailCatcher.php index eaf612d..d18d7d2 100644 --- a/src/Module/MailCatcher.php +++ b/src/Module/MailCatcher.php @@ -610,7 +610,12 @@ protected function grabUrlsFromEmail(Email $email): array preg_match_all($regex, $message->getTextContent() ?? '', $textMatches); preg_match_all($regex, $message->getHtmlContent() ?? '', $htmlMatches); - return array_merge($textMatches[0], $htmlMatches[0]); + $htmlUrls = array_map( + static fn (string $url): string => html_entity_decode($url, ENT_QUOTES | ENT_HTML5), + $htmlMatches[0] + ); + + return array_merge($textMatches[0], $htmlUrls); } /** diff --git a/tests/acceptance/MailcatcherCest.php b/tests/acceptance/MailcatcherCest.php index aec5cf9..36c1a01 100644 --- a/tests/acceptance/MailcatcherCest.php +++ b/tests/acceptance/MailcatcherCest.php @@ -165,6 +165,14 @@ public function test_grab_urls_from_html_email( $I->assertEquals($url, $urls[0]); } + public function test_grab_urls_from_html_email_with_multiple_query_parameters(AcceptanceTester $I) + { + $I->sendEmail("user@example.com", 'Html email with urls', "Confirm", true); + $urls = $I->grabUrlsFromLastEmail(); + + $I->assertEquals(['https://example.com/confirm?id=1&token=abc'], $urls); + } + /** * @param AcceptanceTester $I * @param \Codeception\Example $example diff --git a/tests/unit/Module/MailCatcherTest.php b/tests/unit/Module/MailCatcherTest.php index 7530e93..e7f06bc 100644 --- a/tests/unit/Module/MailCatcherTest.php +++ b/tests/unit/Module/MailCatcherTest.php @@ -504,6 +504,17 @@ public function testGrabUrlsFromSinglePartEmailsRaisesNoDeprecation() } } + public function testGrabUrlsFromLastEmailDecodesHtmlEntities() + { + $mailcatcher = new MailCatcherTest_TestClass(); + $mailcatcher->setLastMessage(new Email(1, [], '', $this->mimeSource( + 'text/html', + 'Confirm' + ))); + + $this->assertEquals(['https://example.com/confirm?id=1&token=abc'], $mailcatcher->grabUrlsFromLastEmail()); + } + private function mailcatcherWithMessages(array $messages, Email ...$emails): MailCatcherTest_TestClass { $mailcatcher = new MailCatcherTest_TestClass();