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 4a9c2e8

Browse filesBrowse files
committed
feature #58361 [Mailer][Mime] Support unicode email addresses (arnt, OskarStark)
This PR was merged into the 7.2 branch. Discussion ---------- [Mailer][Mime] Support unicode email addresses | Q | A | ------------- | --- | Branch? | 7.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | License | MIT This allows applications to send mail to all-Chinese email addresses, or like my test address grå`@gr`å.org. Code that uses Symfony needs no change and should experience no difference, although if the upstream MTA doesn't support it (most do by now) then an exception is thrown slightly later than before this change. Commits ------- b1deef8 Reinstate the restriction that the sender's localpart must be all-ASCII. c11d6e0 Code style conformance and dependency updates. 3fbbb23 Resolve code review comments from stof and oska 8597c1e Update src/Symfony/Component/Mime/Address.php 2d74b98 Fix minor spelling error. 6f07a17 Send SMTPUTF8 if the message needs it and the server supports it. d43b832 Add new accessors to help determine whether to use the SMTPUTF8 extension
2 parents fe9f1cd + b1deef8 commit 4a9c2e8
Copy full SHA for 4a9c2e8

File tree

Expand file treeCollapse file tree

9 files changed

+151
-5
lines changed
Filter options
Expand file treeCollapse file tree

9 files changed

+151
-5
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
@@ -10,6 +10,7 @@ CHANGELOG
1010
you now need to use the `IncompleteDsnTestTrait`.
1111

1212
* Make `TransportFactoryTestCase` compatible with PHPUnit 10+
13+
* Support unicode email addresses such as "dømi@dømi.fo"
1314

1415
7.1
1516
---

‎src/Symfony/Component/Mailer/Envelope.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mailer/Envelope.php
+29Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,33 @@ public function getRecipients(): array
8585
{
8686
return $this->recipients;
8787
}
88+
89+
/**
90+
* Returns true if any address' localpart contains at least one
91+
* non-ASCII character, and false if all addresses have all-ASCII
92+
* localparts.
93+
*
94+
* This helps to decide whether to the SMTPUTF8 extensions (RFC
95+
* 6530 and following) for any given message.
96+
*
97+
* The SMTPUTF8 extension is strictly required if any address
98+
* contains a non-ASCII character in its localpart. If non-ASCII
99+
* is only used in domains (e.g. horst@freiherr-von-mühlhausen.de)
100+
* then it is possible to to send the message using IDN encoding
101+
* instead of SMTPUTF8. The most common software will display the
102+
* message as intended.
103+
*/
104+
public function anyAddressHasUnicodeLocalpart(): bool
105+
{
106+
if ($this->getSender()->hasUnicodeLocalpart()) {
107+
return true;
108+
}
109+
foreach ($this->getRecipients() as $r) {
110+
if ($r->hasUnicodeLocalpart()) {
111+
return true;
112+
}
113+
}
114+
115+
return false;
116+
}
88117
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mailer/Tests/EnvelopeTest.php
+14-1Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public function testSenderFromHeadersWithoutFrom()
9898
$this->assertEquals($from, $e->getSender());
9999
}
100100

101-
public function testSenderFromHeadersWithMulitpleHeaders()
101+
public function testSenderFromHeadersWithMultipleHeaders()
102102
{
103103
$headers = new Headers();
104104
$headers->addMailboxListHeader('From', [new Address('from@symfony.com', 'from'), 'some@symfony.com']);
@@ -127,6 +127,19 @@ public function testRecipientsFromHeaders()
127127
$this->assertEquals([new Address('to@symfony.com'), new Address('cc@symfony.com'), new Address('bcc@symfony.com')], $e->getRecipients());
128128
}
129129

130+
public function testUnicodeLocalparts()
131+
{
132+
/* dømi means example and is reserved by the .fo registry */
133+
$i = new Address('info@dømi.fo');
134+
$d = new Address('dømi@dømi.fo');
135+
$e = new Envelope($i, [$i]);
136+
$this->assertFalse($e->anyAddressHasUnicodeLocalpart());
137+
$e = new Envelope($i, [$d]);
138+
$this->assertTrue($e->anyAddressHasUnicodeLocalpart());
139+
$e = new Envelope($i, [$i, $d]);
140+
$this->assertTrue($e->anyAddressHasUnicodeLocalpart());
141+
}
142+
130143
public function testRecipientsFromHeadersWithNames()
131144
{
132145
$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
+62Lines changed: 62 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,53 @@ 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+
97+
public function testSmtpUtf8FallbackToIDN()
98+
{
99+
$stream = new DummyStream();
100+
$transport = new EsmtpTransport(stream: $stream);
101+
102+
$message = new Email();
103+
$message->from('info@dømi.fo'); // UTF8 only in the domain
104+
$message->addTo('example@example.com');
105+
$message->text('.');
106+
107+
$transport->send($message);
108+
109+
$this->assertContains("MAIL FROM:<info@xn--dmi-0na.fo>\r\n", $stream->getCommands());
110+
$this->assertContains("RCPT TO:<example@example.com>\r\n", $stream->getCommands());
111+
}
112+
65113
public function testConstructorWithDefaultAuthenticators()
66114
{
67115
$stream = new DummyStream();
@@ -270,3 +318,17 @@ public function executeCommand(string $command, array $codes): string
270318
return $response;
271319
}
272320
}
321+
322+
class SmtpUtf8EsmtpTransport extends EsmtpTransport
323+
{
324+
public function executeCommand(string $command, array $codes): string
325+
{
326+
$response = parent::executeCommand($command, $codes);
327+
328+
if (str_starts_with($command, 'EHLO ')) {
329+
$response .= "250 SMTPUTF8\r\n";
330+
}
331+
332+
return $response;
333+
}
334+
}

‎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

‎src/Symfony/Component/Mailer/composer.json

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mailer/composer.json
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"psr/event-dispatcher": "^1",
2222
"psr/log": "^1|^2|^3",
2323
"symfony/event-dispatcher": "^6.4|^7.0",
24-
"symfony/mime": "^6.4|^7.0",
24+
"symfony/mime": "^7.2",
2525
"symfony/service-contracts": "^2.5|^3"
2626
},
2727
"require-dev": {

‎src/Symfony/Component/Mime/Address.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mime/Address.php
+20Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,24 @@ public static function createArray(array $addresses): array
117117

118118
return $addrs;
119119
}
120+
121+
/**
122+
* Returns true if this address' localpart contains at least one
123+
* non-ASCII character, and false if it is only ASCII (or empty).
124+
*
125+
* This is a helper for Envelope, which has to decide whether to
126+
* the SMTPUTF8 extensions (RFC 6530 and following) for any given
127+
* message.
128+
*
129+
* The SMTPUTF8 extension is strictly required if any address
130+
* contains a non-ASCII character in its localpart. If non-ASCII
131+
* is only used in domains (e.g. horst@freiherr-von-mühlhausen.de)
132+
* then it is possible to to send the message using IDN encoding
133+
* instead of SMTPUTF8. The most common software will display the
134+
* message as intended.
135+
*/
136+
public function hasUnicodeLocalpart(): bool
137+
{
138+
return (bool) preg_match('/[\x80-\xFF].*@/', $this->address);
139+
}
120140
}

‎src/Symfony/Component/Mime/Tests/AddressTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Mime/Tests/AddressTest.php
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ public function testCreateArray()
8181
$this->assertEquals([$fabien], Address::createArray(['fabien@symfony.com']));
8282
}
8383

84+
public function testUnicodeLocalpart()
85+
{
86+
/* dømi means example and is reserved by the .fo registry */
87+
$this->assertFalse((new Address('info@dømi.fo'))->hasUnicodeLocalpart());
88+
$this->assertTrue((new Address('dømi@dømi.fo'))->hasUnicodeLocalpart());
89+
}
90+
8491
public function testCreateArrayWrongArg()
8592
{
8693
$this->expectException(\TypeError::class);

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.