Skip to content

Commit 6f07a17

Browse files
committed
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).
1 parent d43b832 commit 6f07a17

File tree

6 files changed

+81
-24
lines changed

6 files changed

+81
-24
lines changed

src/Symfony/Component/Mailer/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Make `TransportFactoryTestCase` compatible with PHPUnit 10+
8+
* Support unicode email addresses such as "dømi@dømi.fo", no client changes needed
89

910
7.1
1011
---

src/Symfony/Component/Mailer/Envelope.php

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ public static function create(RawMessage $message): self
4444

4545
public function setSender(Address $sender): void
4646
{
47-
// to ensure deliverability of bounce emails independent of UTF-8 capabilities of SMTP servers
48-
if (!preg_match('/^[^@\x80-\xFF]++@/', $sender->getAddress())) {
49-
throw new InvalidArgumentException(\sprintf('Invalid sender "%s": non-ASCII characters not supported in local-part of email.', $sender->getAddress()));
50-
}
5147
$this->sender = $sender;
5248
}
5349

@@ -87,13 +83,28 @@ public function getRecipients(): array
8783
}
8884

8985
/**
86+
87+
* Returns true if any address' localpart contains at least one
88+
* non-ASCII character, and false if all addresses have all-ASCII
89+
* localparts.
90+
*
91+
* This helps to decide whether to the SMTPUTF8 extensions (RFC
92+
* 6530 and following) for any given message.
93+
*
94+
* The SMTPUTF8 extension is strictly required if any address
95+
* contains a non-ASCII character in its localpart. If non-ASCII
96+
* is only used in domains (e.g. horst@freiherr-von-mühlhausen.de)
97+
* then it is possible to to send the message using IDN encoding
98+
* instead of SMTPUTF8. The most common software will display the
99+
* message as intended.
100+
*
90101
* @return bool
91102
*/
92103
public function anyAddressHasUnicodeLocalpart(): bool
93104
{
94-
if($this->sender->hasUnicodeLocalpart())
105+
if($this->getSender()->hasUnicodeLocalpart())
95106
return true;
96-
foreach($this->recipients as $r)
107+
foreach($this->getRecipients() as $r)
97108
if($r->hasUnicodeLocalpart())
98109
return true;
99110
return false;

src/Symfony/Component/Mailer/Tests/EnvelopeTest.php

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,6 @@ public function testConstructorWithAddressSender()
2929
$this->assertEquals(new Address('[email protected]'), $e->getSender());
3030
}
3131

32-
public function testConstructorWithAddressSenderAndNonAsciiCharactersInLocalPartOfAddress()
33-
{
34-
$this->expectException(InvalidArgumentException::class);
35-
$this->expectExceptionMessage('Invalid sender "fabiè[email protected]": non-ASCII characters not supported in local-part of email.');
36-
new Envelope(new Address('fabiè[email protected]'), [new Address('[email protected]')]);
37-
}
38-
3932
public function testConstructorWithNamedAddressSender()
4033
{
4134
$e = new Envelope($sender = new Address('[email protected]', 'Fabien'), [new Address('[email protected]')]);
@@ -81,14 +74,6 @@ public function testSenderFromHeaders()
8174
$this->assertEquals($from, $e->getSender());
8275
}
8376

84-
public function testSenderFromHeadersFailsWithNonAsciiCharactersInLocalPart()
85-
{
86-
$this->expectException(InvalidArgumentException::class);
87-
$this->expectExceptionMessage('Invalid sender "fabiè[email protected]": non-ASCII characters not supported in local-part of email.');
88-
$message = new Message(new Headers(new PathHeader('Return-Path', new Address('fabiè[email protected]'))));
89-
Envelope::create($message)->getSender();
90-
}
91-
9277
public function testSenderFromHeadersWithoutFrom()
9378
{
9479
$headers = new Headers();

src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Mailer\Tests\Transport\Smtp;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
1516
use Symfony\Component\Mailer\Exception\TransportException;
1617
use Symfony\Component\Mailer\Transport\Smtp\Auth\CramMd5Authenticator;
1718
use Symfony\Component\Mailer\Transport\Smtp\Auth\LoginAuthenticator;
@@ -62,6 +63,37 @@ public function testExtensibility()
6263
$this->assertContains("RCPT TO:<[email protected]> NOTIFY=FAILURE\r\n", $stream->getCommands());
6364
}
6465

66+
public function testSmtputf8()
67+
{
68+
$stream = new DummyStream();
69+
$transport = new Smtputf8EsmtpTransport(stream: $stream);
70+
71+
$message = new Email();
72+
$message->from('info@dømi.fo');
73+
$message->addTo('dømi@dømi.fo');
74+
$message->text('.');
75+
76+
$transport->send($message);
77+
78+
$this->assertContains("MAIL FROM:<[email protected]> SMTPUTF8\r\n", $stream->getCommands());
79+
$this->assertContains("RCPT TO:<dø[email protected]>\r\n", $stream->getCommands());
80+
}
81+
82+
public function testMissingSmtputf8()
83+
{
84+
$stream = new DummyStream();
85+
$transport = new EsmtpTransport(stream: $stream);
86+
87+
$message = new Email();
88+
$message->from('info@dømi.fo');
89+
$message->addTo('dømi@dømi.fo');
90+
$message->text('.');
91+
92+
$this->expectException(InvalidArgumentException::class);
93+
$this->expectExceptionMessage('Invalid addresses: non-ASCII characters not supported in local-part of email.');
94+
$transport->send($message);
95+
}
96+
6597
public function testConstructorWithDefaultAuthenticators()
6698
{
6799
$stream = new DummyStream();
@@ -270,3 +302,17 @@ public function executeCommand(string $command, array $codes): string
270302
return $response;
271303
}
272304
}
305+
306+
class Smtputf8EsmtpTransport extends EsmtpTransport
307+
{
308+
public function executeCommand(string $command, array $codes): string
309+
{
310+
$response = parent::executeCommand($command, $codes);
311+
312+
if (str_starts_with($command, 'EHLO ')) {
313+
$response .= "250 SMTPUTF8\r\n";
314+
}
315+
316+
return $response;
317+
}
318+
}

src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,11 @@ private function parseCapabilities(string $ehloResponse): array
195195
return $capabilities;
196196
}
197197

198+
protected function serverSupportsSmtputf8(): bool
199+
{
200+
return \array_key_exists('SMTPUTF8', $this->capabilities);
201+
}
202+
198203
private function handleAuth(array $modes): void
199204
{
200205
if (!$this->username) {

src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Psr\EventDispatcher\EventDispatcherInterface;
1515
use Psr\Log\LoggerInterface;
1616
use Symfony\Component\Mailer\Envelope;
17+
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
1718
use Symfony\Component\Mailer\Exception\LogicException;
1819
use Symfony\Component\Mailer\Exception\TransportException;
1920
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
@@ -211,7 +212,7 @@ protected function doSend(SentMessage $message): void
211212

212213
try {
213214
$envelope = $message->getEnvelope();
214-
$this->doMailFromCommand($envelope->getSender()->getEncodedAddress());
215+
$this->doMailFromCommand($envelope->getSender()->getEncodedAddress(), $envelope->anyAddressHasUnicodeLocalpart());
215216
foreach ($envelope->getRecipients() as $recipient) {
216217
$this->doRcptToCommand($recipient->getEncodedAddress());
217218
}
@@ -244,14 +245,22 @@ protected function doSend(SentMessage $message): void
244245
}
245246
}
246247

248+
protected function serverSupportsSmtputf8(): bool
249+
{
250+
return false;
251+
}
252+
247253
private function doHeloCommand(): void
248254
{
249255
$this->executeCommand(\sprintf("HELO %s\r\n", $this->domain), [250]);
250256
}
251257

252-
private function doMailFromCommand(string $address): void
258+
private function doMailFromCommand(string $address, bool $smtputf8): void
253259
{
254-
$this->executeCommand(\sprintf("MAIL FROM:<%s>\r\n", $address), [250]);
260+
if($smtputf8 && !$this->serverSupportsSmtputf8()) {
261+
throw new InvalidArgumentException('Invalid addresses: non-ASCII characters not supported in local-part of email.');
262+
}
263+
$this->executeCommand(sprintf("MAIL FROM:<%s>%s\r\n", $address, ($smtputf8 ? " SMTPUTF8" : "")), [250]);
255264
}
256265

257266
private function doRcptToCommand(string $address): void

0 commit comments

Comments
 (0)