From bae8dbe083558c1d3febc2a5112cfb42135acb94 Mon Sep 17 00:00:00 2001 From: Darko Gjorgjijoski <5760249+gdarko@users.noreply.github.com> Date: Sun, 31 Aug 2025 03:04:31 +0200 Subject: [PATCH] Upgrade mail configuration (#455) * Upgrade the mail configuration * Update mail configuration to match Laravel 12 * Update mail configuration to properly set none or null * Pint code * Upgrade Symfony Mailers --- .env.example | 2 +- .env.testing | 2 +- .../Settings/MailConfigurationController.php | 89 +++++++++++-- app/Http/Requests/MailEnvironmentRequest.php | 2 +- app/Space/EnvironmentManager.php | 34 +++-- composer.json | 1 + composer.lock | 18 ++- config/mail.php | 121 ++++++++++++++++-- resources/scripts/admin/stores/mail-driver.js | 3 +- .../settings/mail-driver/SmtpMailDriver.vue | 14 +- 10 files changed, 233 insertions(+), 53 deletions(-) diff --git a/.env.example b/.env.example index c78d9e93..14639de9 100644 --- a/.env.example +++ b/.env.example @@ -35,7 +35,7 @@ REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 -MAIL_DRIVER=smtp +MAIL_MAILER=smtp MAIL_HOST= MAIL_PORT= MAIL_USERNAME= diff --git a/.env.testing b/.env.testing index 4fd02cf3..48933cca 100644 --- a/.env.testing +++ b/.env.testing @@ -3,7 +3,7 @@ APP_DEBUG=true APP_KEY=base64:IdDlpLmYyWA9z4Ruj5st1FSYrhCR7lPOscLGCz2Jf4I= DB_CONNECTION=sqlite -MAIL_DRIVER=smtp +MAIL_MAILER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=587 MAIL_USERNAME=ff538f0e1037f4 diff --git a/app/Http/Controllers/V1/Admin/Settings/MailConfigurationController.php b/app/Http/Controllers/V1/Admin/Settings/MailConfigurationController.php index a0de314b..a89f4d50 100755 --- a/app/Http/Controllers/V1/Admin/Settings/MailConfigurationController.php +++ b/app/Http/Controllers/V1/Admin/Settings/MailConfigurationController.php @@ -7,6 +7,7 @@ use App\Http\Requests\MailEnvironmentRequest; use App\Mail\TestMail; use App\Models\Setting; use App\Space\EnvironmentManager; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Mail; @@ -14,17 +15,26 @@ use Mail; class MailConfigurationController extends Controller { /** + * The environment manager + * * @var EnvironmentManager */ protected $environmentManager; + /** + * The constructor + */ public function __construct(EnvironmentManager $environmentManager) { $this->environmentManager = $environmentManager; } /** + * Save the mail environment variables + * * @return JsonResponse + * + * @throws AuthorizationException */ public function saveMailEnvironment(MailEnvironmentRequest $request) { @@ -40,32 +50,79 @@ class MailConfigurationController extends Controller return response()->json($results); } + /** + * Return the mail environment variables + * + * @return JsonResponse + * + * @throws AuthorizationException + */ public function getMailEnvironment() { $this->authorize('manage email config'); + $driver = config('mail.default'); + + // Base data that's always available $MailData = [ - 'mail_driver' => config('mail.driver'), - 'mail_host' => config('mail.host'), - 'mail_port' => config('mail.port'), - 'mail_username' => config('mail.username'), - 'mail_password' => config('mail.password'), - 'mail_encryption' => is_null(config('mail.encryption')) ? 'none' : config('mail.encryption'), + 'mail_driver' => $driver, 'from_name' => config('mail.from.name'), 'from_mail' => config('mail.from.address'), - 'mail_mailgun_endpoint' => config('services.mailgun.endpoint'), - 'mail_mailgun_domain' => config('services.mailgun.domain'), - 'mail_mailgun_secret' => config('services.mailgun.secret'), - 'mail_ses_key' => config('services.ses.key'), - 'mail_ses_secret' => config('services.ses.secret'), - 'mail_ses_region' => config('services.ses.region'), ]; + // Driver-specific configuration + switch ($driver) { + case 'smtp': + $MailData = array_merge($MailData, [ + 'mail_host' => config('mail.mailers.smtp.host'), + 'mail_port' => config('mail.mailers.smtp.port'), + 'mail_username' => config('mail.mailers.smtp.username'), + 'mail_password' => config('mail.mailers.smtp.password'), + 'mail_encryption' => config('mail.mailers.smtp.scheme') ?? 'none', + 'mail_scheme' => config('mail.mailers.smtp.scheme'), + 'mail_url' => config('mail.mailers.smtp.url'), + 'mail_timeout' => config('mail.mailers.smtp.timeout'), + 'mail_local_domain' => config('mail.mailers.smtp.local_domain'), + ]); + break; + + case 'mailgun': + $MailData = array_merge($MailData, [ + 'mail_mailgun_domain' => config('mail.mailers.mailgun.domain'), + 'mail_mailgun_secret' => config('mail.mailers.mailgun.secret'), + 'mail_mailgun_endpoint' => config('mail.mailers.mailgun.endpoint'), + 'mail_mailgun_scheme' => config('mail.mailers.mailgun.scheme'), + ]); + break; + + case 'ses': + $MailData = array_merge($MailData, [ + 'mail_ses_key' => config('services.ses.key'), + 'mail_ses_secret' => config('services.ses.secret'), + 'mail_ses_region' => config('services.ses.region'), + ]); + break; + + case 'sendmail': + $MailData = array_merge($MailData, [ + 'mail_sendmail_path' => config('mail.mailers.sendmail.path'), + ]); + break; + + default: + // For unknown drivers, return minimal configuration + break; + } + return response()->json($MailData); } /** + * Return the available mail drivers + * * @return JsonResponse + * + * @throws AuthorizationException */ public function getMailDrivers() { @@ -82,6 +139,14 @@ class MailConfigurationController extends Controller return response()->json($drivers); } + /** + * Test the email configuration + * + * @return JsonResponse + * + * @throws AuthorizationException + * @throws \Illuminate\Validation\ValidationException + */ public function testEmailConfig(Request $request) { $this->authorize('manage email config'); diff --git a/app/Http/Requests/MailEnvironmentRequest.php b/app/Http/Requests/MailEnvironmentRequest.php index 242162c6..2dfc783b 100644 --- a/app/Http/Requests/MailEnvironmentRequest.php +++ b/app/Http/Requests/MailEnvironmentRequest.php @@ -34,7 +34,7 @@ class MailEnvironmentRequest extends FormRequest 'required', ], 'mail_encryption' => [ - 'required', + 'nullable', 'string', ], 'from_name' => [ diff --git a/app/Space/EnvironmentManager.php b/app/Space/EnvironmentManager.php index 2e3aea14..9e7a209c 100755 --- a/app/Space/EnvironmentManager.php +++ b/app/Space/EnvironmentManager.php @@ -26,7 +26,7 @@ class EnvironmentManager /** * Set the .env and .env.example paths. */ - public function __construct() + public function __construct($path = null) { $this->envPath = base_path('.env'); } @@ -64,7 +64,7 @@ class EnvironmentManager // Check if new or old key if ($entry[0] == $data_key) { - $env[$env_key] = $data_key.'='.$this->encode($data_value); + $env[$env_key] = sprintf('%s=%s', $data_key, $this->encode($data_value)); $updated = true; } } @@ -89,8 +89,25 @@ class EnvironmentManager */ private function encode($str) { + // Convert to string if not already + $str = (string) $str; - if ((strpos($str, ' ') !== false || preg_match('/'.preg_quote('^\'£$%^&*()}{@#~?><,@|-=-_+-¬', '/').'/', $str)) && ($str[0] != '"' || $str[strlen($str) - 1] != '"')) { + // If the value is already properly quoted, return as is + if (strlen($str) >= 2 && $str[0] === '"' && $str[strlen($str) - 1] === '"') { + return $str; + } + + // Check if the value contains characters that need quoting + // Using a character class regex to properly match special characters + $specialChars = '\^\'£$%&*()}{@#~?><,|=\-_+¬!'; + $needsQuoting = ( + strpos($str, ' ') !== false || + preg_match('/['.preg_quote($specialChars, '/').']/', $str) + ); + + if ($needsQuoting) { + // Escape any existing double quotes in the string + $str = str_replace('"', '\\"', $str); $str = '"'.$str.'"'; } @@ -314,12 +331,12 @@ class EnvironmentManager case 'smtp': $mailEnv = [ - 'MAIL_DRIVER' => $request->get('mail_driver'), + 'MAIL_MAILER' => $request->get('mail_driver'), 'MAIL_HOST' => $request->get('mail_host'), 'MAIL_PORT' => $request->get('mail_port'), 'MAIL_USERNAME' => $request->get('mail_username'), 'MAIL_PASSWORD' => $request->get('mail_password'), - 'MAIL_ENCRYPTION' => $request->get('mail_encryption') !== 'none' ? $request->get('mail_encryption') : 'null', + 'MAIL_SCHEME' => $request->get('mail_encryption') !== 'none' ? $request->get('mail_encryption') : 'null', 'MAIL_FROM_ADDRESS' => $request->get('from_mail'), 'MAIL_FROM_NAME' => $request->get('from_name'), ]; @@ -329,12 +346,11 @@ class EnvironmentManager case 'mailgun': $mailEnv = [ - 'MAIL_DRIVER' => $request->get('mail_driver'), + 'MAIL_MAILER' => $request->get('mail_driver'), 'MAIL_HOST' => $request->get('mail_host'), 'MAIL_PORT' => $request->get('mail_port'), 'MAIL_USERNAME' => config('mail.username'), 'MAIL_PASSWORD' => config('mail.password'), - 'MAIL_ENCRYPTION' => $request->get('mail_encryption'), 'MAIL_FROM_ADDRESS' => $request->get('from_mail'), 'MAIL_FROM_NAME' => $request->get('from_name'), 'MAILGUN_DOMAIN' => $request->get('mail_mailgun_domain'), @@ -347,7 +363,7 @@ class EnvironmentManager case 'ses': $mailEnv = [ - 'MAIL_DRIVER' => $request->get('mail_driver'), + 'MAIL_MAILER' => $request->get('mail_driver'), 'MAIL_HOST' => $request->get('mail_host'), 'MAIL_PORT' => $request->get('mail_port'), 'MAIL_USERNAME' => config('mail.username'), @@ -366,7 +382,7 @@ class EnvironmentManager case 'mail': $mailEnv = [ - 'MAIL_DRIVER' => $request->get('mail_driver'), + 'MAIL_MAILER' => $request->get('mail_driver'), 'MAIL_HOST' => config('mail.host'), 'MAIL_PORT' => config('mail.port'), 'MAIL_USERNAME' => config('mail.username'), diff --git a/composer.json b/composer.json index d2ad8313..d509b81d 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "spatie/flysystem-dropbox": "^3.0", "spatie/laravel-backup": "^9.2.9", "spatie/laravel-medialibrary": "^11.11", + "symfony/mailer": "^7.3", "symfony/mailgun-mailer": "^7.3", "vinkla/hashids": "^13.0.0" }, diff --git a/composer.lock b/composer.lock index 60dc54a6..e9d9bf05 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": "55a74107ca176346bd153f73881910bb", + "content-hash": "a4ddf34cf22d150b595d8259e4152dd2", "packages": [ { "name": "aws/aws-crt-php", @@ -8383,16 +8383,16 @@ }, { "name": "symfony/mailer", - "version": "v7.2.6", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "998692469d6e698c6eadc7ef37a6530a9eabb356" + "reference": "a32f3f45f1990db8c4341d5122a7d3a381c7e575" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/998692469d6e698c6eadc7ef37a6530a9eabb356", - "reference": "998692469d6e698c6eadc7ef37a6530a9eabb356", + "url": "https://api.github.com/repos/symfony/mailer/zipball/a32f3f45f1990db8c4341d5122a7d3a381c7e575", + "reference": "a32f3f45f1990db8c4341d5122a7d3a381c7e575", "shasum": "" }, "require": { @@ -8443,7 +8443,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.2.6" + "source": "https://github.com/symfony/mailer/tree/v7.3.3" }, "funding": [ { @@ -8454,12 +8454,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-04T09:50:51+00:00" + "time": "2025-08-13T11:49:31+00:00" }, { "name": "symfony/mailgun-mailer", diff --git a/config/mail.php b/config/mail.php index 49981d05..5e683700 100644 --- a/config/mail.php +++ b/config/mail.php @@ -2,20 +2,125 @@ return [ - 'driver' => env('MAIL_DRIVER', 'smtp'), + /* + |-------------------------------------------------------------------------- + | Default Mailer + |-------------------------------------------------------------------------- + | + | This option controls the default mailer that is used to send all email + | messages unless another mailer is explicitly specified when sending + | the message. All additional mailers can be configured within the + | "mailers" array. Examples of each type of mailer are provided. + | + */ - 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'default' => env('MAIL_MAILER', 'log'), - 'port' => env('MAIL_PORT', 587), + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ - 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'mailers' => [ - 'username' => env('MAIL_USERNAME'), + 'smtp' => [ + 'transport' => 'smtp', + 'encryption' => env('MAIL_ENCRYPTION', 'none'), + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], - 'password' => env('MAIL_PASSWORD'), + 'mailgun' => [ + 'domain' => env('MAILGUN_DOMAIN'), + 'secret' => env('MAILGUN_SECRET'), + 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), + 'scheme' => 'https', + ], - 'sendmail' => '/usr/sbin/sendmail -bs', + 'ses' => [ + 'transport' => 'ses', + ], - 'log_channel' => env('MAIL_LOG_CHANNEL'), + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@invoiceshelf.com'), + 'name' => env('MAIL_FROM_NAME', 'InvoiceShelf'), + ], ]; diff --git a/resources/scripts/admin/stores/mail-driver.js b/resources/scripts/admin/stores/mail-driver.js index 41a0971a..31b9b544 100644 --- a/resources/scripts/admin/stores/mail-driver.js +++ b/resources/scripts/admin/stores/mail-driver.js @@ -38,7 +38,6 @@ export const useMailDriverStore = (useWindow = false) => { mail_ses_key: '', mail_ses_secret: '', mail_ses_region: '', - mail_encryption: 'tls', from_mail: '', from_name: '', }, @@ -49,7 +48,7 @@ export const useMailDriverStore = (useWindow = false) => { mail_port: null, mail_username: '', mail_password: '', - mail_encryption: 'tls', + mail_encryption: '', from_mail: '', from_name: '', }, diff --git a/resources/scripts/admin/views/settings/mail-driver/SmtpMailDriver.vue b/resources/scripts/admin/views/settings/mail-driver/SmtpMailDriver.vue index 80af5dde..49ab5501 100644 --- a/resources/scripts/admin/views/settings/mail-driver/SmtpMailDriver.vue +++ b/resources/scripts/admin/views/settings/mail-driver/SmtpMailDriver.vue @@ -93,21 +93,14 @@ @@ -204,7 +197,7 @@ const mailDriverStore = useMailDriverStore() const { t } = useI18n() let isShowPassword = ref(false) -const encryptions = reactive(['none','tls', 'ssl', 'starttls']) +const schemes = reactive(['smtp', 'smtps', 'none']) const getInputType = computed(() => { if (isShowPassword.value) { @@ -226,9 +219,6 @@ const rules = computed(() => { required: helpers.withMessage(t('validation.required'), required), numeric: helpers.withMessage(t('validation.numbers_only'), numeric), }, - mail_encryption: { - required: helpers.withMessage(t('validation.required'), required), - }, from_mail: { required: helpers.withMessage(t('validation.required'), required), email: helpers.withMessage(t('validation.email_incorrect'), email),