Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 6f07a17

Browse filesBrowse 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
Copy full SHA for 6f07a17

File tree

Expand file treeCollapse file tree

6 files changed

+81
-24
lines changed
Filter options
Expand file treeCollapse file tree

6 files changed

+81
-24
lines changed

‎src/Symfony/Component/Mailer/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mailer/CHANGELOG.md
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mailer/Envelope.php
+17-6Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mailer/Tests/EnvelopeTest.php
-15Lines 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('fabien@symfony.com'), $e->getSender());
3030
}
3131

32-
public function testConstructorWithAddressSenderAndNonAsciiCharactersInLocalPartOfAddress()
33-
{
34-
$this->expectException(InvalidArgumentException::class);
35-
$this->expectExceptionMessage('Invalid sender "fabièn@symfony.com": non-ASCII characters not supported in local-part of email.');
36-
new Envelope(new Address('fabièn@symfony.com'), [new Address('thomas@symfony.com')]);
37-
}
38-
3932
public function testConstructorWithNamedAddressSender()
4033
{
4134
$e = new Envelope($sender = new Address('fabien@symfony.com', 'Fabien'), [new Address('thomas@symfony.com')]);
@@ -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èn@symfony.com": non-ASCII characters not supported in local-part of email.');
88-
$message = new Message(new Headers(new PathHeader('Return-Path', new Address('fabièn@symfony.com'))));
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php
+46Lines 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:<recipient@example.org> 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:<info@xn--dmi-0na.fo> SMTPUTF8\r\n", $stream->getCommands());
79+
$this->assertContains("RCPT TO:<dømi@xn--dmi-0na.fo>\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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php
+5Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php
+12-3Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.