mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 02:24:11 -05:00
fix(email): support custom SMTP EHLO domains (#11398)
This commit is contained in:
@@ -6,6 +6,7 @@ use App\Livewire\Notifications\Concerns\TogglesNotificationEvents;
|
||||
use App\Models\EmailNotificationSettings;
|
||||
use App\Models\Team;
|
||||
use App\Notifications\Test;
|
||||
use App\Rules\ValidHostname;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Livewire\Attributes\Locked;
|
||||
@@ -57,6 +58,9 @@ class Email extends Component
|
||||
#[Validate(['nullable', 'numeric'])]
|
||||
public ?string $smtpTimeout = null;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $smtpEhloDomain = null;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $resendEnabled = false;
|
||||
|
||||
@@ -129,6 +133,7 @@ class Email extends Component
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
$this->validate(['smtpEhloDomain' => ['nullable', 'string', new ValidHostname]]);
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->settings->smtp_enabled = $this->smtpEnabled;
|
||||
$this->settings->smtp_from_address = $this->smtpFromAddress;
|
||||
@@ -140,6 +145,7 @@ class Email extends Component
|
||||
$this->settings->smtp_username = $this->smtpUsername;
|
||||
$this->settings->smtp_password = $this->smtpPassword;
|
||||
$this->settings->smtp_timeout = $this->smtpTimeout;
|
||||
$this->settings->smtp_ehlo_domain = $this->smtpEhloDomain;
|
||||
|
||||
$this->settings->resend_enabled = $this->resendEnabled;
|
||||
$this->settings->resend_api_key = $this->resendApiKey;
|
||||
@@ -175,6 +181,7 @@ class Email extends Component
|
||||
? $this->settings->smtp_password
|
||||
: null;
|
||||
$this->smtpTimeout = $this->settings->smtp_timeout;
|
||||
$this->smtpEhloDomain = $this->settings->smtp_ehlo_domain;
|
||||
|
||||
$this->resendEnabled = $this->settings->resend_enabled;
|
||||
$this->resendApiKey = auth()->user()->can('update', $this->settings)
|
||||
@@ -261,6 +268,7 @@ class Email extends Component
|
||||
'smtpUsername' => 'nullable|string',
|
||||
'smtpPassword' => 'nullable|string',
|
||||
'smtpTimeout' => 'nullable|numeric',
|
||||
'smtpEhloDomain' => ['nullable', 'string', new ValidHostname],
|
||||
], [
|
||||
'smtpFromAddress.required' => 'From Address is required.',
|
||||
'smtpFromAddress.email' => 'Please enter a valid email address.',
|
||||
@@ -284,6 +292,7 @@ class Email extends Component
|
||||
$this->settings->smtp_username = $this->smtpUsername;
|
||||
$this->settings->smtp_password = $this->smtpPassword;
|
||||
$this->settings->smtp_timeout = $this->smtpTimeout;
|
||||
$this->settings->smtp_ehlo_domain = $this->smtpEhloDomain;
|
||||
|
||||
$this->settings->save();
|
||||
$this->dispatch('success', 'SMTP settings updated.');
|
||||
@@ -375,6 +384,7 @@ class Email extends Component
|
||||
$this->smtpUsername = $settings->smtp_username;
|
||||
$this->smtpPassword = $settings->smtp_password;
|
||||
$this->smtpTimeout = $settings->smtp_timeout;
|
||||
$this->smtpEhloDomain = $settings->smtp_ehlo_domain;
|
||||
|
||||
if ($settings->resend_enabled) {
|
||||
$this->resendEnabled = true;
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Livewire;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Notifications\TransactionalEmails\Test;
|
||||
use App\Rules\ValidHostname;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Livewire\Attributes\Locked;
|
||||
@@ -50,6 +51,9 @@ class SettingsEmail extends Component
|
||||
#[Validate(['nullable', 'numeric'])]
|
||||
public ?string $smtpTimeout = null;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $smtpEhloDomain = null;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $resendEnabled = false;
|
||||
|
||||
@@ -74,6 +78,7 @@ class SettingsEmail extends Component
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
$this->validate(['smtpEhloDomain' => ['nullable', 'string', new ValidHostname]]);
|
||||
$this->settings->smtp_enabled = $this->smtpEnabled;
|
||||
$this->settings->smtp_host = $this->smtpHost;
|
||||
$this->settings->smtp_port = $this->smtpPort;
|
||||
@@ -81,6 +86,7 @@ class SettingsEmail extends Component
|
||||
$this->settings->smtp_username = $this->smtpUsername;
|
||||
$this->settings->smtp_password = $this->smtpPassword;
|
||||
$this->settings->smtp_timeout = $this->smtpTimeout;
|
||||
$this->settings->smtp_ehlo_domain = $this->smtpEhloDomain;
|
||||
$this->settings->smtp_from_address = $this->smtpFromAddress;
|
||||
$this->settings->smtp_from_name = $this->smtpFromName;
|
||||
|
||||
@@ -95,6 +101,7 @@ class SettingsEmail extends Component
|
||||
$this->smtpUsername = $this->settings->smtp_username;
|
||||
$this->smtpPassword = $this->settings->smtp_password;
|
||||
$this->smtpTimeout = $this->settings->smtp_timeout;
|
||||
$this->smtpEhloDomain = $this->settings->smtp_ehlo_domain;
|
||||
$this->smtpFromAddress = $this->settings->smtp_from_address;
|
||||
$this->smtpFromName = $this->settings->smtp_from_name;
|
||||
|
||||
@@ -167,6 +174,7 @@ class SettingsEmail extends Component
|
||||
'smtpUsername' => 'nullable|string',
|
||||
'smtpPassword' => 'nullable|string',
|
||||
'smtpTimeout' => 'nullable|numeric',
|
||||
'smtpEhloDomain' => ['nullable', 'string', new ValidHostname],
|
||||
], [
|
||||
'smtpFromAddress.required' => 'From Address is required.',
|
||||
'smtpFromAddress.email' => 'Please enter a valid email address.',
|
||||
@@ -184,6 +192,7 @@ class SettingsEmail extends Component
|
||||
$this->settings->smtp_username = $this->smtpUsername;
|
||||
$this->settings->smtp_password = $this->smtpPassword;
|
||||
$this->settings->smtp_timeout = $this->smtpTimeout;
|
||||
$this->settings->smtp_ehlo_domain = $this->smtpEhloDomain;
|
||||
$this->settings->smtp_from_address = $this->smtpFromAddress;
|
||||
$this->settings->smtp_from_name = $this->smtpFromName;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ class EmailNotificationSettings extends Model
|
||||
'smtp_username',
|
||||
'smtp_password',
|
||||
'smtp_timeout',
|
||||
'smtp_ehlo_domain',
|
||||
|
||||
'resend_enabled',
|
||||
'resend_api_key',
|
||||
|
||||
@@ -33,6 +33,7 @@ class InstanceSettings extends Model
|
||||
'smtp_username',
|
||||
'smtp_password',
|
||||
'smtp_timeout',
|
||||
'smtp_ehlo_domain',
|
||||
'resend_enabled',
|
||||
'resend_api_key',
|
||||
'is_dns_validation_enabled',
|
||||
|
||||
@@ -82,7 +82,10 @@ class EmailChannel
|
||||
'html' => (string) $mailMessage->render(),
|
||||
]);
|
||||
} elseif ($isSmtpEnabled) {
|
||||
$transport = SmtpTransportFactory::fromSettings($settings);
|
||||
$transport = SmtpTransportFactory::fromSettings(
|
||||
$settings,
|
||||
config('mail.mailers.smtp.local_domain')
|
||||
);
|
||||
$mailer = new Mailer($transport);
|
||||
|
||||
$email = (new Email)
|
||||
|
||||
@@ -29,6 +29,8 @@ class ConfigurationRepository
|
||||
|
||||
if ($settings->smtp_enabled) {
|
||||
$mailerOptions = SmtpTransportFactory::mailerOptions($settings);
|
||||
$localDomain = $settings->smtp_ehlo_domain
|
||||
?? $this->config->get('mail.mailers.smtp.local_domain');
|
||||
|
||||
$this->config->set('mail.default', 'smtp');
|
||||
$this->applyMailFrom($from);
|
||||
@@ -41,7 +43,7 @@ class ConfigurationRepository
|
||||
'username' => $settings->smtp_username,
|
||||
'password' => $settings->smtp_password,
|
||||
'timeout' => $settings->smtp_timeout,
|
||||
'local_domain' => null,
|
||||
'local_domain' => $localDomain,
|
||||
'auto_tls' => $mailerOptions['auto_tls'],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
|
||||
|
||||
class SmtpTransportFactory
|
||||
{
|
||||
public static function fromSettings(object $settings): EsmtpTransport
|
||||
public static function fromSettings(object $settings, ?string $localDomain = null): EsmtpTransport
|
||||
{
|
||||
$mode = self::encryptionMode($settings);
|
||||
|
||||
@@ -28,6 +28,11 @@ class SmtpTransportFactory
|
||||
$transport->setUsername($settings->smtp_username ?? '');
|
||||
$transport->setPassword($settings->smtp_password ?? '');
|
||||
|
||||
$localDomain = $settings->smtp_ehlo_domain ?? $localDomain;
|
||||
if ($localDomain !== null && $localDomain !== '') {
|
||||
$transport->setLocalDomain($localDomain);
|
||||
}
|
||||
|
||||
$stream = $transport->getStream();
|
||||
if (isset($settings->smtp_timeout) && $stream instanceof SocketStream) {
|
||||
$stream->setTimeout((float) $settings->smtp_timeout);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->string('smtp_ehlo_domain')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('email_notification_settings', function (Blueprint $table) {
|
||||
$table->string('smtp_ehlo_domain')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('smtp_ehlo_domain');
|
||||
});
|
||||
|
||||
Schema::table('email_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('smtp_ehlo_domain');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -113,6 +113,10 @@
|
||||
@endcan
|
||||
<x-forms.input canGate="update" :canResource="$settings" id="smtpTimeout" type="number"
|
||||
helper="Timeout value for sending emails." label="Timeout" />
|
||||
<x-forms.input canGate="update" :canResource="$settings" id="smtpEhloDomain"
|
||||
placeholder="coolify.example.com"
|
||||
helper="Fully qualified domain sent in the SMTP EHLO command. Uses the system default when empty."
|
||||
label="EHLO domain" />
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{{-- One bar for the whole page. Three stacked bars made Save run
|
||||
submitResend(), which required an API key even when Resend was off. --}}
|
||||
<x-unsaved-bar action="submit"
|
||||
targets="smtpFromName,smtpFromAddress,smtpHost,smtpPort,smtpEncryption,smtpUsername,smtpPassword,smtpTimeout,resendApiKey" />
|
||||
targets="smtpFromName,smtpFromAddress,smtpHost,smtpPort,smtpEncryption,smtpUsername,smtpPassword,smtpTimeout,smtpEhloDomain,resendApiKey" />
|
||||
|
||||
<form wire:submit="submit">
|
||||
<x-application.settings-section title="Sender">
|
||||
@@ -48,6 +48,9 @@
|
||||
autocomplete="new-password" />
|
||||
<x-forms.input id="smtpTimeout" type="number"
|
||||
helper="Maximum delivery time in seconds." label="Timeout" />
|
||||
<x-forms.input id="smtpEhloDomain" placeholder="coolify.example.com"
|
||||
helper="Fully qualified domain sent in the SMTP EHLO command. Uses the system default when empty."
|
||||
label="EHLO domain" />
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Notifications\Email as NotificationEmail;
|
||||
use App\Livewire\SettingsEmail;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
@@ -34,6 +35,7 @@ function smtpSetupPayload(): array
|
||||
'smtpHost' => 'smtp.example.com',
|
||||
'smtpPort' => '587',
|
||||
'smtpEncryption' => 'starttls',
|
||||
'smtpEhloDomain' => 'coolify.example.com',
|
||||
'resendEnabled' => false,
|
||||
'resendApiKey' => null,
|
||||
];
|
||||
@@ -56,9 +58,30 @@ test('saving smtp settings does not require a resend api key when resend is disa
|
||||
|
||||
expect($settings->smtp_enabled)->toBeTrue()
|
||||
->and($settings->smtp_host)->toBe('smtp.example.com')
|
||||
->and($settings->smtp_ehlo_domain)->toBe('coolify.example.com')
|
||||
->and($settings->resend_enabled)->toBeFalse();
|
||||
});
|
||||
|
||||
test('team smtp settings save their own ehlo domain', function () {
|
||||
setupInstanceAdminForEmailSettings();
|
||||
$team = Team::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$team->members()->attach($user->id, ['role' => 'admin']);
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $team]);
|
||||
|
||||
Livewire::test(NotificationEmail::class)
|
||||
->fill(smtpSetupPayload())
|
||||
->set('smtpEnabled', true)
|
||||
->call('submitSmtp')
|
||||
->assertHasNoErrors()
|
||||
->assertNotDispatched('error');
|
||||
|
||||
expect($team->emailNotificationSettings->fresh()->smtp_ehlo_domain)
|
||||
->toBe('coolify.example.com');
|
||||
});
|
||||
|
||||
test('saving transactional email settings does not require a resend api key when resend is disabled', function () {
|
||||
$user = setupInstanceAdminForEmailSettings();
|
||||
|
||||
|
||||
@@ -27,3 +27,29 @@ it('configures the laravel smtp scheme from encryption', function (string $mode,
|
||||
'none on port 465' => ['none', 465, 'smtp', null, '0'],
|
||||
'tls on a non-465 port' => ['tls', 587, 'smtps', 'tls', ''],
|
||||
]);
|
||||
|
||||
it('preserves the configured smtp ehlo domain', function () {
|
||||
$config = new Repository([
|
||||
'mail' => [
|
||||
'mailers' => [
|
||||
'smtp' => ['local_domain' => 'coolify.example.com'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$repository = new ConfigurationRepository($config);
|
||||
|
||||
$repository->updateMailConfig((object) [
|
||||
'resend_enabled' => false,
|
||||
'smtp_enabled' => true,
|
||||
'smtp_encryption' => 'starttls',
|
||||
'smtp_host' => 'smtp-relay.gmail.com',
|
||||
'smtp_port' => 587,
|
||||
'smtp_username' => null,
|
||||
'smtp_password' => null,
|
||||
'smtp_timeout' => null,
|
||||
'smtp_from_address' => 'from@example.com',
|
||||
'smtp_from_name' => 'Coolify',
|
||||
]);
|
||||
|
||||
expect($config->get('mail.mailers.smtp.local_domain'))->toBe('coolify.example.com');
|
||||
});
|
||||
|
||||
@@ -86,6 +86,24 @@ it('applies a configured SMTP timeout to the transport stream', function () {
|
||||
expect($transport->getStream()->getTimeout())->toBe(15.0);
|
||||
});
|
||||
|
||||
it('applies the configured ehlo domain to the transport', function () {
|
||||
$transport = SmtpTransportFactory::fromSettings(
|
||||
smtpSettings(['smtp_ehlo_domain' => 'team.example.com']),
|
||||
'instance.example.com'
|
||||
);
|
||||
|
||||
expect($transport->getLocalDomain())->toBe('team.example.com');
|
||||
});
|
||||
|
||||
it('falls back to the instance ehlo domain for legacy smtp settings', function () {
|
||||
$transport = SmtpTransportFactory::fromSettings(
|
||||
smtpSettings(),
|
||||
'instance.example.com'
|
||||
);
|
||||
|
||||
expect($transport->getLocalDomain())->toBe('instance.example.com');
|
||||
});
|
||||
|
||||
it('maps none encryption to laravel mailer options that disable auto tls', function () {
|
||||
expect(SmtpTransportFactory::mailerOptions(smtpSettings([
|
||||
'smtp_encryption' => 'none',
|
||||
|
||||
Reference in New Issue
Block a user