Skip to content

Commit 7a9afbe

Browse files
committed
feature #49900 [Mailer] Allow overriding default eSMTP authenticators (cedric-anne)
This PR was merged into the 6.3 branch. Discussion ---------- [Mailer] Allow overriding default eSMTP authenticators | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #49701 | License | MIT | Doc PR | symfony/symfony-docs#... TODO SMTP authentication using OAuth token on Azure servers is really long, due to high latency responses from the SMTP server (probably to prevent brute-force attacks). Indeed, a `AUTH LOGIN` command is sent first, and have to wait for about 5 seconds get the error response back. Then a `RSET` command is sent and also have to wait for about 5 seconds get a response back. The `AUTH XOAUTH2` command is then sent and all is fast after that. Adding the ability to override default eSMTP authenticators will permit developers to explicitely define that only `XOAUTH2` authenticator has to be used, to prevent high latency in SMTP authentication. I will update the documentation once new methods signatures will be validated. Commits ------- bb656d0bf6 [Mailer] Allow overriding default eSMTP authenticators
2 parents 1b912f4 + f9f99e0 commit 7a9afbe

File tree

4 files changed

+172
-9
lines changed

4 files changed

+172
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ CHANGELOG
66

77
* Add `MessageEvent::reject()` to allow rejecting an email before sending it
88
* Change the default port for the `mailgun+smtp` transport from 465 to 587
9+
* Add `$authenticators` parameter in `EsmtpTransport` constructor and `EsmtpTransport::setAuthenticators()`
10+
to allow overriding of default eSMTP authenticators
911

1012
6.2.7
1113
-----

Tests/Transport/Smtp/DummyStream.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,21 @@ public function write(string $bytes, $debug = true): void
5555
$this->commands[] = $bytes;
5656

5757
if (str_starts_with($bytes, 'EHLO')) {
58-
$this->nextResponse = '250 localhost';
58+
$this->nextResponse = '250 localhost'."\r\n".'250-AUTH PLAIN LOGIN CRAM-MD5 XOAUTH2';
59+
} elseif (str_starts_with($bytes, 'AUTH LOGIN')) {
60+
$this->nextResponse = '334 VXNlcm5hbWU6';
61+
} elseif (str_starts_with($bytes, 'dGVzdHVzZXI=')) {
62+
$this->nextResponse = '334 UGFzc3dvcmQ6';
63+
} elseif (str_starts_with($bytes, 'cDRzc3cwcmQ=')) {
64+
$this->nextResponse = '535 5.7.139 Authentication unsuccessful';
65+
} elseif (str_starts_with($bytes, 'AUTH CRAM-MD5')) {
66+
$this->nextResponse = '334 PDAxMjM0NTY3ODkuMDEyMzQ1NjdAc3ltZm9ueT4=';
67+
} elseif (str_starts_with($bytes, 'dGVzdHVzZXIgNTdlYzg2ODM5OWZhZThjY2M5OWFhZGVjZjhiZTAwNmY=')) {
68+
$this->nextResponse = '535 5.7.139 Authentication unsuccessful';
69+
} elseif (str_starts_with($bytes, 'AUTH PLAIN') || str_starts_with($bytes, 'AUTH XOAUTH2')) {
70+
$this->nextResponse = '535 5.7.139 Authentication unsuccessful';
71+
} elseif (str_starts_with($bytes, 'RSET')) {
72+
$this->nextResponse = '250 2.0.0 Resetting';
5973
} elseif (str_starts_with($bytes, 'DATA')) {
6074
$this->nextResponse = '354 Enter message, ending with "." on a line by itself';
6175
} elseif (str_starts_with($bytes, 'QUIT')) {

Tests/Transport/Smtp/EsmtpTransportTest.php

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

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Mailer\Exception\TransportException;
16+
use Symfony\Component\Mailer\Transport\Smtp\Auth\CramMd5Authenticator;
17+
use Symfony\Component\Mailer\Transport\Smtp\Auth\LoginAuthenticator;
18+
use Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator;
1519
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
1620
use Symfony\Component\Mime\Email;
1721

@@ -57,6 +61,137 @@ public function testExtensibility()
5761
$this->assertContains("MAIL FROM:<[email protected]> RET=HDRS\r\n", $stream->getCommands());
5862
$this->assertContains("RCPT TO:<[email protected]> NOTIFY=FAILURE\r\n", $stream->getCommands());
5963
}
64+
65+
public function testConstructorWithDefaultAuthenticators()
66+
{
67+
$stream = new DummyStream();
68+
$transport = new EsmtpTransport(stream: $stream);
69+
$transport->setUsername('testuser');
70+
$transport->setPassword('p4ssw0rd');
71+
72+
$message = new Email();
73+
$message->from('[email protected]');
74+
$message->addTo('[email protected]');
75+
$message->text('.');
76+
77+
try {
78+
$transport->send($message);
79+
$this->fail('Symfony\Component\Mailer\Exception\TransportException to be thrown');
80+
} catch (TransportException $e) {
81+
$this->assertStringStartsWith('Failed to authenticate on SMTP server with username "testuser" using the following authenticators: "CRAM-MD5", "LOGIN", "PLAIN", "XOAUTH2".', $e->getMessage());
82+
}
83+
84+
$this->assertEquals(
85+
[
86+
"EHLO [127.0.0.1]\r\n",
87+
// S: 250 localhost
88+
// S: 250-AUTH PLAIN LOGIN CRAM-MD5 XOAUTH2
89+
"AUTH CRAM-MD5\r\n",
90+
// S: 334 PDAxMjM0NTY3ODkuMDEyMzQ1NjdAc3ltZm9ueT4=
91+
"dGVzdHVzZXIgNTdlYzg2ODM5OWZhZThjY2M5OWFhZGVjZjhiZTAwNmY=\r\n",
92+
// S: 535 5.7.139 Authentication unsuccessful
93+
"RSET\r\n",
94+
// S: 250 2.0.0 Resetting
95+
"AUTH LOGIN\r\n",
96+
// S: 334 VXNlcm5hbWU6
97+
"dGVzdHVzZXI=\r\n",
98+
// S: 334 UGFzc3dvcmQ6
99+
"cDRzc3cwcmQ=\r\n",
100+
// S: 535 5.7.139 Authentication unsuccessful
101+
"RSET\r\n",
102+
// S: 250 2.0.0 Resetting
103+
"AUTH PLAIN dGVzdHVzZXIAdGVzdHVzZXIAcDRzc3cwcmQ=\r\n",
104+
// S: 535 5.7.139 Authentication unsuccessful
105+
"RSET\r\n",
106+
// S: 250 2.0.0 Resetting
107+
"AUTH XOAUTH2 dXNlcj10ZXN0dXNlcgFhdXRoPUJlYXJlciBwNHNzdzByZAEB\r\n",
108+
// S: 535 5.7.139 Authentication unsuccessful
109+
"RSET\r\n",
110+
// S: 250 2.0.0 Resetting
111+
],
112+
$stream->getCommands()
113+
);
114+
}
115+
116+
public function testConstructorWithRedefinedAuthenticators()
117+
{
118+
$stream = new DummyStream();
119+
$transport = new EsmtpTransport(
120+
stream: $stream,
121+
authenticators: [new CramMd5Authenticator(), new LoginAuthenticator()]
122+
);
123+
$transport->setUsername('testuser');
124+
$transport->setPassword('p4ssw0rd');
125+
126+
$message = new Email();
127+
$message->from('[email protected]');
128+
$message->addTo('[email protected]');
129+
$message->text('.');
130+
131+
try {
132+
$transport->send($message);
133+
$this->fail('Symfony\Component\Mailer\Exception\TransportException to be thrown');
134+
} catch (TransportException $e) {
135+
$this->assertStringStartsWith('Failed to authenticate on SMTP server with username "testuser" using the following authenticators: "CRAM-MD5", "LOGIN".', $e->getMessage());
136+
}
137+
138+
$this->assertEquals(
139+
[
140+
"EHLO [127.0.0.1]\r\n",
141+
// S: 250 localhost
142+
// S: 250-AUTH PLAIN LOGIN CRAM-MD5 XOAUTH2
143+
"AUTH CRAM-MD5\r\n",
144+
// S: 334 PDAxMjM0NTY3ODkuMDEyMzQ1NjdAc3ltZm9ueT4=
145+
"dGVzdHVzZXIgNTdlYzg2ODM5OWZhZThjY2M5OWFhZGVjZjhiZTAwNmY=\r\n",
146+
// S: 535 5.7.139 Authentication unsuccessful
147+
"RSET\r\n",
148+
// S: 250 2.0.0 Resetting
149+
"AUTH LOGIN\r\n",
150+
// S: 334 VXNlcm5hbWU6
151+
"dGVzdHVzZXI=\r\n",
152+
// S: 334 UGFzc3dvcmQ6
153+
"cDRzc3cwcmQ=\r\n",
154+
// S: 535 5.7.139 Authentication unsuccessful
155+
"RSET\r\n",
156+
// S: 250 2.0.0 Resetting
157+
],
158+
$stream->getCommands()
159+
);
160+
}
161+
162+
public function testSetAuthenticators()
163+
{
164+
$stream = new DummyStream();
165+
$transport = new EsmtpTransport(stream: $stream);
166+
$transport->setUsername('testuser');
167+
$transport->setPassword('p4ssw0rd');
168+
$transport->setAuthenticators([new XOAuth2Authenticator()]);
169+
170+
$message = new Email();
171+
$message->from('[email protected]');
172+
$message->addTo('[email protected]');
173+
$message->text('.');
174+
175+
try {
176+
$transport->send($message);
177+
$this->fail('Symfony\Component\Mailer\Exception\TransportException to be thrown');
178+
} catch (TransportException $e) {
179+
$this->assertStringStartsWith('Failed to authenticate on SMTP server with username "testuser" using the following authenticators: "XOAUTH2".', $e->getMessage());
180+
}
181+
182+
$this->assertEquals(
183+
[
184+
"EHLO [127.0.0.1]\r\n",
185+
// S: 250 localhost
186+
// S: 250-AUTH PLAIN LOGIN CRAM-MD5 XOAUTH2
187+
"AUTH XOAUTH2 dXNlcj10ZXN0dXNlcgFhdXRoPUJlYXJlciBwNHNzdzByZAEB\r\n",
188+
// S: 535 5.7.139 Authentication unsuccessful
189+
"RSET\r\n",
190+
// S: 250 2.0.0 Resetting
191+
],
192+
$stream->getCommands()
193+
);
194+
}
60195
}
61196

62197
class CustomEsmtpTransport extends EsmtpTransport

Transport/Smtp/EsmtpTransport.php

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,21 @@ class EsmtpTransport extends SmtpTransport
3232
private string $password = '';
3333
private array $capabilities;
3434

35-
public function __construct(string $host = 'localhost', int $port = 0, bool $tls = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null, AbstractStream $stream = null)
35+
public function __construct(string $host = 'localhost', int $port = 0, bool $tls = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null, AbstractStream $stream = null, array $authenticators = null)
3636
{
3737
parent::__construct($stream, $dispatcher, $logger);
3838

39-
// order is important here (roughly most secure and popular first)
40-
$this->authenticators = [
41-
new Auth\CramMd5Authenticator(),
42-
new Auth\LoginAuthenticator(),
43-
new Auth\PlainAuthenticator(),
44-
new Auth\XOAuth2Authenticator(),
45-
];
39+
if (null === $authenticators) {
40+
// fallback to default authenticators
41+
// order is important here (roughly most secure and popular first)
42+
$authenticators = [
43+
new Auth\CramMd5Authenticator(),
44+
new Auth\LoginAuthenticator(),
45+
new Auth\PlainAuthenticator(),
46+
new Auth\XOAuth2Authenticator(),
47+
];
48+
}
49+
$this->setAuthenticators($authenticators);
4650

4751
/** @var SocketStream $stream */
4852
$stream = $this->getStream();
@@ -95,6 +99,14 @@ public function getPassword(): string
9599
return $this->password;
96100
}
97101

102+
public function setAuthenticators(array $authenticators): void
103+
{
104+
$this->authenticators = [];
105+
foreach ($authenticators as $authenticator) {
106+
$this->addAuthenticator($authenticator);
107+
}
108+
}
109+
98110
public function addAuthenticator(AuthenticatorInterface $authenticator): void
99111
{
100112
$this->authenticators[] = $authenticator;

0 commit comments

Comments
 (0)