diff --git a/app/Config/mail.php b/app/Config/mail.php index 038864f8c..7256ce884 100644 --- a/app/Config/mail.php +++ b/app/Config/mail.php @@ -11,6 +11,7 @@ // Configured mail encryption method. // STARTTLS should still be attempted, but tls/ssl forces TLS usage. $mailEncryption = env('MAIL_ENCRYPTION', null); +$mailPort = intval(env('MAIL_PORT', 587)); return [ @@ -33,13 +34,13 @@ return [ 'transport' => 'smtp', 'scheme' => null, 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), - 'port' => env('MAIL_PORT', 587), + 'port' => $mailPort, 'username' => env('MAIL_USERNAME'), 'password' => env('MAIL_PASSWORD'), 'verify_peer' => env('MAIL_VERIFY_SSL', true), 'timeout' => null, 'local_domain' => null, - 'tls_required' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl'), + 'require_tls' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl' || $mailPort === 465), ], 'sendmail' => [ diff --git a/composer.json b/composer.json index 09f08daee..2ea1d802b 100644 --- a/composer.json +++ b/composer.json @@ -38,8 +38,7 @@ "socialiteproviders/microsoft-azure": "^5.1", "socialiteproviders/okta": "^4.2", "socialiteproviders/twitch": "^5.3", - "ssddanbrown/htmldiff": "^2.0.0", - "ssddanbrown/symfony-mailer": "7.2.x-dev" + "ssddanbrown/htmldiff": "^2.0.0" }, "require-dev": { "fakerphp/faker": "^1.21", diff --git a/composer.lock b/composer.lock index c4f1a1638..1f7cbf884 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d9b8455cf3ec02c21bea7ee94463331b", + "content-hash": "b7695cb9945ec550970c67da96934daf", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.349.2", + "version": "3.349.3", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "63cc727845f077d17cb94791deb327249e1626ce" + "reference": "b2d4718786398f47626add9c29840fc416175ef2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/63cc727845f077d17cb94791deb327249e1626ce", - "reference": "63cc727845f077d17cb94791deb327249e1626ce", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b2d4718786398f47626add9c29840fc416175ef2", + "reference": "b2d4718786398f47626add9c29840fc416175ef2", "shasum": "" }, "require": { @@ -153,9 +153,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.349.2" + "source": "https://github.com/aws/aws-sdk-php/tree/3.349.3" }, - "time": "2025-07-03T18:08:27+00:00" + "time": "2025-07-09T18:10:17+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1955,16 +1955,16 @@ }, { "name": "laravel/prompts", - "version": "v0.3.5", + "version": "v0.3.6", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1" + "reference": "86a8b692e8661d0fb308cec64f3d176821323077" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/57b8f7efe40333cdb925700891c7d7465325d3b1", - "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1", + "url": "https://api.github.com/repos/laravel/prompts/zipball/86a8b692e8661d0fb308cec64f3d176821323077", + "reference": "86a8b692e8661d0fb308cec64f3d176821323077", "shasum": "" }, "require": { @@ -2008,9 +2008,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.5" + "source": "https://github.com/laravel/prompts/tree/v0.3.6" }, - "time": "2025-02-11T13:34:40+00:00" + "time": "2025-07-07T14:17:42+00:00" }, { "name": "laravel/serializable-closure", @@ -4894,16 +4894,16 @@ }, { "name": "sabberworm/php-css-parser", - "version": "v8.8.0", + "version": "v8.9.0", "source": { "type": "git", "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", - "reference": "3de493bdddfd1f051249af725c7e0d2c38fed740" + "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/3de493bdddfd1f051249af725c7e0d2c38fed740", - "reference": "3de493bdddfd1f051249af725c7e0d2c38fed740", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9", + "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9", "shasum": "" }, "require": { @@ -4911,7 +4911,8 @@ "php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" }, "require-dev": { - "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41" + "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41", + "rawr/cross-data-providers": "^2.0.0" }, "suggest": { "ext-mbstring": "for parsing UTF-8 CSS" @@ -4953,9 +4954,9 @@ ], "support": { "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", - "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.8.0" + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0" }, - "time": "2025-03-23T17:59:05+00:00" + "time": "2025-07-11T13:20:48+00:00" }, { "name": "socialiteproviders/discord", @@ -5325,80 +5326,6 @@ ], "time": "2025-07-07T11:55:59+00:00" }, - { - "name": "ssddanbrown/symfony-mailer", - "version": "7.2.x-dev", - "source": { - "type": "git", - "url": "https://github.com/ssddanbrown/symfony-mailer.git", - "reference": "e9de8dccd76a63fc23475016e6574da6f5f12a2d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ssddanbrown/symfony-mailer/zipball/e9de8dccd76a63fc23475016e6574da6f5f12a2d", - "reference": "e9de8dccd76a63fc23475016e6574da6f5f12a2d", - "shasum": "" - }, - "require": { - "egulias/email-validator": "^2.1.10|^3|^4", - "php": ">=8.2", - "psr/event-dispatcher": "^1", - "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/mime": "^7.2", - "symfony/service-contracts": "^2.5|^3" - }, - "conflict": { - "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<6.4", - "symfony/messenger": "<6.4", - "symfony/mime": "<6.4", - "symfony/twig-bridge": "<6.4" - }, - "replace": { - "symfony/mailer": "^7.0" - }, - "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/twig-bridge": "^6.4|^7.0" - }, - "default-branch": true, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Mailer\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Dan Brown", - "homepage": "https://danb.me" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Helps sending emails", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/ssddanbrown/symfony-mailer/tree/7.2" - }, - "time": "2025-01-11T14:57:07+00:00" - }, { "name": "symfony/clock", "version": "v7.3.0", @@ -6189,6 +6116,86 @@ ], "time": "2025-06-28T08:24:55+00:00" }, + { + "name": "symfony/mailer", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b5db5105b290bdbea5ab27b89c69effcf1cb3368", + "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/mime": "^7.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T19:55:54+00:00" + }, { "name": "symfony/mime", "version": "v7.3.0", @@ -8973,16 +8980,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.26", + "version": "11.5.27", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4ad8fe263a0b55b54a8028c38a18e3c5bef312e0" + "reference": "446d43867314781df7e9adf79c3ec7464956fd8f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4ad8fe263a0b55b54a8028c38a18e3c5bef312e0", - "reference": "4ad8fe263a0b55b54a8028c38a18e3c5bef312e0", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/446d43867314781df7e9adf79c3ec7464956fd8f", + "reference": "446d43867314781df7e9adf79c3ec7464956fd8f", "shasum": "" }, "require": { @@ -8992,7 +8999,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.1", + "myclabs/deep-copy": "^1.13.3", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", @@ -9054,7 +9061,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.26" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.27" }, "funding": [ { @@ -9078,7 +9085,7 @@ "type": "tidelift" } ], - "time": "2025-07-04T05:58:21+00:00" + "time": "2025-07-11T04:10:06+00:00" }, { "name": "sebastian/cli-parser", @@ -10318,9 +10325,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "ssddanbrown/symfony-mailer": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/tests/SecurityHeaderTest.php b/tests/SecurityHeaderTest.php index 5d354e553..fe98e3208 100644 --- a/tests/SecurityHeaderTest.php +++ b/tests/SecurityHeaderTest.php @@ -17,7 +17,7 @@ class SecurityHeaderTest extends TestCase public function test_cookies_samesite_none_when_iframe_hosts_set() { - $this->runWithEnv('ALLOWED_IFRAME_HOSTS', 'http://example.com', function () { + $this->runWithEnv(['ALLOWED_IFRAME_HOSTS' => 'http://example.com'], function () { $resp = $this->get('/'); foreach ($resp->headers->getCookies() as $cookie) { $this->assertEquals('none', $cookie->getSameSite()); @@ -27,14 +27,14 @@ class SecurityHeaderTest extends TestCase public function test_secure_cookies_controlled_by_app_url() { - $this->runWithEnv('APP_URL', 'http://example.com', function () { + $this->runWithEnv(['APP_URL' => 'http://example.com'], function () { $resp = $this->get('/'); foreach ($resp->headers->getCookies() as $cookie) { $this->assertFalse($cookie->isSecure()); } }); - $this->runWithEnv('APP_URL', 'https://example.com', function () { + $this->runWithEnv(['APP_URL' => 'https://example.com'], function () { $resp = $this->get('/'); foreach ($resp->headers->getCookies() as $cookie) { $this->assertTrue($cookie->isSecure()); @@ -52,7 +52,7 @@ class SecurityHeaderTest extends TestCase public function test_iframe_csp_includes_extra_hosts_if_configured() { - $this->runWithEnv('ALLOWED_IFRAME_HOSTS', 'https://a.example.com https://b.example.com', function () { + $this->runWithEnv(['ALLOWED_IFRAME_HOSTS' => 'https://a.example.com https://b.example.com'], function () { $resp = $this->get('/'); $frameHeader = $this->getCspHeader($resp, 'frame-ancestors'); diff --git a/tests/TestCase.php b/tests/TestCase.php index 0fb899ea9..a8636fb15 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -118,15 +118,18 @@ abstract class TestCase extends BaseTestCase * Database config is juggled so the value can be restored when * parallel testing are used, where multiple databases exist. */ - protected function runWithEnv(string $name, $value, callable $callback, bool $handleDatabase = true) + protected function runWithEnv(array $valuesByKey, callable $callback, bool $handleDatabase = true): void { Env::disablePutenv(); - $originalVal = $_SERVER[$name] ?? null; + $originals = []; + foreach ($valuesByKey as $key => $value) { + $originals[$key] = $_SERVER[$key] ?? null; - if (is_null($value)) { - unset($_SERVER[$name]); - } else { - $_SERVER[$name] = $value; + if (is_null($value)) { + unset($_SERVER[$key]); + } else { + $_SERVER[$key] = $value; + } } $database = config('database.connections.mysql_testing.database'); @@ -144,10 +147,12 @@ abstract class TestCase extends BaseTestCase DB::rollBack(); } - if (is_null($originalVal)) { - unset($_SERVER[$name]); - } else { - $_SERVER[$name] = $originalVal; + foreach ($originals as $key => $value) { + if (is_null($value)) { + unset($_SERVER[$key]); + } else { + $_SERVER[$key] = $value; + } } } diff --git a/tests/ThemeTest.php b/tests/ThemeTest.php index b3c85d8f7..4dff38418 100644 --- a/tests/ThemeTest.php +++ b/tests/ThemeTest.php @@ -46,7 +46,7 @@ class ThemeTest extends TestCase $functionsFile = theme_path('functions.php'); app()->alias('cat', 'dog'); file_put_contents($functionsFile, "alias('cat', 'dog');});"); - $this->runWithEnv('APP_THEME', $themeFolder, function () { + $this->runWithEnv(['APP_THEME' => $themeFolder], function () { $this->assertEquals('cat', $this->app->getAlias('dog')); }); }); @@ -61,7 +61,7 @@ class ThemeTest extends TestCase $this->expectException(ThemeException::class); $this->expectExceptionMessageMatches('/Failed loading theme functions file at ".*?" with error: Class "BookStack\\\\Biscuits" not found/'); - $this->runWithEnv('APP_THEME', $themeFolder, fn() => null); + $this->runWithEnv(['APP_THEME' => $themeFolder], fn() => null); }); } @@ -504,7 +504,7 @@ END; $this->beforeApplicationDestroyed(fn() => File::deleteDirectory($themeFolderPath)); // Run provided callback with theme env option set - $this->runWithEnv('APP_THEME', $themeFolderName, function () use ($callback, $themeFolderName) { + $this->runWithEnv(['APP_THEME' => $themeFolderName], function () use ($callback, $themeFolderName) { call_user_func($callback, $themeFolderName); }); } diff --git a/tests/Unit/ConfigTest.php b/tests/Unit/ConfigTest.php index 2883fddb8..7795a861a 100644 --- a/tests/Unit/ConfigTest.php +++ b/tests/Unit/ConfigTest.php @@ -16,7 +16,7 @@ class ConfigTest extends TestCase { public function test_filesystem_images_falls_back_to_storage_type_var() { - $this->runWithEnv('STORAGE_TYPE', 'local_secure', function () { + $this->runWithEnv(['STORAGE_TYPE' => 'local_secure'], function () { $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', 's3', 'filesystems.images', 's3'); $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', null, 'filesystems.images', 'local_secure'); }); @@ -24,7 +24,7 @@ class ConfigTest extends TestCase public function test_filesystem_attachments_falls_back_to_storage_type_var() { - $this->runWithEnv('STORAGE_TYPE', 'local_secure', function () { + $this->runWithEnv(['STORAGE_TYPE' => 'local_secure'], function () { $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', 's3', 'filesystems.attachments', 's3'); $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', null, 'filesystems.attachments', 'local_secure'); }); @@ -114,7 +114,7 @@ class ConfigTest extends TestCase $this->assertEmpty($getStreamOptions()); - $this->runWithEnv('MAIL_VERIFY_SSL', 'false', function () use ($getStreamOptions) { + $this->runWithEnv(['MAIL_VERIFY_SSL' => 'false'], function () use ($getStreamOptions) { $options = $getStreamOptions(); $this->assertArrayHasKey('ssl', $options); $this->assertFalse($options['ssl']['verify_peer']); @@ -124,9 +124,9 @@ class ConfigTest extends TestCase public function test_non_null_mail_encryption_options_enforce_smtp_scheme() { - $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'tls', 'mail.mailers.smtp.tls_required', true); - $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'ssl', 'mail.mailers.smtp.tls_required', true); - $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'null', 'mail.mailers.smtp.tls_required', false); + $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'tls', 'mail.mailers.smtp.require_tls', true); + $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'ssl', 'mail.mailers.smtp.require_tls', true); + $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'null', 'mail.mailers.smtp.require_tls', false); } public function test_smtp_scheme_and_certain_port_forces_tls_usage() @@ -135,29 +135,18 @@ class ConfigTest extends TestCase /** @var EsmtpTransport $transport */ $transport = Mail::mailer('smtp')->getSymfonyTransport(); Mail::purge('smtp'); - return $transport->getTlsRequirement(); + return $transport->isTlsRequired(); }; - config()->set([ - 'mail.mailers.smtp.tls_required' => null, - 'mail.mailers.smtp.port' => 587, - ]); + $runTest = function (string $tlsOption, int $port, bool $expectedResult) use ($isMailTlsRequired) { + $this->runWithEnv(['MAIL_ENCRYPTION' => $tlsOption, 'MAIL_PORT' => $port], function () use ($isMailTlsRequired, $port, $expectedResult) { + $this->assertEquals($expectedResult, $isMailTlsRequired()); + }); + }; - $this->assertFalse($isMailTlsRequired()); - - config()->set([ - 'mail.mailers.smtp.tls_required' => 'tls', - 'mail.mailers.smtp.port' => 587, - ]); - - $this->assertTrue($isMailTlsRequired()); - - config()->set([ - 'mail.mailers.smtp.tls_required' => null, - 'mail.mailers.smtp.port' => 465, - ]); - - $this->assertTrue($isMailTlsRequired()); + $runTest('null', 587, false); + $runTest('tls', 587, true); + $runTest('null', 465, true); } public function test_mysql_host_parsed_as_expected() @@ -174,7 +163,7 @@ class ConfigTest extends TestCase ]; foreach ($cases as $host => [$expectedHost, $expectedPort]) { - $this->runWithEnv("DB_HOST", $host, function () use ($expectedHost, $expectedPort) { + $this->runWithEnv(["DB_HOST" => $host], function () use ($expectedHost, $expectedPort) { $this->assertEquals($expectedHost, config("database.connections.mysql.host")); $this->assertEquals($expectedPort, config("database.connections.mysql.port")); }, false); @@ -185,12 +174,10 @@ class ConfigTest extends TestCase * Set an environment variable of the given name and value * then check the given config key to see if it matches the given result. * Providing a null $envVal clears the variable. - * - * @param mixed $expectedResult */ - protected function checkEnvConfigResult(string $envName, ?string $envVal, string $configKey, $expectedResult) + protected function checkEnvConfigResult(string $envName, ?string $envVal, string $configKey, mixed $expectedResult): void { - $this->runWithEnv($envName, $envVal, function () use ($configKey, $expectedResult) { + $this->runWithEnv([$envName => $envVal], function () use ($configKey, $expectedResult) { $this->assertEquals($expectedResult, config($configKey)); }); } diff --git a/tests/UrlTest.php b/tests/UrlTest.php index c1e133804..c1a4d4f1c 100644 --- a/tests/UrlTest.php +++ b/tests/UrlTest.php @@ -8,14 +8,14 @@ class UrlTest extends TestCase { public function test_url_helper_takes_custom_url_into_account() { - $this->runWithEnv('APP_URL', 'http://example.com/bookstack', function () { + $this->runWithEnv(['APP_URL' => 'http://example.com/bookstack'], function () { $this->assertEquals('http://example.com/bookstack/books', url('/books')); }); } public function test_url_helper_sets_correct_scheme_even_when_request_scheme_is_different() { - $this->runWithEnv('APP_URL', 'https://example.com/', function () { + $this->runWithEnv(['APP_URL' => 'https://example.com/'], function () { $this->get('http://example.com/login')->assertSee('https://example.com/dist/styles.css'); }); }