From c9205fbad0b0e846c6f8e3e3da8158f7c6c2ab91 Mon Sep 17 00:00:00 2001 From: James King Date: Thu, 1 Oct 2026 13:57:03 +0100 Subject: [PATCH] Add URL lookup by link label in a given email Closes #43 --- README.md | 30 +++++++++++++++++ src/Module/MailCatcher.php | 11 ++++++- tests/unit/Module/MailCatcherTest.php | 47 +++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9f1787d..c819257 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,36 @@ For `Second link`, this returns * Param $label +### grabUrlForLinkFromEmail + +Returns the `href` of the first link whose text exactly matches the label in +the given email's HTML body. Matching is case-sensitive. Nested tags and HTML +entities are decoded. Whitespace in the label is preserved. + +Fails if the email has no HTML body or no matching link with an `href`. + +Example: + +```php +$email = $I->lastMessageTo('user@example.com'); +$url = $I->grabUrlForLinkFromEmail($email, 'Second link'); +``` + +To use the last email: + +```php +$url = $I->grabUrlForLinkFromEmail($I->lastMessage(), 'Second link'); +``` + +For `Second link`, this returns +`http://second-link.com`. + +Use an `Email` object from `lastMessage()`, `lastMessageTo()`, or +`lastMessageFrom()`, or one you created directly. + +* Param $email +* Param $label + ### lastMessageFrom Grab the full email object sent to an address. diff --git a/src/Module/MailCatcher.php b/src/Module/MailCatcher.php index bc22c51..eaf612d 100644 --- a/src/Module/MailCatcher.php +++ b/src/Module/MailCatcher.php @@ -392,7 +392,16 @@ public function grabUrlsFromNthEmail(int $nth): array */ public function grabUrlForLinkFromLastEmail(string $label): string { - $message = Message::from($this->lastMessage()->getSource(), false); + return $this->grabUrlForLinkFromEmail($this->lastMessage(), $label); + } + + /** + * Return the URL of the first link with an exact, case-sensitive label + * in the given email's HTML body. Fail if no matching link has an href. + */ + public function grabUrlForLinkFromEmail(Email $email, string $label): string + { + $message = Message::from($email->getSource(), false); $html = $message->getHtmlContent(); if ($html !== null && $html !== '') { diff --git a/tests/unit/Module/MailCatcherTest.php b/tests/unit/Module/MailCatcherTest.php index 0f720fd..8655f02 100644 --- a/tests/unit/Module/MailCatcherTest.php +++ b/tests/unit/Module/MailCatcherTest.php @@ -222,19 +222,48 @@ public function testGrabUrlForLinkFromLastEmail() $this->assertSame('http://second-link.com', $mailcatcher->grabUrlForLinkFromLastEmail('Second link')); } + public function testGrabUrlForLinkFromEmail() + { + $mailcatcher = new MailCatcherTest_TestClass(); + $email = new Email(1, [], '', + "Content-Type: text/html; charset=UTF-8\r\n\r\n" + . 'First link' + . 'Second link' + ); + + $this->assertSame('http://second-link.com', $mailcatcher->grabUrlForLinkFromEmail($email, 'Second link')); + } + + public function testGrabUrlForLinkFromGivenEmail() + { + $mailcatcher = new MailCatcherTest_TestClass(); + $mailcatcher->setLastMessage(new Email(2, [], '', + "Content-Type: text/html; charset=UTF-8\r\n\r\n" + . 'Reset password' + )); + $email = new Email(1, ['user@example.com'], '', + "Content-Type: text/html; charset=UTF-8\r\n\r\n" + . 'Reset password' + ); + + $this->assertSame('https://example.com/selected', $mailcatcher->grabUrlForLinkFromEmail($email, 'Reset password')); + } + public function testGrabUrlForLinkFromEncodedHtmlEmail() { $html = 'Café & "tea"' . 'Café & "tea"'; $mailcatcher = new MailCatcherTest_TestClass(); - $mailcatcher->setLastMessage(new Email(1, [], '', + $email = new Email(1, [], '', "MIME-Version: 1.0\r\nContent-Type: multipart/alternative; boundary=parts\r\n\r\n" . "--parts\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nPlain text\r\n" . "--parts\r\nContent-Type: text/html; charset=UTF-8\r\n" . "Content-Transfer-Encoding: quoted-printable\r\n\r\n" . quoted_printable_encode($html) . "\r\n--parts--\r\n" - )); + ); + $this->assertSame('https://example.com/first?one=1&two=2', $mailcatcher->grabUrlForLinkFromEmail($email, 'Café & "tea"')); + $mailcatcher->setLastMessage($email); $this->assertSame('https://example.com/first?one=1&two=2', $mailcatcher->grabUrlForLinkFromLastEmail('Café & "tea"')); } @@ -252,6 +281,20 @@ public function testGrabUrlForLinkFromLastEmailFailsWhenNoLinkMatches(string $so $mailcatcher->grabUrlForLinkFromLastEmail('Second link'); } + /** + * @dataProvider missingLinkEmails + */ + public function testGrabUrlForLinkFromEmailFailsWhenNoLinkMatches(string $source) + { + $mailcatcher = new MailCatcherTest_TestClass(); + $email = new Email(1, [], '', $source); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('No link found with label: Second link'); + + $mailcatcher->grabUrlForLinkFromEmail($email, 'Second link'); + } + public static function missingLinkEmails(): array { return [