diff --git a/src/Symfony/Component/Mailer/CHANGELOG.md b/src/Symfony/Component/Mailer/CHANGELOG.md index 01fb57558f366..ac98eb242df1b 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" 7.1 --- diff --git a/src/Symfony/Component/Mailer/Envelope.php b/src/Symfony/Component/Mailer/Envelope.php index 45e1db36397b6..9bcb1a3c8bc93 100644 --- a/src/Symfony/Component/Mailer/Envelope.php +++ b/src/Symfony/Component/Mailer/Envelope.php @@ -85,4 +85,33 @@ public function getRecipients(): array { return $this->recipients; } + + /** + * 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()) { + return true; + } + 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 de4eb0e0810a2..80feba96b38a2 100644 --- a/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php +++ b/src/Symfony/Component/Mailer/Tests/EnvelopeTest.php @@ -98,7 +98,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']); @@ -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/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php index 977d2a05e5981..9b4eacbf1b7f0 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,53 @@ 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 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(); @@ -270,3 +318,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..3663dc6f5604c 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..9d99c061660a8 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 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 a78b64321d7fb..e05781ce5ead4 100644 --- a/src/Symfony/Component/Mime/Address.php +++ b/src/Symfony/Component/Mime/Address.php @@ -117,4 +117,24 @@ 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. + */ + 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..58c90161346f1 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('dømi@dømi.fo'))->hasUnicodeLocalpart()); + } + public function testCreateArrayWrongArg() { $this->expectException(\TypeError::class); 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