clusev/app/Livewire/Settings/Email.php

236 lines
8.5 KiB
PHP

<?php
namespace App\Livewire\Settings;
use App\Models\AuditEvent;
use App\Models\Setting;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Livewire\Component;
use Throwable;
/**
* SMTP configuration card. The operator points the panel at an SMTP server so
* password-reset mails (and future notifications) actually leave the box. Values
* are stored as Setting rows and applied to runtime config by AppServiceProvider::boot().
*
* The SMTP password is the one secret here: it is stored ENCRYPTED at rest
* (Crypt::encryptString, APP_KEY) — Setting::put stores plaintext, so we encrypt the
* value ourselves — never rendered back to the browser, and never logged/echoed
* (any error from the test-send is shown without the password).
*/
class Email extends Component
{
public string $mail_host = '';
public int $mail_port = 587;
public string $mail_username = '';
/** New password typed in the form. Empty = keep the stored one (never pre-filled). */
public string $mail_password = '';
public string $mail_encryption = 'tls';
public string $mail_from_address = '';
public string $mail_from_name = '';
/** True once a password has been stored, so the field shows a "(set)" placeholder. */
public bool $passwordSet = false;
public function mount(): void
{
abort_unless(auth()->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'));
}
/**
* Fully clear the SMTP configuration — removes every mail_* setting (incl. the encrypted
* password) so the panel reverts to the safe default (log) mailer. Reversible: just reconfigure.
*/
public function clearConfig(): void
{
abort_unless(auth()->user()?->can('manage-panel'), 403);
foreach (['mail_host', 'mail_port', 'mail_username', 'mail_password', 'mail_encryption', 'mail_from_address', 'mail_from_name'] as $key) {
Setting::forget($key);
}
$this->mail_host = '';
$this->mail_port = 587;
$this->mail_username = '';
$this->mail_password = '';
$this->mail_encryption = 'tls';
$this->mail_from_address = '';
$this->mail_from_name = '';
$this->passwordSet = false;
AuditEvent::create([
'user_id' => Auth::id(),
'actor' => Auth::user()->name,
'action' => 'mail.reset',
'target' => '—',
'ip' => request()->ip(),
]);
$this->dispatch('notify', message: __('mail.notify_reset'));
}
/**
* 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
{
abort_unless(auth()->user()?->can('manage-panel'), 403);
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');
}
}