From d43b8329f233448f7eaf82621aa78eaa7bd94ccd Mon Sep 17 00:00:00 2001 From: Arnt Gulbrandsen Date: Mon, 23 Sep 2024 13:45:08 +0200 Subject: [PATCH 1/7] Add new accessors to help determine whether to use the SMTPUTF8 extension --- src/Symfony/Component/Mailer/Envelope.php | 13 +++++++++++ .../Component/Mailer/Tests/EnvelopeTest.php | 13 +++++++++++ src/Symfony/Component/Mime/Address.php | 23 +++++++++++++++++++ .../Component/Mime/Tests/AddressTest.php | 7 ++++++ 4 files changed, 56 insertions(+) diff --git a/src/Symfony/Component/Mailer/Envelope.php b/src/Symfony/Component/Mailer/Envelope.php index 45e1db36397b6..1df8a04648b6f 100644 --- a/src/Symfony/Component/Mailer/Envelope.php +++ b/src/Symfony/Component/Mailer/Envelope.php @@ -85,4 +85,17 @@ public function getRecipients(): array { return $this->recipients; } + + /** + * @return bool + */ + public function anyAddressHasUnicodeLocalpart(): bool + { + if($this->sender->hasUnicodeLocalpart()) + return true; + foreach($this->recipients as $r) + if($r->hasUnicodeLocalpart()) + return true; + return false; + } } diff --git a/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php b/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php index de4eb0e0810a2..dca493a185fa8 100644 --- a/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php +++ b/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php @@ -127,6 +127,19 @@ public function testRecipientsFromHeaders() $this->assertEquals([new Address('to@symfony.com'), new Address('cc@symfony.com'), new Address('bcc@symfony.com')], $e->getRecipients()); } + public function testUnicodeLocalparts() + { + /* dømi means example and is reserved by the .fo registry */ + $i = new Address('info@dømi.fo'); + $d = new Address('dømi@dømi.fo'); + $e = new Envelope($i, [$i]); + $this->assertFalse($e->anyAddressHasUnicodeLocalpart()); + $e = new Envelope($i, [$d]); + $this->assertTrue($e->anyAddressHasUnicodeLocalpart()); + $e = new Envelope($i, [$i, $d]); + $this->assertTrue($e->anyAddressHasUnicodeLocalpart()); + } + public function testRecipientsFromHeadersWithNames() { $headers = new Headers(); diff --git a/src/Symfony/Component/Mime/Address.php b/src/Symfony/Component/Mime/Address.php index a78b64321d7fb..4f5e4421e4bac 100644 --- a/src/Symfony/Component/Mime/Address.php +++ b/src/Symfony/Component/Mime/Address.php @@ -117,4 +117,27 @@ public static function createArray(array $addresses): array return $addrs; } + + /** + + * Returns true if this address' localpart contains at least one + * non-ASCII character, and false if it is only ASCII (or empty). + * + * This is a helper for Envelope, which has to decide whether to + * the SMTPUTF8 extensions (RFC 6530 and following) for any given + * message. + * + * The SMTPUTF8 extension is strictly required if any address + * contains a non-ASCII character in its localpart. If non-ASCII + * is only used in domains (e.g. horst@freiherr-von-mühlhausen.de) + * then it is possible to to send the message using IDN encoding + * instead of SMTPUTF8. The most common software will display the + * message as intended. + * + * @return bool + */ + public function hasUnicodeLocalpart(): bool + { + return (bool) preg_match( '/[\x80-\xFF].*@/', $this->address); + } } diff --git a/src/Symfony/Component/Mime/Tests/AddressTest.php b/src/Symfony/Component/Mime/Tests/AddressTest.php index baef170efc4f6..e769b4b78df67 100644 --- a/src/Symfony/Component/Mime/Tests/AddressTest.php +++ b/src/Symfony/Component/Mime/Tests/AddressTest.php @@ -81,6 +81,13 @@ public function testCreateArray() $this->assertEquals([$fabien], Address::createArray(['fabien@symfony.com'])); } + public function testUnicodeLocalpart() + { + /* dømi means example and is reserved by the .fo registry */ + $this->assertFalse((new Address('info@dømi.fo'))->hasUnicodeLocalpart()); + $this->assertTrue((new Address('info@dømi.fo'))->hasUnicodeLocalpart()); + } + public function testCreateArrayWrongArg() { $this->expectException(\TypeError::class); From 6f07a17bbf50267b47f6165e1701b94fbde85831 Mon Sep 17 00:00:00 2001 From: Arnt Gulbrandsen Date: Mon, 23 Sep 2024 13:51:07 +0200 Subject: [PATCH 2/7] Send SMTPUTF8 if the message needs it and the server supports it. Before this commit, Envelope would throw InvalidArgumentException when a unicode sender address was used. Now, that error is thrown slightly later, is thrown for recipient addresses as well, but is not thrown if the next-hop server supports SMTPUTF8. As a side effect, transports that use JSON APIs to ESPs can also use unicode addresses if the ESP supports that (many do, many don't). --- src/Symfony/Component/Mailer/CHANGELOG.md | 1 + src/Symfony/Component/Mailer/Envelope.php | 23 +++++++--- .../Component/Mailer/Tests/EnvelopeTest.php | 15 ------ .../Transport/Smtp/EsmtpTransportTest.php | 46 +++++++++++++++++++ .../Mailer/Transport/Smtp/EsmtpTransport.php | 5 ++ .../Mailer/Transport/Smtp/SmtpTransport.php | 15 ++++-- 6 files changed, 81 insertions(+), 24 deletions(-) diff --git a/src/Symfony/Component/Mailer/CHANGELOG.md b/src/Symfony/Component/Mailer/CHANGELOG.md index 01fb57558f366..3fc83d62cccd1 100644 --- a/src/Symfony/Component/Mailer/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Make `TransportFactoryTestCase` compatible with PHPUnit 10+ + * Support unicode email addresses such as "dømi@dømi.fo", no client changes needed 7.1 --- diff --git a/src/Symfony/Component/Mailer/Envelope.php b/src/Symfony/Component/Mailer/Envelope.php index 1df8a04648b6f..3ce2a30227a50 100644 --- a/src/Symfony/Component/Mailer/Envelope.php +++ b/src/Symfony/Component/Mailer/Envelope.php @@ -44,10 +44,6 @@ public static function create(RawMessage $message): self public function setSender(Address $sender): void { - // to ensure deliverability of bounce emails independent of UTF-8 capabilities of SMTP servers - if (!preg_match('/^[^@\x80-\xFF]++@/', $sender->getAddress())) { - throw new InvalidArgumentException(\sprintf('Invalid sender "%s": non-ASCII characters not supported in local-part of email.', $sender->getAddress())); - } $this->sender = $sender; } @@ -87,13 +83,28 @@ public function getRecipients(): array } /** + + * Returns true if any address' localpart contains at least one + * non-ASCII character, and false if all addresses have all-ASCII + * localparts. + * + * This helps to decide whether to the SMTPUTF8 extensions (RFC + * 6530 and following) for any given message. + * + * The SMTPUTF8 extension is strictly required if any address + * contains a non-ASCII character in its localpart. If non-ASCII + * is only used in domains (e.g. horst@freiherr-von-mühlhausen.de) + * then it is possible to to send the message using IDN encoding + * instead of SMTPUTF8. The most common software will display the + * message as intended. + * * @return bool */ public function anyAddressHasUnicodeLocalpart(): bool { - if($this->sender->hasUnicodeLocalpart()) + if($this->getSender()->hasUnicodeLocalpart()) return true; - foreach($this->recipients as $r) + foreach($this->getRecipients() as $r) if($r->hasUnicodeLocalpart()) return true; return false; diff --git a/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php b/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php index dca493a185fa8..4c372b1514e9e 100644 --- a/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php +++ b/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php @@ -29,13 +29,6 @@ public function testConstructorWithAddressSender() $this->assertEquals(new Address('fabien@symfony.com'), $e->getSender()); } - public function testConstructorWithAddressSenderAndNonAsciiCharactersInLocalPartOfAddress() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid sender "fabièn@symfony.com": non-ASCII characters not supported in local-part of email.'); - new Envelope(new Address('fabièn@symfony.com'), [new Address('thomas@symfony.com')]); - } - public function testConstructorWithNamedAddressSender() { $e = new Envelope($sender = new Address('fabien@symfony.com', 'Fabien'), [new Address('thomas@symfony.com')]); @@ -81,14 +74,6 @@ public function testSenderFromHeaders() $this->assertEquals($from, $e->getSender()); } - public function testSenderFromHeadersFailsWithNonAsciiCharactersInLocalPart() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid sender "fabièn@symfony.com": non-ASCII characters not supported in local-part of email.'); - $message = new Message(new Headers(new PathHeader('Return-Path', new Address('fabièn@symfony.com')))); - Envelope::create($message)->getSender(); - } - public function testSenderFromHeadersWithoutFrom() { $headers = new Headers(); diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php index 977d2a05e5981..9c6ef09634a60 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Mailer\Tests\Transport\Smtp; use PHPUnit\Framework\TestCase; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Transport\Smtp\Auth\CramMd5Authenticator; use Symfony\Component\Mailer\Transport\Smtp\Auth\LoginAuthenticator; @@ -62,6 +63,37 @@ public function testExtensibility() $this->assertContains("RCPT TO: NOTIFY=FAILURE\r\n", $stream->getCommands()); } + public function testSmtputf8() + { + $stream = new DummyStream(); + $transport = new Smtputf8EsmtpTransport(stream: $stream); + + $message = new Email(); + $message->from('info@dømi.fo'); + $message->addTo('dømi@dømi.fo'); + $message->text('.'); + + $transport->send($message); + + $this->assertContains("MAIL FROM: SMTPUTF8\r\n", $stream->getCommands()); + $this->assertContains("RCPT TO:\r\n", $stream->getCommands()); + } + + public function testMissingSmtputf8() + { + $stream = new DummyStream(); + $transport = new EsmtpTransport(stream: $stream); + + $message = new Email(); + $message->from('info@dømi.fo'); + $message->addTo('dømi@dømi.fo'); + $message->text('.'); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid addresses: non-ASCII characters not supported in local-part of email.'); + $transport->send($message); + } + public function testConstructorWithDefaultAuthenticators() { $stream = new DummyStream(); @@ -270,3 +302,17 @@ public function executeCommand(string $command, array $codes): string return $response; } } + +class Smtputf8EsmtpTransport extends EsmtpTransport +{ + public function executeCommand(string $command, array $codes): string + { + $response = parent::executeCommand($command, $codes); + + if (str_starts_with($command, 'EHLO ')) { + $response .= "250 SMTPUTF8\r\n"; + } + + return $response; + } +} diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php index e3e12443be4a7..b9604652c634d 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php @@ -195,6 +195,11 @@ private function parseCapabilities(string $ehloResponse): array return $capabilities; } + protected function serverSupportsSmtputf8(): bool + { + return \array_key_exists('SMTPUTF8', $this->capabilities); + } + private function handleAuth(array $modes): void { if (!$this->username) { diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php index 432394dad5fef..73bb56cc36395 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php @@ -14,6 +14,7 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; @@ -211,7 +212,7 @@ protected function doSend(SentMessage $message): void try { $envelope = $message->getEnvelope(); - $this->doMailFromCommand($envelope->getSender()->getEncodedAddress()); + $this->doMailFromCommand($envelope->getSender()->getEncodedAddress(), $envelope->anyAddressHasUnicodeLocalpart()); foreach ($envelope->getRecipients() as $recipient) { $this->doRcptToCommand($recipient->getEncodedAddress()); } @@ -244,14 +245,22 @@ protected function doSend(SentMessage $message): void } } + protected function serverSupportsSmtputf8(): bool + { + return false; + } + private function doHeloCommand(): void { $this->executeCommand(\sprintf("HELO %s\r\n", $this->domain), [250]); } - private function doMailFromCommand(string $address): void + private function doMailFromCommand(string $address, bool $smtputf8): void { - $this->executeCommand(\sprintf("MAIL FROM:<%s>\r\n", $address), [250]); + if($smtputf8 && !$this->serverSupportsSmtputf8()) { + throw new InvalidArgumentException('Invalid addresses: non-ASCII characters not supported in local-part of email.'); + } + $this->executeCommand(sprintf("MAIL FROM:<%s>%s\r\n", $address, ($smtputf8 ? " SMTPUTF8" : "")), [250]); } private function doRcptToCommand(string $address): void From 2d74b980614d10de762785af2a0e848e2008f1e6 Mon Sep 17 00:00:00 2001 From: Arnt Gulbrandsen Date: Mon, 23 Sep 2024 13:51:57 +0200 Subject: [PATCH 3/7] Fix minor spelling error. --- src/Symfony/Component/Mailer/Tests/EnvelopeTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php b/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php index 4c372b1514e9e..aab2eb933d243 100644 --- a/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php +++ b/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php @@ -83,7 +83,7 @@ public function testSenderFromHeadersWithoutFrom() $this->assertEquals($from, $e->getSender()); } - public function testSenderFromHeadersWithMulitpleHeaders() + public function testSenderFromHeadersWithMultipleHeaders() { $headers = new Headers(); $headers->addMailboxListHeader('From', [new Address('from@symfony.com', 'from'), 'some@symfony.com']); From 8597c1ee8dbf66353e47db0a2f25188ccc9b5e7b Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Mon, 23 Sep 2024 14:21:51 +0200 Subject: [PATCH 4/7] Update src/Symfony/Component/Mime/Address.php --- src/Symfony/Component/Mime/Address.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Symfony/Component/Mime/Address.php b/src/Symfony/Component/Mime/Address.php index 4f5e4421e4bac..d7883356ff5ba 100644 --- a/src/Symfony/Component/Mime/Address.php +++ b/src/Symfony/Component/Mime/Address.php @@ -119,7 +119,6 @@ public static function createArray(array $addresses): array } /** - * Returns true if this address' localpart contains at least one * non-ASCII character, and false if it is only ASCII (or empty). * @@ -133,8 +132,6 @@ public static function createArray(array $addresses): array * then it is possible to to send the message using IDN encoding * instead of SMTPUTF8. The most common software will display the * message as intended. - * - * @return bool */ public function hasUnicodeLocalpart(): bool { From 3fbbb235d668fb9345de23ca4436051671c959ec Mon Sep 17 00:00:00 2001 From: Arnt Gulbrandsen Date: Mon, 23 Sep 2024 21:15:13 +0200 Subject: [PATCH 5/7] Resolve code review comments from stof and oska Also fix one mysteriously broken unit test. --- src/Symfony/Component/Mailer/CHANGELOG.md | 2 +- src/Symfony/Component/Mailer/Envelope.php | 3 --- .../Component/Mailer/Transport/Smtp/EsmtpTransport.php | 2 +- src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php | 4 ++-- src/Symfony/Component/Mime/Tests/AddressTest.php | 2 +- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/Mailer/CHANGELOG.md b/src/Symfony/Component/Mailer/CHANGELOG.md index 3fc83d62cccd1..ac98eb242df1b 100644 --- a/src/Symfony/Component/Mailer/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/CHANGELOG.md @@ -5,7 +5,7 @@ CHANGELOG --- * Make `TransportFactoryTestCase` compatible with PHPUnit 10+ - * Support unicode email addresses such as "dømi@dømi.fo", no client changes needed + * Support unicode email addresses such as "dømi@dømi.fo" 7.1 --- diff --git a/src/Symfony/Component/Mailer/Envelope.php b/src/Symfony/Component/Mailer/Envelope.php index 3ce2a30227a50..088990379541a 100644 --- a/src/Symfony/Component/Mailer/Envelope.php +++ b/src/Symfony/Component/Mailer/Envelope.php @@ -83,7 +83,6 @@ public function getRecipients(): array } /** - * Returns true if any address' localpart contains at least one * non-ASCII character, and false if all addresses have all-ASCII * localparts. @@ -97,8 +96,6 @@ public function getRecipients(): array * then it is possible to to send the message using IDN encoding * instead of SMTPUTF8. The most common software will display the * message as intended. - * - * @return bool */ public function anyAddressHasUnicodeLocalpart(): bool { diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php index b9604652c634d..3663dc6f5604c 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php @@ -195,7 +195,7 @@ private function parseCapabilities(string $ehloResponse): array return $capabilities; } - protected function serverSupportsSmtputf8(): bool + protected function serverSupportsSmtpUtf8(): bool { return \array_key_exists('SMTPUTF8', $this->capabilities); } diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php index 73bb56cc36395..0a38052f18fc0 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php @@ -245,7 +245,7 @@ protected function doSend(SentMessage $message): void } } - protected function serverSupportsSmtputf8(): bool + protected function serverSupportsSmtpUtf8(): bool { return false; } @@ -257,7 +257,7 @@ private function doHeloCommand(): void private function doMailFromCommand(string $address, bool $smtputf8): void { - if($smtputf8 && !$this->serverSupportsSmtputf8()) { + if($smtputf8 && !$this->serverSupportsSmtpUtf8()) { throw new InvalidArgumentException('Invalid addresses: non-ASCII characters not supported in local-part of email.'); } $this->executeCommand(sprintf("MAIL FROM:<%s>%s\r\n", $address, ($smtputf8 ? " SMTPUTF8" : "")), [250]); diff --git a/src/Symfony/Component/Mime/Tests/AddressTest.php b/src/Symfony/Component/Mime/Tests/AddressTest.php index e769b4b78df67..58c90161346f1 100644 --- a/src/Symfony/Component/Mime/Tests/AddressTest.php +++ b/src/Symfony/Component/Mime/Tests/AddressTest.php @@ -85,7 +85,7 @@ public function testUnicodeLocalpart() { /* dømi means example and is reserved by the .fo registry */ $this->assertFalse((new Address('info@dømi.fo'))->hasUnicodeLocalpart()); - $this->assertTrue((new Address('info@dømi.fo'))->hasUnicodeLocalpart()); + $this->assertTrue((new Address('dømi@dømi.fo'))->hasUnicodeLocalpart()); } public function testCreateArrayWrongArg() From c11d6e068d5fa0e989beffeff5b6f9c2ace06c56 Mon Sep 17 00:00:00 2001 From: Arnt Gulbrandsen Date: Tue, 24 Sep 2024 16:49:09 +0200 Subject: [PATCH 6/7] Code style conformance and dependency updates. --- src/Symfony/Component/Mailer/Envelope.php | 38 ++++++++++--------- .../Component/Mailer/Tests/EnvelopeTest.php | 2 - .../Mailer/Transport/Smtp/SmtpTransport.php | 4 +- src/Symfony/Component/Mailer/composer.json | 2 +- src/Symfony/Component/Mime/Address.php | 30 +++++++-------- 5 files changed, 39 insertions(+), 37 deletions(-) diff --git a/src/Symfony/Component/Mailer/Envelope.php b/src/Symfony/Component/Mailer/Envelope.php index 088990379541a..9ffa28af943ab 100644 --- a/src/Symfony/Component/Mailer/Envelope.php +++ b/src/Symfony/Component/Mailer/Envelope.php @@ -83,27 +83,31 @@ public function getRecipients(): array } /** - * Returns true if any address' localpart contains at least one - * non-ASCII character, and false if all addresses have all-ASCII - * localparts. - * - * This helps to decide whether to the SMTPUTF8 extensions (RFC - * 6530 and following) for any given message. - * - * The SMTPUTF8 extension is strictly required if any address - * contains a non-ASCII character in its localpart. If non-ASCII - * is only used in domains (e.g. horst@freiherr-von-mühlhausen.de) - * then it is possible to to send the message using IDN encoding - * instead of SMTPUTF8. The most common software will display the - * message as intended. - */ + * Returns true if any address' localpart contains at least one + * non-ASCII character, and false if all addresses have all-ASCII + * localparts. + * + * This helps to decide whether to the SMTPUTF8 extensions (RFC + * 6530 and following) for any given message. + * + * The SMTPUTF8 extension is strictly required if any address + * contains a non-ASCII character in its localpart. If non-ASCII + * is only used in domains (e.g. horst@freiherr-von-mühlhausen.de) + * then it is possible to to send the message using IDN encoding + * instead of SMTPUTF8. The most common software will display the + * message as intended. + */ public function anyAddressHasUnicodeLocalpart(): bool { - if($this->getSender()->hasUnicodeLocalpart()) + if ($this->getSender()->hasUnicodeLocalpart()) { return true; - foreach($this->getRecipients() as $r) - if($r->hasUnicodeLocalpart()) + } + foreach ($this->getRecipients() as $r) { + if ($r->hasUnicodeLocalpart()) { return true; + } + } + return false; } } diff --git a/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php b/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php index aab2eb933d243..c362279c46a48 100644 --- a/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php +++ b/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php @@ -13,11 +13,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mailer\Envelope; -use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Header\Headers; -use Symfony\Component\Mime\Header\PathHeader; use Symfony\Component\Mime\Message; use Symfony\Component\Mime\RawMessage; diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php index 0a38052f18fc0..9d99c061660a8 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php @@ -257,10 +257,10 @@ private function doHeloCommand(): void private function doMailFromCommand(string $address, bool $smtputf8): void { - if($smtputf8 && !$this->serverSupportsSmtpUtf8()) { + if ($smtputf8 && !$this->serverSupportsSmtpUtf8()) { throw new InvalidArgumentException('Invalid addresses: non-ASCII characters not supported in local-part of email.'); } - $this->executeCommand(sprintf("MAIL FROM:<%s>%s\r\n", $address, ($smtputf8 ? " SMTPUTF8" : "")), [250]); + $this->executeCommand(\sprintf("MAIL FROM:<%s>%s\r\n", $address, $smtputf8 ? ' SMTPUTF8' : ''), [250]); } private function doRcptToCommand(string $address): void diff --git a/src/Symfony/Component/Mailer/composer.json b/src/Symfony/Component/Mailer/composer.json index 76e2d9d486ffb..4336e725133fc 100644 --- a/src/Symfony/Component/Mailer/composer.json +++ b/src/Symfony/Component/Mailer/composer.json @@ -21,7 +21,7 @@ "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", + "symfony/mime": "^7.2", "symfony/service-contracts": "^2.5|^3" }, "require-dev": { diff --git a/src/Symfony/Component/Mime/Address.php b/src/Symfony/Component/Mime/Address.php index d7883356ff5ba..e05781ce5ead4 100644 --- a/src/Symfony/Component/Mime/Address.php +++ b/src/Symfony/Component/Mime/Address.php @@ -119,22 +119,22 @@ public static function createArray(array $addresses): array } /** - * Returns true if this address' localpart contains at least one - * non-ASCII character, and false if it is only ASCII (or empty). - * - * This is a helper for Envelope, which has to decide whether to - * the SMTPUTF8 extensions (RFC 6530 and following) for any given - * message. - * - * The SMTPUTF8 extension is strictly required if any address - * contains a non-ASCII character in its localpart. If non-ASCII - * is only used in domains (e.g. horst@freiherr-von-mühlhausen.de) - * then it is possible to to send the message using IDN encoding - * instead of SMTPUTF8. The most common software will display the - * message as intended. - */ + * Returns true if this address' localpart contains at least one + * non-ASCII character, and false if it is only ASCII (or empty). + * + * This is a helper for Envelope, which has to decide whether to + * the SMTPUTF8 extensions (RFC 6530 and following) for any given + * message. + * + * The SMTPUTF8 extension is strictly required if any address + * contains a non-ASCII character in its localpart. If non-ASCII + * is only used in domains (e.g. horst@freiherr-von-mühlhausen.de) + * then it is possible to to send the message using IDN encoding + * instead of SMTPUTF8. The most common software will display the + * message as intended. + */ public function hasUnicodeLocalpart(): bool { - return (bool) preg_match( '/[\x80-\xFF].*@/', $this->address); + return (bool) preg_match('/[\x80-\xFF].*@/', $this->address); } } From b1deef8ff38a1220614362d98c8f8f5d9734d2e3 Mon Sep 17 00:00:00 2001 From: Arnt Gulbrandsen Date: Fri, 27 Sep 2024 13:16:59 +0200 Subject: [PATCH 7/7] Reinstate the restriction that the sender's localpart must be all-ASCII. This commit also adds a test that Symfony chooses IDN encoding when possible (to be compatible with all email receivers), and adjusts a couple of tests to match the name used in the main source code. --- src/Symfony/Component/Mailer/Envelope.php | 4 ++++ .../Component/Mailer/Tests/EnvelopeTest.php | 17 +++++++++++++ .../Transport/Smtp/EsmtpTransportTest.php | 24 +++++++++++++++---- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Mailer/Envelope.php b/src/Symfony/Component/Mailer/Envelope.php index 9ffa28af943ab..9bcb1a3c8bc93 100644 --- a/src/Symfony/Component/Mailer/Envelope.php +++ b/src/Symfony/Component/Mailer/Envelope.php @@ -44,6 +44,10 @@ public static function create(RawMessage $message): self public function setSender(Address $sender): void { + // to ensure deliverability of bounce emails independent of UTF-8 capabilities of SMTP servers + if (!preg_match('/^[^@\x80-\xFF]++@/', $sender->getAddress())) { + throw new InvalidArgumentException(\sprintf('Invalid sender "%s": non-ASCII characters not supported in local-part of email.', $sender->getAddress())); + } $this->sender = $sender; } diff --git a/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php b/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php index c362279c46a48..80feba96b38a2 100644 --- a/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php +++ b/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php @@ -13,9 +13,11 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; use Symfony\Component\Mailer\Exception\LogicException; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Header\PathHeader; use Symfony\Component\Mime\Message; use Symfony\Component\Mime\RawMessage; @@ -27,6 +29,13 @@ public function testConstructorWithAddressSender() $this->assertEquals(new Address('fabien@symfony.com'), $e->getSender()); } + public function testConstructorWithAddressSenderAndNonAsciiCharactersInLocalPartOfAddress() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid sender "fabièn@symfony.com": non-ASCII characters not supported in local-part of email.'); + new Envelope(new Address('fabièn@symfony.com'), [new Address('thomas@symfony.com')]); + } + public function testConstructorWithNamedAddressSender() { $e = new Envelope($sender = new Address('fabien@symfony.com', 'Fabien'), [new Address('thomas@symfony.com')]); @@ -72,6 +81,14 @@ public function testSenderFromHeaders() $this->assertEquals($from, $e->getSender()); } + public function testSenderFromHeadersFailsWithNonAsciiCharactersInLocalPart() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid sender "fabièn@symfony.com": non-ASCII characters not supported in local-part of email.'); + $message = new Message(new Headers(new PathHeader('Return-Path', new Address('fabièn@symfony.com')))); + Envelope::create($message)->getSender(); + } + public function testSenderFromHeadersWithoutFrom() { $headers = new Headers(); diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php index 9c6ef09634a60..9b4eacbf1b7f0 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php @@ -63,10 +63,10 @@ public function testExtensibility() $this->assertContains("RCPT TO: NOTIFY=FAILURE\r\n", $stream->getCommands()); } - public function testSmtputf8() + public function testSmtpUtf8() { $stream = new DummyStream(); - $transport = new Smtputf8EsmtpTransport(stream: $stream); + $transport = new SmtpUtf8EsmtpTransport(stream: $stream); $message = new Email(); $message->from('info@dømi.fo'); @@ -79,7 +79,7 @@ public function testSmtputf8() $this->assertContains("RCPT TO:\r\n", $stream->getCommands()); } - public function testMissingSmtputf8() + public function testMissingSmtpUtf8() { $stream = new DummyStream(); $transport = new EsmtpTransport(stream: $stream); @@ -94,6 +94,22 @@ public function testMissingSmtputf8() $transport->send($message); } + public function testSmtpUtf8FallbackToIDN() + { + $stream = new DummyStream(); + $transport = new EsmtpTransport(stream: $stream); + + $message = new Email(); + $message->from('info@dømi.fo'); // UTF8 only in the domain + $message->addTo('example@example.com'); + $message->text('.'); + + $transport->send($message); + + $this->assertContains("MAIL FROM:\r\n", $stream->getCommands()); + $this->assertContains("RCPT TO:\r\n", $stream->getCommands()); + } + public function testConstructorWithDefaultAuthenticators() { $stream = new DummyStream(); @@ -303,7 +319,7 @@ public function executeCommand(string $command, array $codes): string } } -class Smtputf8EsmtpTransport extends EsmtpTransport +class SmtpUtf8EsmtpTransport extends EsmtpTransport { public function executeCommand(string $command, array $codes): string { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy