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
This commit is contained in:
Darko Gjorgjijoski
2025-08-31 03:04:31 +02:00
committed by GitHub
parent d1bca362de
commit bae8dbe083
10 changed files with 233 additions and 53 deletions

View File

@@ -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=

View File

@@ -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

View File

@@ -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');

View File

@@ -34,7 +34,7 @@ class MailEnvironmentRequest extends FormRequest
'required',
],
'mail_encryption' => [
'required',
'nullable',
'string',
],
'from_name' => [

View File

@@ -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'),

View File

@@ -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"
},

18
composer.lock generated
View File

@@ -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",

View File

@@ -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'),
],
];

View File

@@ -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: '',
},

View File

@@ -93,21 +93,14 @@
<BaseInputGroup
:label="$t('settings.mail.encryption')"
:content-loading="isFetchingInitialData"
:error="
v$.smtpConfig.mail_encryption.$error &&
v$.smtpConfig.mail_encryption.$errors[0].$message
"
required
>
<BaseMultiselect
v-model.trim="mailDriverStore.smtpConfig.mail_encryption"
:content-loading="isFetchingInitialData"
:options="encryptions"
:options="schemes"
:searchable="true"
:show-labels="false"
placeholder="Select option"
:invalid="v$.smtpConfig.mail_encryption.$error"
@input="v$.smtpConfig.mail_encryption.$touch()"
/>
</BaseInputGroup>
@@ -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),