set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32))); config()->set('mail.default', 'log'); // as on a development machine Settings::set('mail.host', '127.0.0.1'); Settings::set('mail.port', 1); // 'none', not 'tls': FakeSmtpServer only ever speaks plain text (see its // own docblock) and never advertises STARTTLS. Since the P1 fix makes // 'tls' REQUIRE a STARTTLS upgrade, keeping this default at 'tls' would // have broken every success/rejection test below that needs the fake // server to complete a full plaintext dialogue — none of which are // actually about the TLS policy. The tests that ARE about it set their // own encryption explicitly, below. Settings::set('mail.encryption', 'none'); }); /* | The brief points this beforeEach at 'mail.example.invalid' and asserts the | error mentions it. Measured by hand in this container (php artisan tinker | equivalent, three runs each): a lookup for 'mail.example.invalid' fails in | 0.7-7ms via NXDOMAIN, but that number depends on a resolver being reachable | at all — nothing here guarantees that in every environment this suite might | run in. 127.0.0.1:1 (nothing listens on port 1) refused every one of those | three runs in ~0.2ms with zero DNS involved, and it is the convention this | codebase already settled on for the identical reason — see | MailboxTransportTest's "LOAD-BEARING for the MAIL_MAILER=log guard" comment. | Using it here keeps both tests fast AND deterministic instead of merely fast | today. */ it('does NOT quietly succeed through the log mailer', function () { // The whole point: on dev the default mailer is the log, and a tester // that honoured it would report success while writing to a file — a // test that proves nothing. The log "transport" never fails, so only a // real attempt (which a closed port refuses) can make this assertion // meaningful. $result = app(MailboxTester::class)->run( Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']), 'ziel@example.com', ); expect($result['ok'])->toBeFalse() ->and($result['error'])->not->toBeNull(); }); it('passes the SMTP error through verbatim, because that is the whole diagnosis', function () { $result = app(MailboxTester::class)->run( Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']), 'ziel@example.com', ); // Not "Fehler beim Senden" — the operating system's own sentence, naming // the actual host:port and the actual reason. expect($result['error'])->toContain('127.0.0.1:1') ->and($result['error'])->toContain('Connection refused'); }); it('refuses a mailbox without a password rather than pretending to try — when it authenticates', function () { // The control case for the block below: authenticates defaults to true // on the factory, so this is what the guard must still refuse. $box = Mailbox::factory()->create(['password' => null]); $result = app(MailboxTester::class)->run($box, 'ziel@example.com'); expect($result['ok'])->toBeFalse() ->and($result['error'])->toContain('Passwort'); }); // --- Codex R15#4, P1b: an unauthenticated relay (a trusted local or // private-network server) must not be refused for lacking a password — // Laravel's previous SMTP configuration has always supported sending through // one with none configured at all. it('does not refuse an unauthenticated mailbox for lacking a password', function () { $server = FakeSmtpServer::succeeding(); try { Settings::set('mail.host', '127.0.0.1'); Settings::set('mail.port', $server->port); $box = Mailbox::factory()->create([ 'address' => 'no-reply@clupilot.com', 'username' => null, 'password' => null, 'authenticates' => false, ]); $result = app(MailboxTester::class)->run($box, 'ziel@example.com'); expect($result['ok'])->toBeTrue() ->and($result['error'])->toBeNull(); } finally { $server->stop(); } }); it('never attempts AUTH when the mailbox does not authenticate, even against a server that offers it', function () { // succeeding() alone cannot prove this — it never advertises AUTH, so // handleAuth() is unreachable regardless of what MailboxTester supplies. // succeedingUnlessAuthAttempted() offers AUTH and rejects any attempt, // so only a client that never sends "AUTH ..." at all can succeed here. $server = FakeSmtpServer::succeedingUnlessAuthAttempted(); try { Settings::set('mail.host', '127.0.0.1'); Settings::set('mail.port', $server->port); $box = Mailbox::factory()->create([ 'address' => 'no-reply@clupilot.com', 'username' => null, 'password' => null, 'authenticates' => false, ]); $result = app(MailboxTester::class)->run($box, 'ziel@example.com'); expect($result['ok'])->toBeTrue() ->and($result['error'])->toBeNull(); } finally { $server->stop(); } }); it('still attempts AUTH when the mailbox does authenticate, against the same AUTH-offering server', function () { // The control case for the test above: proves succeedingUnlessAuthAttempted() // actually discriminates rather than always succeeding regardless of // what the client does — an authenticating mailbox reaches handleAuth(), // which sends AUTH LOGIN, which this server always rejects. $server = FakeSmtpServer::succeedingUnlessAuthAttempted(); try { Settings::set('mail.host', '127.0.0.1'); Settings::set('mail.port', $server->port); $box = Mailbox::factory()->create([ 'address' => 'no-reply@clupilot.com', 'password' => 'a-real-password', 'authenticates' => true, ]); $result = app(MailboxTester::class)->run($box, 'ziel@example.com'); expect($result['ok'])->toBeFalse(); } finally { $server->stop(); } }); it('stamps last_verified_at only on success', function () { $box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']); app(MailboxTester::class)->run($box, 'ziel@example.com'); expect($box->fresh()->last_verified_at)->toBeNull(); }); it('gives a readable message instead of throwing when SECRETS_KEY is unusable', function () { // Facts the brief could not know: SECRETS_KEY is not set on this // installation at all (the mail settings page's own banner says so). // $box->password decrypts on read, so touching it throws a // RuntimeException out of SecretCipher — Task 7's EditMailbox::save() // shipped a Critical fix for exactly this shape of bug. The tester // touches the very same cipher and needs the very same guard. $box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']); config()->set('admin_access.secrets_key', ''); $result = app(MailboxTester::class)->run($box, 'ziel@example.com'); expect($result['ok'])->toBeFalse() ->and($result['error'])->toBe(__('mail_settings.no_key')); }); // --- Codex R15#5, P2: an unauthenticated mailbox never reads $box->password // (see the credentials ternary above in run() and the guard just above it), // so requiring a usable SECRETS_KEY before even checking authenticates // refused a configuration real sending accepts — on exactly the "fresh // install, no SECRETS_KEY yet" installation the unauthenticated-relay // support (Codex R15#4, P1b) exists for. it('does not require a usable cipher for an unauthenticated mailbox, even with no key configured at all', function () { $server = FakeSmtpServer::succeeding(); try { config()->set('admin_access.secrets_key', ''); Settings::set('mail.host', '127.0.0.1'); Settings::set('mail.port', $server->port); $box = Mailbox::factory()->create([ 'address' => 'no-reply@clupilot.com', 'username' => null, 'password' => null, 'authenticates' => false, ]); $result = app(MailboxTester::class)->run($box, 'ziel@example.com'); expect($result['ok'])->toBeTrue() ->and($result['error'])->toBeNull(); } finally { $server->stop(); } }); /* | LOAD-BEARING for the success path. Every test above only proves a FAILURE | is reported correctly — none of them ever reaches | `$box->forceFill(['last_verified_at' => now()])->save()`, so all of them | would stay green even if that line were deleted outright. A real success | needs something real to answer on the other end of the socket; see | FakeSmtpServer for why that has to be a subprocess rather than an in-process | fake. */ it('actually delivers and stamps last_verified_at when the server accepts the message', function () { $server = FakeSmtpServer::succeeding(); try { Settings::set('mail.host', '127.0.0.1'); Settings::set('mail.port', $server->port); $box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']); $result = app(MailboxTester::class)->run($box, 'ziel@example.com'); expect($result['ok'])->toBeTrue() ->and($result['error'])->toBeNull() ->and($box->fresh()->last_verified_at)->not->toBeNull(); } finally { $server->stop(); } }); it('passes a real SMTP rejection through verbatim too, not only a refused connection', function () { // The strongest version of "the server's own sentence": an actual // protocol-level 5xx from an actual SMTP dialogue, not merely the // operating system's "connection refused". Proves MailboxTester's catch // block does not paraphrase or replace what Symfony Mailer reports. $server = FakeSmtpServer::rejectingAt('rcpt', '550 5.1.1 No such user ziel@example.com'); try { Settings::set('mail.host', '127.0.0.1'); Settings::set('mail.port', $server->port); $box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']); $result = app(MailboxTester::class)->run($box, 'ziel@example.com'); expect($result['ok'])->toBeFalse() ->and($result['error'])->toContain('550 5.1.1 No such user ziel@example.com') ->and($box->fresh()->last_verified_at)->toBeNull(); } finally { $server->stop(); } }); it('actually attempts TLS when SSL is configured on a non-standard port, instead of silently going out in the clear', function () { // The exact trap this task names: Laravel 13's MailManager reads // 'scheme', not 'encryption' (MailManager.php:196). An 'encryption' key // is silently ignored, and MailManager falls back to choosing the scheme // from the port number alone — smtps only for port 465, smtp otherwise. // Configured for SSL on a non-465 port (a real EU hoster's submission // port with implicit TLS, say), that bug would connect in PLAIN TEXT // while the console still reported TLS. // // FakeSmtpServer only ever speaks plain text, so it can tell the two // apart: a correct 'scheme' => 'smtps' opens an implicit-TLS connection // this server cannot answer and the send FAILS; the buggy 'encryption' // key would default to plain 'smtp' on this non-465 port and let it // succeed instead. Verified by hand this fails in ~0.2s, not a hang — // OpenSSL rejects the server's plain-text greeting as an invalid TLS // record immediately. $server = FakeSmtpServer::succeeding(); try { Settings::set('mail.host', '127.0.0.1'); Settings::set('mail.port', $server->port); // deliberately not 465 Settings::set('mail.encryption', 'ssl'); $box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']); $result = app(MailboxTester::class)->run($box, 'ziel@example.com'); expect($result['ok'])->toBeFalse(); } finally { $server->stop(); } }); /* | P1: the test-send button must enforce the exact same TLS policy real | sending does (MailboxTransportTest carries the identical pair of tests for | the real path) — otherwise a green test-send proves nothing about what a | queued mail would actually do. Before the fix, MailboxTester built its | transport with the default autoTls/requireTls Symfony ships (autoTls on, | requireTls off): against a server that never offers STARTTLS, autoTls found | nothing to upgrade to, nothing else objected, and the send completed in the | clear while reporting success. */ it('fails, rather than sending in the clear, when TLS is required but the server never offers STARTTLS', function () { $server = FakeSmtpServer::succeeding(); try { Settings::set('mail.host', '127.0.0.1'); Settings::set('mail.port', $server->port); Settings::set('mail.encryption', 'tls'); $box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']); $result = app(MailboxTester::class)->run($box, 'ziel@example.com'); expect($result['ok'])->toBeFalse() ->and($result['error'])->toContain('TLS required but neither TLS or STARTTLS are in use.') ->and($box->fresh()->last_verified_at)->toBeNull(); } finally { $server->stop(); } }); it('still delivers over a plain connection when encryption is none, against a server that never offers TLS', function () { // The control case for the test above: requireTls must stay OFF for // 'none', or a legitimately plaintext-only relay would break the moment // the P1 fix landed. $server = FakeSmtpServer::succeeding(); try { Settings::set('mail.host', '127.0.0.1'); Settings::set('mail.port', $server->port); Settings::set('mail.encryption', 'none'); $box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']); $result = app(MailboxTester::class)->run($box, 'ziel@example.com'); expect($result['ok'])->toBeTrue() ->and($result['error'])->toBeNull() ->and($box->fresh()->last_verified_at)->not->toBeNull(); } finally { $server->stop(); } }); // --- Codex R15#5, P2 comparison pass: MailboxTransport::delegate() refuses // host==='' and port<1 with its own explicit, loud guards BEFORE building // anything (see its docblock: "Loud beats a silently downgraded // connection"). MailboxTester had neither. host==='' turned out not to be a // real divergence — confirmed by hand, not assumed: Mail::build() with a // blank host still fails fast (a few ms, via getaddrinfo), so both paths // already refuse, just with different wording. port<1 is different in kind, // not merely in wording: Symfony's EsmtpTransportFactory silently // reinterprets an invalid port as 25 rather than rejecting it (confirmed by // hand too), so MailboxTester would have gone on to actually attempt a // connection on port 25 — accepting a configuration the real transport // refuses outright. If a local MTA happens to listen there (common enough on // a self-hosted box), the test button could report success for exactly the // send MailboxTransport would never even attempt. Only the port guard is // added below; a host guard would be redundant with what Mail::build() // already does correctly on its own. it('refuses to send when the mail server port is not a positive number, rather than letting Symfony silently fall back to port 25', function () { Settings::set('mail.host', '127.0.0.1'); Settings::set('mail.port', null); // what a stored NULL json-decodes to, then casts to 0 — mirrors MailboxTransportTest's identical scenario $box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']); $result = app(MailboxTester::class)->run($box, 'ziel@example.com'); expect($result['ok'])->toBeFalse() ->and($result['error'])->toBe(__('mail_settings.test_port_not_configured')); });