[Mailer] simplified the way TLS/SSL/StartTls work

This commit is contained in:
Fabien Potencier 2019-08-19 09:22:00 +02:00
parent bc79cfe003
commit 5b8c4676d0
28 changed files with 196 additions and 62 deletions

View File

@ -43,6 +43,11 @@ class SesTransportFactoryTest extends TransportFactoryTestCase
true,
];
yield [
new Dsn('smtps', 'ses'),
true,
];
yield [
new Dsn('smtp', 'example.com'),
false,
@ -84,13 +89,18 @@ class SesTransportFactoryTest extends TransportFactoryTestCase
new Dsn('smtp', 'ses', self::USER, self::PASSWORD, null, ['region' => 'eu-west-1']),
new SesSmtpTransport(self::USER, self::PASSWORD, 'eu-west-1', $dispatcher, $logger),
];
yield [
new Dsn('smtps', 'ses', self::USER, self::PASSWORD, null, ['region' => 'eu-west-1']),
new SesSmtpTransport(self::USER, self::PASSWORD, 'eu-west-1', $dispatcher, $logger),
];
}
public function unsupportedSchemeProvider(): iterable
{
yield [
new Dsn('foo', 'ses', self::USER, self::PASSWORD),
'The "foo" scheme is not supported for mailer "ses". Supported schemes are: "api", "http", "smtp".',
'The "foo" scheme is not supported for mailer "ses". Supported schemes are: "api", "http", "smtp", "smtps".',
];
}

View File

@ -25,7 +25,7 @@ class SesSmtpTransport extends EsmtpTransport
*/
public function __construct(string $username, string $password, string $region = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
{
parent::__construct(sprintf('email-smtp.%s.amazonaws.com', $region ?: 'eu-west-1'), 587, 'tls', null, $dispatcher, $logger);
parent::__construct(sprintf('email-smtp.%s.amazonaws.com', $region ?: 'eu-west-1'), 587, true, null, $dispatcher, $logger);
$this->setUsername($username);
$this->setPassword($password);

View File

@ -36,11 +36,11 @@ final class SesTransportFactory extends AbstractTransportFactory
return new SesHttpTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger);
}
if ('smtp' === $scheme) {
if ('smtp' === $scheme || 'smtps' === $scheme) {
return new SesSmtpTransport($user, $password, $region, $this->dispatcher, $this->logger);
}
throw new UnsupportedSchemeException($dsn, ['api', 'http', 'smtp']);
throw new UnsupportedSchemeException($dsn, ['api', 'http', 'smtp', 'smtps']);
}
public function supports(Dsn $dsn): bool

View File

@ -22,6 +22,11 @@ class GmailTransportFactoryTest extends TransportFactoryTestCase
true,
];
yield [
new Dsn('smtps', 'gmail'),
true,
];
yield [
new Dsn('smtp', 'example.com'),
false,
@ -34,13 +39,18 @@ class GmailTransportFactoryTest extends TransportFactoryTestCase
new Dsn('smtp', 'gmail', self::USER, self::PASSWORD),
new GmailSmtpTransport(self::USER, self::PASSWORD, $this->getDispatcher(), $this->getLogger()),
];
yield [
new Dsn('smtps', 'gmail', self::USER, self::PASSWORD),
new GmailSmtpTransport(self::USER, self::PASSWORD, $this->getDispatcher(), $this->getLogger()),
];
}
public function unsupportedSchemeProvider(): iterable
{
yield [
new Dsn('foo', 'gmail', self::USER, self::PASSWORD),
'The "foo" scheme is not supported for mailer "gmail". Supported schemes are: "smtp".',
'The "foo" scheme is not supported for mailer "gmail". Supported schemes are: "smtp", "smtps".',
];
}

View File

@ -22,7 +22,7 @@ class GmailSmtpTransport extends EsmtpTransport
{
public function __construct(string $username, string $password, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
{
parent::__construct('smtp.gmail.com', 465, 'ssl', null, $dispatcher, $logger);
parent::__construct('smtp.gmail.com', 465, true, null, $dispatcher, $logger);
$this->setUsername($username);
$this->setPassword($password);

View File

@ -23,11 +23,11 @@ final class GmailTransportFactory extends AbstractTransportFactory
{
public function create(Dsn $dsn): TransportInterface
{
if ('smtp' === $dsn->getScheme()) {
if ('smtp' === $dsn->getScheme() || 'smtps' === $dsn->getScheme()) {
return new GmailSmtpTransport($this->getUser($dsn), $this->getPassword($dsn), $this->dispatcher, $this->logger);
}
throw new UnsupportedSchemeException($dsn, ['smtp']);
throw new UnsupportedSchemeException($dsn, ['smtp', 'smtps']);
}
public function supports(Dsn $dsn): bool

View File

@ -43,6 +43,11 @@ class MandrillTransportFactoryTest extends TransportFactoryTestCase
true,
];
yield [
new Dsn('smtps', 'mandrill'),
true,
];
yield [
new Dsn('smtp', 'example.com'),
false,
@ -69,13 +74,18 @@ class MandrillTransportFactoryTest extends TransportFactoryTestCase
new Dsn('smtp', 'mandrill', self::USER, self::PASSWORD),
new MandrillSmtpTransport(self::USER, self::PASSWORD, $dispatcher, $logger),
];
yield [
new Dsn('smtps', 'mandrill', self::USER, self::PASSWORD),
new MandrillSmtpTransport(self::USER, self::PASSWORD, $dispatcher, $logger),
];
}
public function unsupportedSchemeProvider(): iterable
{
yield [
new Dsn('foo', 'mandrill', self::USER),
'The "foo" scheme is not supported for mailer "mandrill". Supported schemes are: "api", "http", "smtp".',
'The "foo" scheme is not supported for mailer "mandrill". Supported schemes are: "api", "http", "smtp", "smtps".',
];
}

View File

@ -22,7 +22,7 @@ class MandrillSmtpTransport extends EsmtpTransport
{
public function __construct(string $username, string $password, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
{
parent::__construct('smtp.mandrillapp.com', 587, 'tls', null, $dispatcher, $logger);
parent::__construct('smtp.mandrillapp.com', 587, true, null, $dispatcher, $logger);
$this->setUsername($username);
$this->setPassword($password);

View File

@ -34,13 +34,13 @@ final class MandrillTransportFactory extends AbstractTransportFactory
return new MandrillHttpTransport($user, $this->client, $this->dispatcher, $this->logger);
}
if ('smtp' === $scheme) {
if ('smtp' === $scheme || 'smtps' === $scheme) {
$password = $this->getPassword($dsn);
return new MandrillSmtpTransport($user, $password, $this->dispatcher, $this->logger);
}
throw new UnsupportedSchemeException($dsn, ['api', 'http', 'smtp']);
throw new UnsupportedSchemeException($dsn, ['api', 'http', 'smtp', 'smtps']);
}
public function supports(Dsn $dsn): bool

View File

@ -43,6 +43,11 @@ class MailgunTransportFactoryTest extends TransportFactoryTestCase
true,
];
yield [
new Dsn('smtps', 'mailgun'),
true,
];
yield [
new Dsn('smtp', 'example.com'),
false,
@ -74,13 +79,18 @@ class MailgunTransportFactoryTest extends TransportFactoryTestCase
new Dsn('smtp', 'mailgun', self::USER, self::PASSWORD),
new MailgunSmtpTransport(self::USER, self::PASSWORD, null, $dispatcher, $logger),
];
yield [
new Dsn('smtps', 'mailgun', self::USER, self::PASSWORD),
new MailgunSmtpTransport(self::USER, self::PASSWORD, null, $dispatcher, $logger),
];
}
public function unsupportedSchemeProvider(): iterable
{
yield [
new Dsn('foo', 'mailgun', self::USER, self::PASSWORD),
'The "foo" scheme is not supported for mailer "mailgun". Supported schemes are: "api", "http", "smtp".',
'The "foo" scheme is not supported for mailer "mailgun". Supported schemes are: "api", "http", "smtp", "smtps".',
];
}

View File

@ -22,7 +22,7 @@ class MailgunSmtpTransport extends EsmtpTransport
{
public function __construct(string $username, string $password, string $region = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
{
parent::__construct('us' !== ($region ?: 'us') ? sprintf('smtp.%s.mailgun.org', $region) : 'smtp.mailgun.org', 465, 'ssl', null, $dispatcher, $logger);
parent::__construct('us' !== ($region ?: 'us') ? sprintf('smtp.%s.mailgun.org', $region) : 'smtp.mailgun.org', 465, true, null, $dispatcher, $logger);
$this->setUsername($username);
$this->setPassword($password);

View File

@ -36,11 +36,11 @@ final class MailgunTransportFactory extends AbstractTransportFactory
return new MailgunHttpTransport($user, $password, $region, $this->client, $this->dispatcher, $this->logger);
}
if ('smtp' === $scheme) {
if ('smtp' === $scheme || 'smtps' === $scheme) {
return new MailgunSmtpTransport($user, $password, $region, $this->dispatcher, $this->logger);
}
throw new UnsupportedSchemeException($dsn, ['api', 'http', 'smtp']);
throw new UnsupportedSchemeException($dsn, ['api', 'http', 'smtp', 'smtps']);
}
public function supports(Dsn $dsn): bool

View File

@ -37,6 +37,11 @@ class PostmarkTransportFactoryTest extends TransportFactoryTestCase
true,
];
yield [
new Dsn('smtps', 'postmark'),
true,
];
yield [
new Dsn('smtp', 'example.com'),
false,
@ -57,13 +62,18 @@ class PostmarkTransportFactoryTest extends TransportFactoryTestCase
new Dsn('smtp', 'postmark', self::USER),
new PostmarkSmtpTransport(self::USER, $dispatcher, $logger),
];
yield [
new Dsn('smtps', 'postmark', self::USER),
new PostmarkSmtpTransport(self::USER, $dispatcher, $logger),
];
}
public function unsupportedSchemeProvider(): iterable
{
yield [
new Dsn('foo', 'postmark', self::USER),
'The "foo" scheme is not supported for mailer "postmark". Supported schemes are: "api", "smtp".',
'The "foo" scheme is not supported for mailer "postmark". Supported schemes are: "api", "smtp", "smtps".',
];
}

View File

@ -22,7 +22,7 @@ class PostmarkSmtpTransport extends EsmtpTransport
{
public function __construct(string $id, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
{
parent::__construct('smtp.postmarkapp.com', 587, 'tls', null, $dispatcher, $logger);
parent::__construct('smtp.postmarkapp.com', 587, true, null, $dispatcher, $logger);
$this->setUsername($id);
$this->setPassword($id);

View File

@ -30,11 +30,11 @@ final class PostmarkTransportFactory extends AbstractTransportFactory
return new PostmarkApiTransport($user, $this->client, $this->dispatcher, $this->logger);
}
if ('smtp' === $scheme) {
if ('smtp' === $scheme || 'smtps' === $scheme) {
return new PostmarkSmtpTransport($user, $this->dispatcher, $this->logger);
}
throw new UnsupportedSchemeException($dsn, ['api', 'smtp']);
throw new UnsupportedSchemeException($dsn, ['api', 'smtp', 'smtps']);
}
public function supports(Dsn $dsn): bool

View File

@ -37,6 +37,11 @@ class SendgridTransportFactoryTest extends TransportFactoryTestCase
true,
];
yield [
new Dsn('smtps', 'sendgrid'),
true,
];
yield [
new Dsn('smtp', 'example.com'),
false,
@ -57,13 +62,18 @@ class SendgridTransportFactoryTest extends TransportFactoryTestCase
new Dsn('smtp', 'sendgrid', self::USER),
new SendgridSmtpTransport(self::USER, $dispatcher, $logger),
];
yield [
new Dsn('smtps', 'sendgrid', self::USER),
new SendgridSmtpTransport(self::USER, $dispatcher, $logger),
];
}
public function unsupportedSchemeProvider(): iterable
{
yield [
new Dsn('foo', 'sendgrid', self::USER),
'The "foo" scheme is not supported for mailer "sendgrid". Supported schemes are: "api", "smtp".',
'The "foo" scheme is not supported for mailer "sendgrid". Supported schemes are: "api", "smtp", "smtps".',
];
}
}

View File

@ -22,7 +22,7 @@ class SendgridSmtpTransport extends EsmtpTransport
{
public function __construct(string $key, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
{
parent::__construct('smtp.sendgrid.net', 465, 'ssl', null, $dispatcher, $logger);
parent::__construct('smtp.sendgrid.net', 465, true, null, $dispatcher, $logger);
$this->setUsername('apikey');
$this->setPassword($key);

View File

@ -29,11 +29,11 @@ final class SendgridTransportFactory extends AbstractTransportFactory
return new SendgridApiTransport($key, $this->client, $this->dispatcher, $this->logger);
}
if ('smtp' === $dsn->getScheme()) {
if ('smtp' === $dsn->getScheme() || 'smtps' === $dsn->getScheme()) {
return new SendgridSmtpTransport($key, $this->dispatcher, $this->logger);
}
throw new UnsupportedSchemeException($dsn, ['api', 'smtp']);
throw new UnsupportedSchemeException($dsn, ['api', 'smtp', 'smtps']);
}
public function supports(Dsn $dsn): bool

View File

@ -4,6 +4,9 @@ CHANGELOG
4.4.0
-----
* STARTTLS cannot be enabled anymore (it is used automatically if TLS is disabled and the server supports STARTTLS)
* [BC BREAK] Removed the `encryption` DSN option (use `smtps` instead)
* Added support for the `smtps` protocol (does the same as using `smtp` and port `465`)
* Added PHPUnit constraints
* Added `MessageDataCollector`
* Added `MessageEvents` and `MessageLoggerListener` to allow collecting sent emails

View File

@ -69,7 +69,7 @@ abstract class TransportFactoryTestCase extends TestCase
$factory = $this->getFactory();
$this->assertEquals($transport, $factory->create($dsn));
if ('smtp' !== $dsn->getScheme()) {
if ('smtp' !== $dsn->getScheme() && 'smtps' !== $dsn->getScheme()) {
$this->assertStringMatchesFormat($dsn->getScheme().'://%S'.$dsn->getHost().'%S', $transport->getName());
}
}

View File

@ -22,6 +22,11 @@ class EsmtpTransportFactoryTest extends TransportFactoryTestCase
true,
];
yield [
new Dsn('smtps', 'example.com'),
true,
];
yield [
new Dsn('api', 'example.com'),
false,
@ -33,19 +38,33 @@ class EsmtpTransportFactoryTest extends TransportFactoryTestCase
$eventDispatcher = $this->getDispatcher();
$logger = $this->getLogger();
$transport = new EsmtpTransport('example.com', 25, null, null, $eventDispatcher, $logger);
$transport = new EsmtpTransport('localhost', 25, false, null, $eventDispatcher, $logger);
yield [
new Dsn('smtp', 'example.com'),
new Dsn('smtp', 'localhost'),
$transport,
];
$transport = new EsmtpTransport('example.com', 99, 'ssl', 'login', $eventDispatcher, $logger);
$transport = new EsmtpTransport('example.com', 99, true, 'login', $eventDispatcher, $logger);
$transport->setUsername(self::USER);
$transport->setPassword(self::PASSWORD);
yield [
new Dsn('smtp', 'example.com', self::USER, self::PASSWORD, 99, ['encryption' => 'ssl', 'auth_mode' => 'login']),
new Dsn('smtps', 'example.com', self::USER, self::PASSWORD, 99, ['auth_mode' => 'login']),
$transport,
];
$transport = new EsmtpTransport('example.com', 465, true, null, $eventDispatcher, $logger);
yield [
new Dsn('smtps', 'example.com'),
$transport,
];
$transport = new EsmtpTransport('example.com', 465, true, null, $eventDispatcher, $logger);
yield [
new Dsn('smtp', 'example.com', '', '', 465),
$transport,
];
}

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mailer\Tests\Transport\Smtp;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
class EsmtpTransportTest extends TestCase
{
public function testName()
{
$t = new EsmtpTransport();
$this->assertEquals('smtp://localhost', $t->getName());
$t = new EsmtpTransport('example.com');
if (\defined('OPENSSL_VERSION_NUMBER')) {
$this->assertEquals('smtps://example.com', $t->getName());
} else {
$this->assertEquals('smtp://example.com', $t->getName());
}
$t = new EsmtpTransport('example.com', 2525);
$this->assertEquals('smtp://example.com:2525', $t->getName());
$t = new EsmtpTransport('example.com', 0, true);
$this->assertEquals('smtps://example.com', $t->getName());
$t = new EsmtpTransport('example.com', 0, false);
$this->assertEquals('smtp://example.com', $t->getName());
$t = new EsmtpTransport('example.com', 466, true);
$this->assertEquals('smtps://example.com:466', $t->getName());
}
}

View File

@ -20,9 +20,9 @@ class SmtpTransportTest extends TestCase
public function testName()
{
$t = new SmtpTransport();
$this->assertEquals('smtp://localhost:25', $t->getName());
$this->assertEquals('smtps://localhost', $t->getName());
$t = new SmtpTransport((new SocketStream())->setHost('127.0.0.1')->setPort(2525));
$t = new SmtpTransport((new SocketStream())->setHost('127.0.0.1')->setPort(2525)->disableTls());
$this->assertEquals('smtp://127.0.0.1:2525', $t->getName());
}
}

View File

@ -37,7 +37,6 @@ class SocketStreamTest extends TestCase
'cafile' => __FILE__,
],
]);
$s->setEncryption('ssl');
$s->setHost('smtp.gmail.com');
$s->setPort(465);
$s->initialize();

View File

@ -31,7 +31,7 @@ class EsmtpTransport extends SmtpTransport
private $password = '';
private $authMode;
public function __construct(string $host = 'localhost', int $port = 25, string $encryption = null, string $authMode = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
public function __construct(string $host = 'localhost', int $port = 0, bool $tls = null, string $authMode = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
{
parent::__construct(null, $dispatcher, $logger);
@ -44,11 +44,23 @@ class EsmtpTransport extends SmtpTransport
/** @var SocketStream $stream */
$stream = $this->getStream();
if (null === $tls) {
if (465 === $port) {
$tls = true;
} else {
$tls = \defined('OPENSSL_VERSION_NUMBER') && 0 === $port && 'localhost' !== $host;
}
}
if (!$tls) {
$stream->disableTls();
}
if (0 === $port) {
$port = $tls ? 465 : 25;
}
$stream->setHost($host);
$stream->setPort($port);
if (null !== $encryption) {
$stream->setEncryption($encryption);
}
if (null !== $authMode) {
$this->setAuthMode($authMode);
}
@ -105,13 +117,15 @@ class EsmtpTransport extends SmtpTransport
return;
}
$capabilities = $this->getCapabilities($response);
/** @var SocketStream $stream */
$stream = $this->getStream();
if ($stream->isTLS()) {
if (!$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $capabilities)) {
$this->executeCommand("STARTTLS\r\n", [220]);
if (!$stream->startTLS()) {
throw new TransportException('Unable to connect with TLS encryption.');
throw new TransportException('Unable to connect with STARTTLS.');
}
try {
@ -123,7 +137,6 @@ class EsmtpTransport extends SmtpTransport
}
}
$capabilities = $this->getCapabilities($response);
if (\array_key_exists('AUTH', $capabilities)) {
$this->handleAuth($capabilities['AUTH']);
}

View File

@ -22,12 +22,12 @@ final class EsmtpTransportFactory extends AbstractTransportFactory
{
public function create(Dsn $dsn): TransportInterface
{
$encryption = $dsn->getOption('encryption');
$tls = 'smtps' === $dsn->getScheme() ? true : null;
$authMode = $dsn->getOption('auth_mode');
$port = $dsn->getPort(25);
$port = $dsn->getPort(0);
$host = $dsn->getHost();
$transport = new EsmtpTransport($host, $port, $encryption, $authMode, $this->dispatcher, $this->logger);
$transport = new EsmtpTransport($host, $port, $tls, $authMode, $this->dispatcher, $this->logger);
if ($user = $dsn->getUser()) {
$transport->setUsername($user);
@ -42,6 +42,6 @@ final class EsmtpTransportFactory extends AbstractTransportFactory
public function supports(Dsn $dsn): bool
{
return 'smtp' === $dsn->getScheme();
return 'smtp' === $dsn->getScheme() || 'smtps' === $dsn->getScheme();
}
}

View File

@ -129,7 +129,13 @@ class SmtpTransport extends AbstractTransport
public function getName(): string
{
if ($this->stream instanceof SocketStream) {
return sprintf('smtp://%s:%d', $this->stream->getHost(), $this->stream->getPort());
$name = sprintf('smtp%s://%s', ($tls = $this->stream->isTLS()) ? 's' : '', $this->stream->getHost());
$port = $this->stream->getPort();
if (!(25 === $port || ($tls && 465 === $port))) {
$name .= ':'.$port;
}
return $name;
}
return sprintf('smtp://sendmail');

View File

@ -25,10 +25,9 @@ final class SocketStream extends AbstractStream
{
private $url;
private $host = 'localhost';
private $protocol = 'tcp';
private $port = 25;
private $port = 465;
private $timeout = 15;
private $tls = false;
private $tls = true;
private $sourceIp;
private $streamContextOptions = [];
@ -72,18 +71,11 @@ final class SocketStream extends AbstractStream
}
/**
* Sets the encryption type (tls or ssl).
* Sets the TLS/SSL on the socket (disables STARTTLS).
*/
public function setEncryption(string $encryption): self
public function disableTls(): self
{
$encryption = strtolower($encryption);
if ('tls' === $encryption) {
$this->protocol = 'tcp';
$this->tls = true;
} else {
$this->protocol = $encryption;
$this->tls = false;
}
$this->tls = false;
return $this;
}
@ -128,8 +120,8 @@ final class SocketStream extends AbstractStream
public function initialize(): void
{
$this->url = $this->host.':'.$this->port;
if ($this->protocol) {
$this->url = $this->protocol.'://'.$this->url;
if ($this->tls) {
$this->url = 'ssl://'.$this->url;
}
$options = [];
if ($this->sourceIp) {
@ -138,9 +130,8 @@ final class SocketStream extends AbstractStream
if ($this->streamContextOptions) {
$options = array_merge($options, $this->streamContextOptions);
}
if ($this->isTLS()) {
$options['ssl']['crypto_method'] = $options['ssl']['crypto_method'] ?? STREAM_CRYPTO_METHOD_TLS_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
}
// do it unconditionnally as it will be used by STARTTLS as well if supported
$options['ssl']['crypto_method'] = $options['ssl']['crypto_method'] ?? STREAM_CRYPTO_METHOD_TLS_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
$streamContext = stream_context_create($options);
set_error_handler(function ($type, $msg) {