user()?->can('manage-panel'), 403); $this->mail_host = Setting::get('mail_host', '') ?? ''; $this->mail_port = (int) (Setting::get('mail_port', '587') ?? 587); $this->mail_username = Setting::get('mail_username', '') ?? ''; $this->mail_encryption = Setting::get('mail_encryption', 'tls') ?? 'tls'; $this->mail_from_address = Setting::get('mail_from_address', '') ?? ''; $this->mail_from_name = Setting::get('mail_from_name', '') ?? ''; $this->passwordSet = (Setting::get('mail_password') ?? '') !== ''; } protected function rules(): array { return [ 'mail_host' => ['required', 'string', 'max:255'], 'mail_port' => ['required', 'integer', 'min:1', 'max:65535'], 'mail_username' => ['nullable', 'string', 'max:255'], 'mail_password' => ['nullable', 'string', 'max:255'], 'mail_encryption' => ['required', 'in:none,tls,ssl'], 'mail_from_address' => ['required', 'email', 'max:255'], 'mail_from_name' => ['required', 'string', 'max:255'], ]; } public function save(): void { abort_unless(auth()->user()?->can('manage-panel'), 403); $this->validate(); Setting::put('mail_host', $this->mail_host); Setting::put('mail_port', (string) $this->mail_port); Setting::put('mail_username', $this->mail_username); Setting::put('mail_encryption', $this->mail_encryption); Setting::put('mail_from_address', $this->mail_from_address); Setting::put('mail_from_name', $this->mail_from_name); // Only overwrite the stored password when a new one was typed; otherwise keep // the existing (encrypted) value untouched. Store encrypted at rest. if ($this->mail_password !== '') { Setting::put('mail_password', Crypt::encryptString($this->mail_password)); $this->passwordSet = true; } $this->reset('mail_password'); AuditEvent::create([ 'user_id' => Auth::id(), 'actor' => Auth::user()->name, 'action' => 'mail.configure', 'target' => $this->mail_host, 'ip' => request()->ip(), ]); $this->dispatch('notify', message: __('mail.notify_saved')); } /** * Send a one-line test mail to the CURRENT user's own address, using a mailer built * from the saved settings. Never targets anyone else; never leaks the password — any * transport error is surfaced as a message with the secret masked out. */ public function sendTest(): void { if (! $this->configured()) { $this->dispatch('notify', message: __('mail.notify_test_failed', ['error' => __('mail.not_configured')])); return; } // Throttle test-sends so the panel can't be turned into a spam/mail-bomb relay. // Per-user + auto-expiring: only ever limits the operator's own test button briefly. $key = 'mail-test:'.Auth::id(); if (RateLimiter::tooManyAttempts($key, 3)) { $this->dispatch('notify', message: __('mail.notify_test_throttled', ['seconds' => RateLimiter::availableIn($key)]), level: 'error'); return; } RateLimiter::hit($key, 600); // 3 test e-mails / 10 minutes per user $to = Auth::user()->email; try { $this->applyMailerConfig(); Mail::raw(__('mail.test_body'), function ($message) use ($to) { $message->to($to)->subject(__('mail.test_subject')); }); $this->dispatch('notify', message: __('mail.notify_test_ok', ['email' => $to])); } catch (Throwable $e) { $this->dispatch('notify', message: __('mail.notify_test_failed', [ 'error' => $this->maskSecret(Str::limit($e->getMessage(), 160)), ])); } } /** Host + from-address present — the minimum to actually send. */ public function configured(): bool { return $this->mail_host !== '' && $this->mail_from_address !== ''; } /** * Apply the saved SMTP settings to runtime config for the duration of this request, * so the test-send uses exactly what the operator configured (decrypting the stored * password). Mirrors AppServiceProvider's runtime override. */ private function applyMailerConfig(): void { $password = ''; $stored = Setting::get('mail_password'); if ($stored !== null && $stored !== '') { try { $password = Crypt::decryptString($stored); } catch (Throwable) { $password = ''; } } config([ 'mail.default' => 'smtp', 'mail.mailers.smtp.host' => $this->mail_host, 'mail.mailers.smtp.port' => $this->mail_port, 'mail.mailers.smtp.username' => $this->mail_username ?: null, 'mail.mailers.smtp.password' => $password ?: null, 'mail.mailers.smtp.encryption' => $this->mail_encryption === 'none' ? null : $this->mail_encryption, 'mail.from.address' => $this->mail_from_address, 'mail.from.name' => $this->mail_from_name, ]); // Drop any mailer Laravel already resolved this request so the new config takes effect. Mail::purge('smtp'); } /** Strip the stored SMTP password out of any string before it is shown/logged. */ private function maskSecret(string $text): string { $stored = Setting::get('mail_password'); if ($stored === null || $stored === '') { return $text; } try { $plain = Crypt::decryptString($stored); } catch (Throwable) { return $text; } return $plain !== '' ? str_replace($plain, '****', $text) : $text; } public function render() { return view('livewire.settings.email'); } }