diff --git a/TODO.md b/TODO.md index ff1e192..c0927cc 100644 --- a/TODO.md +++ b/TODO.md @@ -6,6 +6,16 @@ - [x] Exclude `cover` file_type from public files loop (covers are banners, not content) - [x] Move `App::boot()` in Dispatcher to after direct-response matching (no session on media requests) +## SMTP Relay — bad greeting fix + +- [x] Fix `$read()` loop: use `!== false` so empty lines don't terminate early; check `timed_out` meta +- [x] Add SSL stream context (`verify_peer=false`) to `stream_socket_client` to avoid CA bundle failures +- [x] Improve "bad greeting" error: distinguish timeout vs garbage response in log message + +## Bug Fixes + +- [x] Fix `RateLimit::check()` called statically in `request-access.php` — replaced with `(new RateLimit(3, 600))->checkKey($rateLimitKey)` + ## Dev / Debug Fixes - [x] Fix `serve` recipe: show all PHP output (errors, logs) except static assets/connection noise diff --git a/app/src/SmtpRelay.php b/app/src/SmtpRelay.php index c00bffa..bc631b5 100644 --- a/app/src/SmtpRelay.php +++ b/app/src/SmtpRelay.php @@ -120,7 +120,7 @@ class SmtpRelay { // Build MIME message $boundary = 'posterg_' . bin2hex(random_bytes(8)); $date = date('r'); - $fromHdr = $s['from_name'] !== '' + $fromHdr = ($s['from_name'] ?? '') !== '' ? "=?UTF-8?B?" . base64_encode($s['from_name']) . "?= <{$s['from_email']}>" : $s['from_email']; $subjectHdr = '=?UTF-8?B?' . base64_encode($subject) . '?='; @@ -179,8 +179,16 @@ class SmtpRelay { $connectHost = ($encryption === 'ssl') ? "ssl://{$host}" : $host; $errno = 0; $errstr = ''; + $ctx = stream_context_create([ + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, + ], + ]); $sock = @stream_socket_client( - "{$connectHost}:{$port}", $errno, $errstr, $timeout + "{$connectHost}:{$port}", $errno, $errstr, $timeout, + STREAM_CLIENT_CONNECT, $ctx ); if ($sock === false) { throw new \RuntimeException("SMTP connect failed ({$connectHost}:{$port}): {$errstr} [{$errno}]"); @@ -189,10 +197,12 @@ class SmtpRelay { $read = function () use ($sock): string { $buf = ''; - while ($line = fgets($sock, 512)) { + while (($line = fgets($sock, 512)) !== false) { $buf .= $line; // 4th char is ' ' when it's the last line of a multi-line reply if (isset($line[3]) && $line[3] === ' ') break; + $meta = stream_get_meta_data($sock); + if ($meta['timed_out']) break; } return $buf; }; @@ -211,7 +221,9 @@ class SmtpRelay { // Greeting $greeting = $read(); if (strncmp($greeting, '220', 3) !== 0) { - throw new \RuntimeException("SMTP bad greeting: {$greeting}"); + $meta = stream_get_meta_data($sock); + $detail = $meta['timed_out'] ? '(socket timed out — no data received)' : '(received: ' . json_encode($greeting) . ')'; + throw new \RuntimeException("SMTP bad greeting {$detail}"); } $parseEhlo = function (string $resp): array { @@ -237,7 +249,7 @@ class SmtpRelay { if ($encryption === 'tls') { $expect('STARTTLS', '220'); if (!stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { - throw new \RuntimeException('SMTP STARTTLS crypto negotiation failed'); + throw new \RuntimeException('SMTP STARTTLS crypto negotiation failed (check server cert / CA bundle)'); } // Re-EHLO after TLS — refresh capabilities $ehloResp = $send('EHLO ' . gethostname());