diff --git a/sources/Application/TwigBase/Twig/Extension.php b/sources/Application/TwigBase/Twig/Extension.php index 2c5ebeffae..18721f9af1 100644 --- a/sources/Application/TwigBase/Twig/Extension.php +++ b/sources/Application/TwigBase/Twig/Extension.php @@ -153,8 +153,12 @@ class Extension return twig_array_filter($oTwigEnv, $array, $arrow); }, ['needs_environment' => true]); - // @since 3.3.0 N°8579 - // Filter to remove spaces between HTML tags, overwrite the deprecated core "spaceless" filter + /** + * Filter to remove spaces between HTML tags, overwrite the deprecated core "spaceless" filter + * Usage in twig: {% apply spaceless %}some html{% endapply %} + * + * @since 3.2.3 3.3.0 N°8579 + */ $aFilters[] = new TwigFilter('spaceless', function (?string $content) { return trim(preg_replace('/>\s+<', $content ?? '')); }, ['is_safe' => ['html']]); diff --git a/sources/Core/Email/EmailSymfony.php b/sources/Core/Email/EmailSymfony.php index 1d29aec200..fbb0120343 100644 --- a/sources/Core/Email/EmailSymfony.php +++ b/sources/Core/Email/EmailSymfony.php @@ -29,6 +29,7 @@ use Symfony\Component\CssSelector\Exception\ParseException; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Mailer; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; use Symfony\Component\Mime\Email as SymfonyEmail; use Symfony\Component\Mime\HtmlToTextConverter\DefaultHtmlToTextConverter; use Symfony\Component\Mime\Part\DataPart; @@ -184,18 +185,7 @@ class EMailSymfony extends Email $sDsn = sprintf('smtp://%s%s@%s%s', $sDsnUser, $sDsnPassword, $sDsnPort, $sEncQuery); } - $oTransport = Transport::fromDsn($sDsn); - - // Handle peer verification - $oStream = $oTransport->getStream(); - $aOptions = $oStream->getStreamOptions(); - if (!$bVerifyPeer && array_key_exists('ssl', $aOptions)) { - // Disable verification - $aOptions['ssl']['verify_peer'] = false; - $aOptions['ssl']['verify_peer_name'] = false; - $aOptions['ssl']['allow_self_signed'] = true; - } - $oStream->setStreamOptions($aOptions); + $oTransport = $this->CreateSmtpTransport($sDsn, $bVerifyPeer); $oMailer = new Mailer($oTransport); break; @@ -261,6 +251,36 @@ class EMailSymfony extends Email } } + /** + * Build and configure an SMTP transport from a DSN string. + * + * Extracted from {@see SendSynchronous} to make SSL option handling independently testable. + * When $bVerifyPeer is false, the ssl stream context options must be written unconditionally: + * with STARTTLS the connection starts unencrypted, so the 'ssl' key is absent from the stream + * options at construction time and only used later when stream_socket_enable_crypto() is called. + * + * @param string $sDsn Full Symfony Mailer DSN (smtp:// or smtps://) + * @param bool $bVerifyPeer Whether to verify the peer SSL certificate + * + * @return \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport + */ + protected function CreateSmtpTransport(string $sDsn, bool $bVerifyPeer): EsmtpTransport + { + /** @var EsmtpTransport $oTransport */ + $oTransport = Transport::fromDsn($sDsn); + + $oStream = $oTransport->getStream(); + $aOptions = $oStream->getStreamOptions(); + if (!$bVerifyPeer) { + $aOptions['ssl']['verify_peer'] = false; + $aOptions['ssl']['verify_peer_name'] = false; + $aOptions['ssl']['allow_self_signed'] = true; + } + $oStream->setStreamOptions($aOptions); + + return $oTransport; + } + /** * Reprocess the body of the message (if it is an HTML message) * to replace the URL of images based on attachments by a link diff --git a/tests/php-unit-tests/unitary-tests/sources/core/Email/EmailSymfonyTest.php b/tests/php-unit-tests/unitary-tests/sources/core/Email/EmailSymfonyTest.php index 1a289798d5..2cefa18742 100644 --- a/tests/php-unit-tests/unitary-tests/sources/core/Email/EmailSymfonyTest.php +++ b/tests/php-unit-tests/unitary-tests/sources/core/Email/EmailSymfonyTest.php @@ -2,6 +2,7 @@ use Combodo\iTop\Core\Email\EMailSymfony; use Combodo\iTop\Test\UnitTest\ItopTestCase; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; use Symfony\Component\Mime\Part\DataPart; use Symfony\Component\Mime\Part\Multipart\AlternativePart; use Symfony\Component\Mime\Part\Multipart\RelatedPart; @@ -298,4 +299,63 @@ HTML; ], ]; } + + /** + * @dataProvider provideCreateSmtpTransportSslOptions + */ + public function testCreateSmtpTransportSslOptions(string $sDsn, bool $bVerifyPeer, array $aExpectedSslOptions): void + { + $oEmail = new EMailSymfony(); + /** @var EsmtpTransport $oTransport */ + $oTransport = $this->InvokeNonPublicMethod(EMailSymfony::class, 'CreateSmtpTransport', $oEmail, [$sDsn, $bVerifyPeer]); + + $aActualSslOptions = $oTransport->getStream()->getStreamOptions()['ssl'] ?? []; + + $this->assertSame($aExpectedSslOptions, $aActualSslOptions); + } + + public function provideCreateSmtpTransportSslOptions(): array + { + $aDisabledVerification = [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, + ]; + + return [ + // Regression scenario (N°9584): STARTTLS starts the connection unencrypted, so the 'ssl' key + // is absent from stream options at construction time. verify_peer=false must still be applied. + 'STARTTLS, verify_peer=false' => [ + 'smtp://localhost:587?encryption=starttls', + false, + $aDisabledVerification, + ], + 'implicit TLS (smtps), verify_peer=false' => [ + 'smtps://localhost:465', + false, + $aDisabledVerification, + ], + 'plain SMTP, verify_peer=false' => [ + 'smtp://localhost:25', + false, + $aDisabledVerification, + ], + // Default behavior: verify_peer=true must leave stream options untouched (empty). + 'STARTTLS, verify_peer=true (default)' => [ + 'smtp://localhost:587?encryption=starttls', + true, + [], + ], + 'implicit TLS (smtps), verify_peer=true (default)' => [ + 'smtps://localhost:465', + true, + [], + ], + 'plain SMTP, verify_peer=true (default)' => [ + 'smtp://localhost:25', + true, + [], + ], + ]; + } }