From dae454840765236c88fdf71ef9eb0321ee62e894 Mon Sep 17 00:00:00 2001 From: webklex Date: Sun, 19 Jan 2025 21:21:59 +0100 Subject: [PATCH] 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