CluPilotCloud/app/Livewire/EditMailbox.php

131 lines
4.7 KiB
PHP

<?php
namespace App\Livewire;
use App\Livewire\Concerns\ConfirmsPassword;
use App\Models\Mailbox;
use App\Services\Mail\MailPurpose;
use App\Services\Secrets\SecretCipher;
use App\Support\Settings;
use LivewireUI\Modal\ModalComponent;
/**
* Editing a mailbox, in a modal (R20).
*
* A modal is reachable WITHOUT the page's route middleware, so it authorises
* itself and re-reads the record instead of trusting a property the browser
* hydrated. save() re-authorises independently of mount() for the same reason
* Secrets::guard() checks on every action rather than once: a Livewire action
* is reachable by anyone who can post to /livewire/update, and a mount that
* succeeded earlier in the session proves nothing about the request now.
*/
class EditMailbox extends ModalComponent
{
use ConfirmsPassword;
public string $uuid = '';
public string $address = '';
public string $displayName = '';
public string $username = '';
/** Always starts empty: a stored password never travels to the browser. */
public string $password = '';
public bool $noReply = false;
public bool $active = true;
public function mount(string $uuid): void
{
$this->authorize('mail.manage');
$box = Mailbox::query()->where('uuid', $uuid)->firstOrFail();
$this->uuid = $box->uuid;
$this->address = $box->address;
$this->displayName = (string) $box->display_name;
$this->username = (string) $box->username;
$this->noReply = $box->no_reply;
$this->active = $box->active;
}
public function save(): void
{
$this->authorize('mail.manage');
$this->validate([
'address' => ['required', 'email', 'max:255'],
'displayName' => ['nullable', 'string', 'max:255'],
'username' => ['nullable', 'string', 'max:255'],
'password' => ['nullable', 'string', 'max:255'],
]);
$box = Mailbox::query()->where('uuid', $this->uuid)->firstOrFail();
// Deactivating the mailbox "system" currently points at is not just
// this purpose losing its sender: MailboxResolver::active() filters an
// inactive mailbox at BOTH the direct lookup AND the system fallback,
// so every OTHER purpose that has no mailbox of its own falls through
// to nothing too. savePurposes() already refuses to leave system
// empty; this closes the other way to reach the same broken state.
if (! $this->active && $box->key === (string) Settings::get(MailPurpose::settingKey(MailPurpose::SYSTEM))) {
$this->addError('active', __('mail_settings.cannot_deactivate_system'));
return;
}
// Setting a new SMTP password is one of the two actions the
// mail.manage split still leaves able to intercept mail outright (the
// other is Admin\Mail::saveServer()) — gated the same second way
// Admin\Secrets is: a capability decides who may open this modal at
// all, a recent password decides whether THIS session may point the
// outgoing account at new credentials.
if ($this->password !== '') {
abort_unless($this->passwordRecentlyConfirmed(), 403);
// A password can only be STORED where SECRETS_KEY exists to
// encrypt it under — the same condition the page's own banner
// already names. Checked here rather than left for
// Mailbox::setPasswordAttribute() to throw through: an uncaught
// RuntimeException would render Laravel's debug page, whose
// request-payload inspector can show the very password just
// typed. The page must say so, not crash on it.
if (! app(SecretCipher::class)->isUsable()) {
$this->addError('password', __('mail_settings.no_key'));
return;
}
}
$box->address = $this->address;
$box->display_name = $this->displayName ?: null;
$box->username = $this->username ?: null;
$box->no_reply = $this->noReply;
$box->active = $this->active;
// Empty means "leave it alone", not "delete it" — otherwise every edit
// of an address would silently drop the password.
if ($this->password !== '') {
$box->password = $this->password;
$box->last_verified_at = null;
}
$box->save();
$this->password = '';
$this->dispatch('notify', message: __('mail_settings.saved'));
$this->dispatch('mailbox-saved');
$this->closeModal();
}
public function render()
{
return view('livewire.edit-mailbox', [
'passwordConfirmed' => $this->passwordRecentlyConfirmed(),
]);
}
}