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 {