password: that accessor // decrypts under SECRETS_KEY, and it is not set on this installation // at all (the mail settings page's own banner says so). Reading it // first would let a RuntimeException out of a Livewire action — // Task 7's EditMailbox::save() shipped a Critical fix for exactly // this shape of bug. The tester touches the same cipher and gets the // same guard, checked here rather than left for the property access // below to throw through. // // Codex R15#5, P2: gated on authenticates, same as the password // presence check right below. When this mailbox does not // authenticate, $box->password is never read (see the credentials // ternary further down) — requiring a usable cipher anyway refused a // configuration real sending accepts, on exactly the "fresh install, // no SECRETS_KEY yet" installation the unauthenticated-relay support // exists for. if ($box->authenticates && ! $this->cipher->isUsable()) { return ['ok' => false, 'error' => __('mail_settings.no_key')]; } // Codex R15#9, P2: isUsable() only proves SECRETS_KEY is well-formed // (right length, right shape) — never that THIS row's ciphertext was // ever encrypted under it. A rotated key, or a row seeded/encrypted // under a different one, sails straight past the guard above and // THEN throws a DecryptException the instant $box->password is // actually read: the same uncaught-out-of-a-Livewire-action shape // the guard above exists for, just a failure isUsable() cannot see // by design. Read once, here, behind its own guard, and reuse the // plaintext below — the null-check on the next line and the // credentials ternary further down used to both call this SAME // decrypting accessor a second and third time, each one another // uncaught throw waiting to happen. try { $password = $box->authenticates ? $box->password : null; } catch (DecryptException) { return ['ok' => false, 'error' => __('mail_settings.test_password_undecryptable')]; } // Codex R15#4, P1b: a password is only REQUIRED when this mailbox // actually authenticates — a trusted local or private-network relay // can legitimately need none at all, and refusing it here is exactly // the "no password stored" outage the finding names, even though the // relay was never broken. if ($box->authenticates && $password === null) { return ['ok' => false, 'error' => __('mail_settings.test_no_password')]; } $port = (int) Settings::get('mail.port', 587); // Codex R15#5, P2 comparison pass: MailboxTransport::delegate() has // an equivalent guard (host==='' too — but that one is not repeated // here, since Mail::build() with a blank host already fails just as // fast on its own, confirmed by hand). Port is different in kind, not // merely in wording: a stored NULL setting casts to 0, and left // unguarded, Symfony's EsmtpTransportFactory silently reinterprets // port 0 as port 25 rather than rejecting it — confirmed by hand, // not assumed. Without this guard, a test-send against a mailbox // whose port was never (or no longer) configured could actually // reach whatever happens to listen on 25 (a local MTA, say) and // report success for a configuration MailboxTransport refuses // outright before it ever opens a socket. if ($port < 1) { return ['ok' => false, 'error' => __('mail_settings.test_port_not_configured')]; } $policy = MailTlsPolicy::for((string) Settings::get('mail.encryption', 'tls'), $port); // Omitted entirely, not merely blank, when this mailbox does not // authenticate: Mailbox::smtpUsername() always returns something // non-empty (it falls back to the address), so passing it through // regardless would still hand Mail::build() a real 'username' — // enough by itself to make Symfony attempt AUTH against any server // that advertises it, which a trusted no-auth relay was never asked // to answer. Mirrors MailboxTransport's own setUsername()/ // setPassword() guard for the real send path. $credentials = $box->authenticates ? ['username' => $box->smtpUsername(), 'password' => $password] : []; try { Mail::build([ 'transport' => 'smtp', 'host' => (string) Settings::get('mail.host', ''), 'port' => $port, // 'scheme', NOT 'encryption'. Laravel 13's MailManager reads // scheme only (MailManager.php:196) — an 'encryption' key is // silently ignored, and the connection would go out in the // clear while the console reported TLS. 'scheme' => $policy->implicit ? 'smtps' : 'smtp', // auto_tls / require_tls: MailManager::createSmtpTransport() // forwards this whole config array to EsmtpTransportFactory // as DSN options, and the factory reads exactly these two // keys itself (see EsmtpTransportFactory::create() in the // installed vendor source) — so the same MailTlsPolicy that // configures the real MailboxTransport reaches this path too, // through the only channel Mail::build()'s config array // actually offers for it. Without these, this path would // silently prove a different policy than real sends enforce. 'auto_tls' => $policy->autoTls, 'require_tls' => $policy->requireTls, ...$credentials, ])->raw( __('mail_settings.test_body', ['key' => $box->key]), function ($message) use ($box, $recipient) { $message->to($recipient) ->from($box->address, $box->display_name ?: null) ->subject(__('mail_settings.test_subject')); }, ); } catch (Throwable $e) { // Verbatim. With wrong credentials, or a server that rejects the // message outright, the server's own sentence IS the entire // diagnosis — "sending failed" would throw that away. return ['ok' => false, 'error' => $e->getMessage()]; } $box->forceFill(['last_verified_at' => now()])->save(); return ['ok' => true, 'error' => null]; } }