From ee01926aabe898005c4761b637fea606357abdba Mon Sep 17 00:00:00 2001 From: Jukka Lampikoski Date: Tue, 10 Mar 2026 23:39:12 +0200 Subject: [PATCH] Kirjoita SMTP-client uusiksi: AUTH PLAIN + LOGIN, SSL-konteksti MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Kunnon multi-line response -lukija (smtpReadResponse) - Kokeilee ensin AUTH PLAIN, sitten AUTH LOGIN fallbackinä - SSL-konteksti sallii self-signed-sertifikaatit (Plesk) - Täysi debug-loki joka vaiheesta (näkyy error_log:ssa) - Virheviestissä salasanan pituus (ei itse salasanaa) Co-Authored-By: Claude Opus 4.6 --- api.php | 184 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 113 insertions(+), 71 deletions(-) diff --git a/api.php b/api.php index 806f46a..37acf5f 100644 --- a/api.php +++ b/api.php @@ -622,6 +622,27 @@ function sendTicketMail(string $to, string $subject, string $body, string $inRep /** @var string|null Viimeisin SMTP-virhe (palautetaan frontendille) */ $GLOBALS['smtp_last_error'] = null; +function smtpReadResponse($fp): string { + $full = ''; + while ($line = @fgets($fp, 512)) { + $full .= $line; + // Viimeinen rivi: koodi + välilyönti (ei -) + if (isset($line[3]) && $line[3] !== '-') break; + // Timeout / EOF + if ($line === false) break; + } + return $full; +} + +function smtpCommand($fp, string $cmd): string { + fwrite($fp, $cmd . "\r\n"); + return smtpReadResponse($fp); +} + +function smtpCode(string $resp): string { + return substr(trim($resp), 0, 3); +} + function sendViaSMTP(string $to, string $subject, string $body, string $fromEmail, string $fromName, string $inReplyTo, string $references, array $mailbox, string $cc): bool { $host = $mailbox['smtp_host']; $port = (int)($mailbox['smtp_port'] ?? 587); @@ -629,90 +650,110 @@ function sendViaSMTP(string $to, string $subject, string $body, string $fromEmai $pass = $mailbox['smtp_password'] ?? ''; $encryption = $mailbox['smtp_encryption'] ?? 'tls'; - $timeout = 15; - $errno = 0; $errstr = ''; + $log = []; // Debug-loki - $fail = function(string $step, string $detail) use (&$fp) { - $msg = "SMTP $step: $detail"; + $fail = function(string $step, string $detail) use (&$fp, &$log) { + $log[] = "FAIL @ {$step}: {$detail}"; + $msg = "SMTP {$step}: {$detail} | log: " . implode(' → ', $log); error_log($msg); - $GLOBALS['smtp_last_error'] = $msg; - if (isset($fp) && $fp) fclose($fp); + $GLOBALS['smtp_last_error'] = "SMTP {$step}: {$detail}"; + if (isset($fp) && is_resource($fp)) fclose($fp); return false; }; - // Yhteys - if ($encryption === 'ssl') { - $fp = @stream_socket_client("ssl://{$host}:{$port}", $errno, $errstr, $timeout); - } else { - $fp = @stream_socket_client("tcp://{$host}:{$port}", $errno, $errstr, $timeout); - } - if (!$fp) { - return $fail('connect', "{$errstr} ({$errno}) — host={$host}:{$port} enc={$encryption}"); - } + // 1. Yhteys + $timeout = 15; + $errno = 0; $errstr = ''; + $connStr = ($encryption === 'ssl' ? "ssl" : "tcp") . "://{$host}:{$port}"; + $log[] = "connect {$connStr}"; + + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, + ]]); + $fp = @stream_socket_client($connStr, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $ctx); + if (!$fp) return $fail('connect', "{$errstr} ({$errno})"); stream_set_timeout($fp, $timeout); - $resp = fgets($fp, 512); - if (substr($resp, 0, 3) !== '220') return $fail('banner', trim($resp)); + // 2. Banner + $resp = smtpReadResponse($fp); + $log[] = "banner:" . smtpCode($resp); + if (smtpCode($resp) !== '220') return $fail('banner', trim($resp)); - // EHLO - fwrite($fp, "EHLO " . gethostname() . "\r\n"); - $ehloResp = ''; - while ($line = fgets($fp, 512)) { - $ehloResp .= $line; - if (substr($line, 3, 1) === ' ') break; - } + // 3. EHLO + $ehlo = smtpCommand($fp, "EHLO " . gethostname()); + $log[] = "ehlo:" . smtpCode($ehlo); + if (smtpCode($ehlo) !== '250') return $fail('EHLO', trim($ehlo)); - // STARTTLS jos tls + // 4. STARTTLS if ($encryption === 'tls') { - fwrite($fp, "STARTTLS\r\n"); - $resp = fgets($fp, 512); - if (substr($resp, 0, 3) !== '220') return $fail('STARTTLS', trim($resp)); - $crypto = @stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT); - if (!$crypto) return $fail('TLS', 'TLS negotiation failed'); - // EHLO uudelleen TLS:n jälkeen - fwrite($fp, "EHLO " . gethostname() . "\r\n"); - while ($line = fgets($fp, 512)) { - if (substr($line, 3, 1) === ' ') break; - } + $resp = smtpCommand($fp, "STARTTLS"); + $log[] = "starttls:" . smtpCode($resp); + if (smtpCode($resp) !== '220') return $fail('STARTTLS', trim($resp)); + $crypto = @stream_socket_enable_crypto($fp, true, + STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT); + if (!$crypto) return $fail('TLS', 'negotiation failed'); + $log[] = "tls:ok"; + // EHLO opnieuw na TLS + $ehlo = smtpCommand($fp, "EHLO " . gethostname()); + $log[] = "ehlo2:" . smtpCode($ehlo); } - // AUTH LOGIN + // 5. AUTH — probeer eerst PLAIN, dan LOGIN if ($user !== '') { - fwrite($fp, "AUTH LOGIN\r\n"); - $resp = fgets($fp, 512); - if (substr($resp, 0, 3) !== '334') return $fail('AUTH', trim($resp)); - fwrite($fp, base64_encode($user) . "\r\n"); - $resp = fgets($fp, 512); - if (substr($resp, 0, 3) !== '334') return $fail('AUTH user', trim($resp) . " (user={$user})"); - fwrite($fp, base64_encode($pass) . "\r\n"); - $resp = fgets($fp, 512); - if (substr($resp, 0, 3) !== '235') return $fail('AUTH pass', trim($resp)); - } + $authOk = false; - // MAIL FROM - fwrite($fp, "MAIL FROM:<{$fromEmail}>\r\n"); - $resp = fgets($fp, 512); - if (substr($resp, 0, 3) !== '250') return $fail('MAIL FROM', trim($resp) . " (from={$fromEmail})"); - - // RCPT TO - $allRecipients = array_filter(array_map('trim', explode(',', $to))); - if ($cc) { - $allRecipients = array_merge($allRecipients, array_filter(array_map('trim', explode(',', $cc)))); - } - foreach ($allRecipients as $rcpt) { - fwrite($fp, "RCPT TO:<{$rcpt}>\r\n"); - $resp = fgets($fp, 512); - if (substr($resp, 0, 3) !== '250' && substr($resp, 0, 3) !== '251') { - return $fail('RCPT TO', trim($resp) . " (rcpt={$rcpt})"); + // AUTH PLAIN + $cred = base64_encode("\0{$user}\0{$pass}"); + $resp = smtpCommand($fp, "AUTH PLAIN {$cred}"); + $log[] = "auth_plain:" . smtpCode($resp); + if (smtpCode($resp) === '235') { + $authOk = true; + } else { + // AUTH LOGIN fallback + $resp = smtpCommand($fp, "AUTH LOGIN"); + $log[] = "auth_login:" . smtpCode($resp); + if (smtpCode($resp) === '334') { + $resp = smtpCommand($fp, base64_encode($user)); + $log[] = "auth_user:" . smtpCode($resp); + if (smtpCode($resp) === '334') { + $resp = smtpCommand($fp, base64_encode($pass)); + $log[] = "auth_pass:" . smtpCode($resp); + if (smtpCode($resp) === '235') { + $authOk = true; + } + } + } } + + if (!$authOk) { + $passHint = strlen($pass) > 0 ? strlen($pass) . ' chars' : 'EMPTY'; + return $fail('AUTH', trim($resp) . " (user={$user}, pass={$passHint})"); + } + $log[] = "auth:ok"; } - // DATA - fwrite($fp, "DATA\r\n"); - $resp = fgets($fp, 512); - if (substr($resp, 0, 3) !== '354') return $fail('DATA', trim($resp)); + // 6. MAIL FROM + $resp = smtpCommand($fp, "MAIL FROM:<{$fromEmail}>"); + $log[] = "from:" . smtpCode($resp); + if (smtpCode($resp) !== '250') return $fail('MAIL FROM', trim($resp)); - // Rakennetaan viesti + // 7. RCPT TO + $allRecipients = array_filter(array_map('trim', explode(',', $to))); + if ($cc) $allRecipients = array_merge($allRecipients, array_filter(array_map('trim', explode(',', $cc)))); + foreach ($allRecipients as $rcpt) { + $resp = smtpCommand($fp, "RCPT TO:<{$rcpt}>"); + $log[] = "rcpt:" . smtpCode($resp); + if (!in_array(smtpCode($resp), ['250', '251'])) return $fail('RCPT TO', trim($resp) . " ({$rcpt})"); + } + + // 8. DATA + $resp = smtpCommand($fp, "DATA"); + $log[] = "data:" . smtpCode($resp); + if (smtpCode($resp) !== '354') return $fail('DATA', trim($resp)); + + // 9. Viesti $messageId = '<' . uniqid('msg_', true) . '@' . (explode('@', $fromEmail)[1] ?? 'localhost') . '>'; $msg = "From: {$fromName} <{$fromEmail}>\r\n"; $msg .= "To: {$to}\r\n"; @@ -729,15 +770,16 @@ function sendViaSMTP(string $to, string $subject, string $body, string $fromEmai $msg .= "Date: " . date('r') . "\r\n"; $msg .= "\r\n"; $msg .= chunk_split(base64_encode($body)); - $msg .= "\r\n.\r\n"; - - fwrite($fp, $msg); - $resp = fgets($fp, 512); - if (substr($resp, 0, 3) !== '250') return $fail('send', trim($resp)); + // Lopeta piste omalla rivillä + fwrite($fp, $msg . "\r\n.\r\n"); + $resp = smtpReadResponse($fp); + $log[] = "send:" . smtpCode($resp); + if (smtpCode($resp) !== '250') return $fail('send', trim($resp)); // QUIT fwrite($fp, "QUIT\r\n"); fclose($fp); + error_log("SMTP OK: " . implode(' → ', $log)); return true; }