Kirjoita SMTP-client uusiksi: AUTH PLAIN + LOGIN, SSL-konteksti

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 23:39:12 +02:00
parent f7e5a3c1db
commit ee01926aab

184
api.php
View File

@@ -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;
}