purpose => mailbox key */ public array $purposes = []; public string $testRecipient = ''; /** @var array{ok: bool, error: ?string}|null */ public ?array $testResult = null; public ?string $testedKey = null; /** * Whether credentials can be stored at all on this installation. * * Public so the page can say it rather than throwing on the first password * a mailbox tries to decrypt — the same courtesy the secrets page already * extends. A missing SECRETS_KEY is a setup state, not an error. */ public bool $usable = true; public function mount(): void { $this->authorize('mail.manage'); $this->host = (string) Settings::get('mail.host', ''); $this->port = (int) Settings::get('mail.port', 587); $this->encryption = (string) Settings::get('mail.encryption', 'tls'); foreach (MailPurpose::ALL as $purpose) { $this->purposes[$purpose] = (string) Settings::get(MailPurpose::settingKey($purpose), ''); } } public function saveServer(): void { $this->authorize('mail.manage'); // $this->host is the platform's outbound relay for every purpose // mailbox at once — pointing it at an attacker's server intercepts // everything CluPilot sends. The capability decides who may open this // page; a recent password decides whether THIS session may repoint // it, the same second gate Admin\Secrets uses and for the same // reason: the realistic threat is an unlocked machine, not a // stranger. savePurposes() and test() stay on the capability alone — // this is the "changing an address" split's one exception. abort_unless($this->passwordRecentlyConfirmed(), 403); // Rules on the ACTION, not on the property: a #[Validate] attribute on // a Livewire property applies class-wide, and savePurposes() below // would drag these along. $this->validate([ 'host' => ['required', 'string', 'max:255'], 'port' => ['required', 'integer', 'min:1', 'max:65535'], 'encryption' => ['required', 'in:tls,ssl,none'], ]); Settings::set('mail.host', $this->host); Settings::set('mail.port', (int) $this->port); Settings::set('mail.encryption', $this->encryption); $this->dispatch('notify', message: __('mail_settings.server_saved')); } public function savePurposes(): void { $this->authorize('mail.manage'); // Reachable by posting straight to /livewire/update, past whatever // the