Skip to content

Commit

Permalink
feature #49900 [Mailer] Allow overriding default eSMTP authenticators…
Browse files Browse the repository at this point in the history
… (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
-------

bb656d0 [Mailer] Allow overriding default eSMTP authenticators
  • Loading branch information
fabpot committed Apr 14, 2023
2 parents 58f0915 + bb656d0 commit 88f04e6
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 9 deletions.
2 changes: 2 additions & 0 deletions src/Symfony/Component/Mailer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ CHANGELOG

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

6.2.7
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,21 @@ public function write(string $bytes, $debug = true): void
$this->commands[] = $bytes;

if (str_starts_with($bytes, 'EHLO')) {
$this->nextResponse = '250 localhost';
$this->nextResponse = '250 localhost'."\r\n".'250-AUTH PLAIN LOGIN CRAM-MD5 XOAUTH2';
} elseif (str_starts_with($bytes, 'AUTH LOGIN')) {
$this->nextResponse = '334 VXNlcm5hbWU6';
} elseif (str_starts_with($bytes, 'dGVzdHVzZXI=')) {
$this->nextResponse = '334 UGFzc3dvcmQ6';
} elseif (str_starts_with($bytes, 'cDRzc3cwcmQ=')) {
$this->nextResponse = '535 5.7.139 Authentication unsuccessful';
} elseif (str_starts_with($bytes, 'AUTH CRAM-MD5')) {
$this->nextResponse = '334 PDAxMjM0NTY3ODkuMDEyMzQ1NjdAc3ltZm9ueT4=';
} elseif (str_starts_with($bytes, 'dGVzdHVzZXIgNTdlYzg2ODM5OWZhZThjY2M5OWFhZGVjZjhiZTAwNmY=')) {
$this->nextResponse = '535 5.7.139 Authentication unsuccessful';
} elseif (str_starts_with($bytes, 'AUTH PLAIN') || str_starts_with($bytes, 'AUTH XOAUTH2')) {
$this->nextResponse = '535 5.7.139 Authentication unsuccessful';
} elseif (str_starts_with($bytes, 'RSET')) {
$this->nextResponse = '250 2.0.0 Resetting';
} elseif (str_starts_with($bytes, 'DATA')) {
$this->nextResponse = '354 Enter message, ending with "." on a line by itself';
} elseif (str_starts_with($bytes, 'QUIT')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
namespace Symfony\Component\Mailer\Tests\Transport\Smtp;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Mailer\Transport\Smtp\Auth\CramMd5Authenticator;
use Symfony\Component\Mailer\Transport\Smtp\Auth\LoginAuthenticator;
use Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
use Symfony\Component\Mime\Email;

Expand Down Expand Up @@ -57,6 +61,137 @@ public function testExtensibility()
$this->assertContains("MAIL FROM:<sender@example.org> RET=HDRS\r\n", $stream->getCommands());
$this->assertContains("RCPT TO:<recipient@example.org> NOTIFY=FAILURE\r\n", $stream->getCommands());
}

public function testConstructorWithDefaultAuthenticators()
{
$stream = new DummyStream();
$transport = new EsmtpTransport(stream: $stream);
$transport->setUsername('testuser');
$transport->setPassword('p4ssw0rd');

$message = new Email();
$message->from('sender@example.org');
$message->addTo('recipient@example.org');
$message->text('.');

try {
$transport->send($message);
$this->fail('Symfony\Component\Mailer\Exception\TransportException to be thrown');
} catch (TransportException $e) {
$this->assertStringStartsWith('Failed to authenticate on SMTP server with username "testuser" using the following authenticators: "CRAM-MD5", "LOGIN", "PLAIN", "XOAUTH2".', $e->getMessage());
}

$this->assertEquals(
[
"EHLO [127.0.0.1]\r\n",
// S: 250 localhost
// S: 250-AUTH PLAIN LOGIN CRAM-MD5 XOAUTH2
"AUTH CRAM-MD5\r\n",
// S: 334 PDAxMjM0NTY3ODkuMDEyMzQ1NjdAc3ltZm9ueT4=
"dGVzdHVzZXIgNTdlYzg2ODM5OWZhZThjY2M5OWFhZGVjZjhiZTAwNmY=\r\n",
// S: 535 5.7.139 Authentication unsuccessful
"RSET\r\n",
// S: 250 2.0.0 Resetting
"AUTH LOGIN\r\n",
// S: 334 VXNlcm5hbWU6
"dGVzdHVzZXI=\r\n",
// S: 334 UGFzc3dvcmQ6
"cDRzc3cwcmQ=\r\n",
// S: 535 5.7.139 Authentication unsuccessful
"RSET\r\n",
// S: 250 2.0.0 Resetting
"AUTH PLAIN dGVzdHVzZXIAdGVzdHVzZXIAcDRzc3cwcmQ=\r\n",
// S: 535 5.7.139 Authentication unsuccessful
"RSET\r\n",
// S: 250 2.0.0 Resetting
"AUTH XOAUTH2 dXNlcj10ZXN0dXNlcgFhdXRoPUJlYXJlciBwNHNzdzByZAEB\r\n",
// S: 535 5.7.139 Authentication unsuccessful
"RSET\r\n",
// S: 250 2.0.0 Resetting
],
$stream->getCommands()
);
}

public function testConstructorWithRedefinedAuthenticators()
{
$stream = new DummyStream();
$transport = new EsmtpTransport(
stream: $stream,
authenticators: [new CramMd5Authenticator(), new LoginAuthenticator()]
);
$transport->setUsername('testuser');
$transport->setPassword('p4ssw0rd');

$message = new Email();
$message->from('sender@example.org');
$message->addTo('recipient@example.org');
$message->text('.');

try {
$transport->send($message);
$this->fail('Symfony\Component\Mailer\Exception\TransportException to be thrown');
} catch (TransportException $e) {
$this->assertStringStartsWith('Failed to authenticate on SMTP server with username "testuser" using the following authenticators: "CRAM-MD5", "LOGIN".', $e->getMessage());
}

$this->assertEquals(
[
"EHLO [127.0.0.1]\r\n",
// S: 250 localhost
// S: 250-AUTH PLAIN LOGIN CRAM-MD5 XOAUTH2
"AUTH CRAM-MD5\r\n",
// S: 334 PDAxMjM0NTY3ODkuMDEyMzQ1NjdAc3ltZm9ueT4=
"dGVzdHVzZXIgNTdlYzg2ODM5OWZhZThjY2M5OWFhZGVjZjhiZTAwNmY=\r\n",
// S: 535 5.7.139 Authentication unsuccessful
"RSET\r\n",
// S: 250 2.0.0 Resetting
"AUTH LOGIN\r\n",
// S: 334 VXNlcm5hbWU6
"dGVzdHVzZXI=\r\n",
// S: 334 UGFzc3dvcmQ6
"cDRzc3cwcmQ=\r\n",
// S: 535 5.7.139 Authentication unsuccessful
"RSET\r\n",
// S: 250 2.0.0 Resetting
],
$stream->getCommands()
);
}

public function testSetAuthenticators()
{
$stream = new DummyStream();
$transport = new EsmtpTransport(stream: $stream);
$transport->setUsername('testuser');
$transport->setPassword('p4ssw0rd');
$transport->setAuthenticators([new XOAuth2Authenticator()]);

$message = new Email();
$message->from('sender@example.org');
$message->addTo('recipient@example.org');
$message->text('.');

try {
$transport->send($message);
$this->fail('Symfony\Component\Mailer\Exception\TransportException to be thrown');
} catch (TransportException $e) {
$this->assertStringStartsWith('Failed to authenticate on SMTP server with username "testuser" using the following authenticators: "XOAUTH2".', $e->getMessage());
}

$this->assertEquals(
[
"EHLO [127.0.0.1]\r\n",
// S: 250 localhost
// S: 250-AUTH PLAIN LOGIN CRAM-MD5 XOAUTH2
"AUTH XOAUTH2 dXNlcj10ZXN0dXNlcgFhdXRoPUJlYXJlciBwNHNzdzByZAEB\r\n",
// S: 535 5.7.139 Authentication unsuccessful
"RSET\r\n",
// S: 250 2.0.0 Resetting
],
$stream->getCommands()
);
}
}

class CustomEsmtpTransport extends EsmtpTransport
Expand Down
28 changes: 20 additions & 8 deletions src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,21 @@ class EsmtpTransport extends SmtpTransport
private string $password = '';
private array $capabilities;

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

// order is important here (roughly most secure and popular first)
$this->authenticators = [
new Auth\CramMd5Authenticator(),
new Auth\LoginAuthenticator(),
new Auth\PlainAuthenticator(),
new Auth\XOAuth2Authenticator(),
];
if (null === $authenticators) {
// fallback to default authenticators
// order is important here (roughly most secure and popular first)
$authenticators = [
new Auth\CramMd5Authenticator(),
new Auth\LoginAuthenticator(),
new Auth\PlainAuthenticator(),
new Auth\XOAuth2Authenticator(),
];
}
$this->setAuthenticators($authenticators);

/** @var SocketStream $stream */
$stream = $this->getStream();
Expand Down Expand Up @@ -95,6 +99,14 @@ public function getPassword(): string
return $this->password;
}

public function setAuthenticators(array $authenticators): void
{
$this->authenticators = [];
foreach ($authenticators as $authenticator) {
$this->addAuthenticator($authenticator);
}
}

public function addAuthenticator(AuthenticatorInterface $authenticator): void
{
$this->authenticators[] = $authenticator;
Expand Down

0 comments on commit 88f04e6

Please sign in to comment.