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; $this->authenticates = $box->authenticates; } 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; } } $oldAddress = $box->address; $oldUsername = $box->username; $oldAuthenticates = $box->authenticates; $box->address = $this->address; $box->display_name = $this->displayName ?: null; $box->username = $this->username ?: null; $box->no_reply = $this->noReply; $box->active = $this->active; $box->authenticates = $this->authenticates; // Either one changes the identity smtpUsername() authenticates as: // username directly, or address as username's fallback when none is // set (including username being CLEARED back to that fallback). // authenticates itself is the fourth: whether that identity gets // sent at all. Leaving last_verified_at standing after any of the // three would keep showing a successful verification for a // connection nothing has actually tested since. Mailbox:: // invalidateVerification() is the same clear Admin\Mail::saveServer() // uses for the server card, in its single-row shape — see that // method's own comment for why the two do NOT also share the "did // this actually change" comparison above this block. if ($box->address !== $oldAddress || $box->username !== $oldUsername || $box->authenticates !== $oldAuthenticates) { $box->invalidateVerification(); } // 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->invalidateVerification(); } $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(), ]); } }