From a93d5f6cb7960746f27f060f039fb53816c28912 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 20 Jan 2025 11:29:21 +0000 Subject: [PATCH] [Mailer] [Smtp] Add DSN param to enforce TLS/STARTTLS Adds 'require_tls' param which can be set to true to enforce the use of TLS/STARTTLS within the ESMTP transport. --- src/Symfony/Component/Mailer/CHANGELOG.md | 1 + .../Smtp/EsmtpTransportFactoryTest.php | 12 ++++++++++ .../Transport/Smtp/EsmtpTransportTest.php | 17 ++++++++++++++ .../Mailer/Transport/Smtp/EsmtpTransport.php | 22 +++++++++++++++++++ .../Transport/Smtp/EsmtpTransportFactory.php | 1 + 5 files changed, 53 insertions(+) diff --git a/src/Symfony/Component/Mailer/CHANGELOG.md b/src/Symfony/Component/Mailer/CHANGELOG.md index f876701168c59..3816cc474948b 100644 --- a/src/Symfony/Component/Mailer/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Add DSN param `retry_period` to override default email transport retry period * Add `Dsn::getBooleanOption()` * Add DSN param `source_ip` to allow binding to a (specific) IPv4 or IPv6 address. + * Add DSN param `require_tls` to enforce use of TLS/STARTTLS * Add `DkimSignedMessageListener`, `SmimeEncryptedMessageListener`, and `SmimeSignedMessageListener` 7.2 diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportFactoryTest.php b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportFactoryTest.php index 1fbb20ba22694..b43a9c8bbd362 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportFactoryTest.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportFactoryTest.php @@ -194,6 +194,18 @@ public static function createProvider(): iterable Dsn::fromString('smtps://:@example.com:465?source_ip=[2606:4700:20::681a:5fb]'), $transport, ]; + + $transport = new EsmtpTransport('example.com', 465, true, null, $logger); + $transport->setRequireTls(true); + + yield [ + new Dsn('smtps', 'example.com', '', '', 465, ['require_tls' => true]), + $transport, + ]; + yield [ + Dsn::fromString('smtps://:@example.com?require_tls=true'), + $transport, + ]; } public static function unsupportedSchemeProvider(): iterable diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php index 9b4eacbf1b7f0..ae336939cda74 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/EsmtpTransportTest.php @@ -297,6 +297,23 @@ public function testSocketTimeout() $stream->getCommands() ); } + + public function testRequireTls() + { + $stream = new DummyStream(); + $transport = new EsmtpTransport(stream: $stream); + $transport->setRequireTls(true); + + $message = new Email(); + $message->from('sender@example.org'); + $message->addTo('recipient@example.org'); + $message->text('.'); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage('TLS required but neither TLS or STARTTLS are in use.'); + + $transport->send($message); + } } class CustomEsmtpTransport extends EsmtpTransport diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php index 3663dc6f5604c..1c332d677aaa1 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php @@ -33,6 +33,7 @@ class EsmtpTransport extends SmtpTransport private string $password = ''; private array $capabilities; private bool $autoTls = true; + private bool $requireTls = false; public function __construct(string $host = 'localhost', int $port = 0, ?bool $tls = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null, ?AbstractStream $stream = null, ?array $authenticators = null) { @@ -116,6 +117,21 @@ public function isAutoTls(): bool return $this->autoTls; } + /** + * @return $this + */ + public function setRequireTls(bool $requireTls): static + { + $this->requireTls = $requireTls; + + return $this; + } + + public function isTlsRequired(): bool + { + return $this->requireTls; + } + public function setAuthenticators(array $authenticators): void { $this->authenticators = []; @@ -159,6 +175,7 @@ private function doEhloCommand(): string /** @var SocketStream $stream */ $stream = $this->getStream(); + $tlsStarted = $stream->isTls(); // WARNING: !$stream->isTLS() is right, 100% sure :) // if you think that the ! should be removed, read the code again // if doing so "fixes" your issue then it probably means your SMTP server behaves incorrectly or is wrongly configured @@ -169,10 +186,15 @@ private function doEhloCommand(): string throw new TransportException('Unable to connect with STARTTLS.'); } + $tlsStarted = true; $response = $this->executeCommand(\sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); $this->capabilities = $this->parseCapabilities($response); } + if (!$tlsStarted && $this->isTlsRequired()) { + throw new TransportException('TLS required but neither TLS or STARTTLS are in use.'); + } + if (\array_key_exists('AUTH', $this->capabilities)) { $this->handleAuth($this->capabilities['AUTH']); } diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransportFactory.php b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransportFactory.php index 17869353128af..acfa2c4ef2b1e 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransportFactory.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransportFactory.php @@ -35,6 +35,7 @@ public function create(Dsn $dsn): TransportInterface $transport = new EsmtpTransport($host, $port, $tls, $this->dispatcher, $this->logger); $transport->setAutoTls($autoTls); + $transport->setRequireTls($dsn->getBooleanOption('require_tls')); /** @var SocketStream $stream */ $stream = $transport->getStream();