Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Mailer] Allow overriding default eSMTP authenticators #49900

Merged
merged 1 commit into from
Apr 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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