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) */ /** @var string|null Viimeisin SMTP-virhe (palautetaan frontendille) */
$GLOBALS['smtp_last_error'] = null; $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 { 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']; $host = $mailbox['smtp_host'];
$port = (int)($mailbox['smtp_port'] ?? 587); $port = (int)($mailbox['smtp_port'] ?? 587);
@@ -629,90 +650,110 @@ function sendViaSMTP(string $to, string $subject, string $body, string $fromEmai
$pass = $mailbox['smtp_password'] ?? ''; $pass = $mailbox['smtp_password'] ?? '';
$encryption = $mailbox['smtp_encryption'] ?? 'tls'; $encryption = $mailbox['smtp_encryption'] ?? 'tls';
$timeout = 15; $log = []; // Debug-loki
$errno = 0; $errstr = '';
$fail = function(string $step, string $detail) use (&$fp) { $fail = function(string $step, string $detail) use (&$fp, &$log) {
$msg = "SMTP $step: $detail"; $log[] = "FAIL @ {$step}: {$detail}";
$msg = "SMTP {$step}: {$detail} | log: " . implode(' → ', $log);
error_log($msg); error_log($msg);
$GLOBALS['smtp_last_error'] = $msg; $GLOBALS['smtp_last_error'] = "SMTP {$step}: {$detail}";
if (isset($fp) && $fp) fclose($fp); if (isset($fp) && is_resource($fp)) fclose($fp);
return false; return false;
}; };
// Yhteys // 1. Yhteys
if ($encryption === 'ssl') { $timeout = 15;
$fp = @stream_socket_client("ssl://{$host}:{$port}", $errno, $errstr, $timeout); $errno = 0; $errstr = '';
} else { $connStr = ($encryption === 'ssl' ? "ssl" : "tcp") . "://{$host}:{$port}";
$fp = @stream_socket_client("tcp://{$host}:{$port}", $errno, $errstr, $timeout); $log[] = "connect {$connStr}";
}
if (!$fp) { $ctx = stream_context_create(['ssl' => [
return $fail('connect', "{$errstr} ({$errno}) — host={$host}:{$port} enc={$encryption}"); '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); stream_set_timeout($fp, $timeout);
$resp = fgets($fp, 512); // 2. Banner
if (substr($resp, 0, 3) !== '220') return $fail('banner', trim($resp)); $resp = smtpReadResponse($fp);
$log[] = "banner:" . smtpCode($resp);
if (smtpCode($resp) !== '220') return $fail('banner', trim($resp));
// EHLO // 3. EHLO
fwrite($fp, "EHLO " . gethostname() . "\r\n"); $ehlo = smtpCommand($fp, "EHLO " . gethostname());
$ehloResp = ''; $log[] = "ehlo:" . smtpCode($ehlo);
while ($line = fgets($fp, 512)) { if (smtpCode($ehlo) !== '250') return $fail('EHLO', trim($ehlo));
$ehloResp .= $line;
if (substr($line, 3, 1) === ' ') break;
}
// STARTTLS jos tls // 4. STARTTLS
if ($encryption === 'tls') { if ($encryption === 'tls') {
fwrite($fp, "STARTTLS\r\n"); $resp = smtpCommand($fp, "STARTTLS");
$resp = fgets($fp, 512); $log[] = "starttls:" . smtpCode($resp);
if (substr($resp, 0, 3) !== '220') return $fail('STARTTLS', trim($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); $crypto = @stream_socket_enable_crypto($fp, true,
if (!$crypto) return $fail('TLS', 'TLS negotiation failed'); STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT);
// EHLO uudelleen TLS:n jälkeen if (!$crypto) return $fail('TLS', 'negotiation failed');
fwrite($fp, "EHLO " . gethostname() . "\r\n"); $log[] = "tls:ok";
while ($line = fgets($fp, 512)) { // EHLO opnieuw na TLS
if (substr($line, 3, 1) === ' ') break; $ehlo = smtpCommand($fp, "EHLO " . gethostname());
} $log[] = "ehlo2:" . smtpCode($ehlo);
} }
// AUTH LOGIN // 5. AUTH — probeer eerst PLAIN, dan LOGIN
if ($user !== '') { if ($user !== '') {
fwrite($fp, "AUTH LOGIN\r\n"); $authOk = false;
$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));
}
// MAIL FROM // AUTH PLAIN
fwrite($fp, "MAIL FROM:<{$fromEmail}>\r\n"); $cred = base64_encode("\0{$user}\0{$pass}");
$resp = fgets($fp, 512); $resp = smtpCommand($fp, "AUTH PLAIN {$cred}");
if (substr($resp, 0, 3) !== '250') return $fail('MAIL FROM', trim($resp) . " (from={$fromEmail})"); $log[] = "auth_plain:" . smtpCode($resp);
if (smtpCode($resp) === '235') {
// RCPT TO $authOk = true;
$allRecipients = array_filter(array_map('trim', explode(',', $to))); } else {
if ($cc) { // AUTH LOGIN fallback
$allRecipients = array_merge($allRecipients, array_filter(array_map('trim', explode(',', $cc)))); $resp = smtpCommand($fp, "AUTH LOGIN");
} $log[] = "auth_login:" . smtpCode($resp);
foreach ($allRecipients as $rcpt) { if (smtpCode($resp) === '334') {
fwrite($fp, "RCPT TO:<{$rcpt}>\r\n"); $resp = smtpCommand($fp, base64_encode($user));
$resp = fgets($fp, 512); $log[] = "auth_user:" . smtpCode($resp);
if (substr($resp, 0, 3) !== '250' && substr($resp, 0, 3) !== '251') { if (smtpCode($resp) === '334') {
return $fail('RCPT TO', trim($resp) . " (rcpt={$rcpt})"); $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 // 6. MAIL FROM
fwrite($fp, "DATA\r\n"); $resp = smtpCommand($fp, "MAIL FROM:<{$fromEmail}>");
$resp = fgets($fp, 512); $log[] = "from:" . smtpCode($resp);
if (substr($resp, 0, 3) !== '354') return $fail('DATA', trim($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') . '>'; $messageId = '<' . uniqid('msg_', true) . '@' . (explode('@', $fromEmail)[1] ?? 'localhost') . '>';
$msg = "From: {$fromName} <{$fromEmail}>\r\n"; $msg = "From: {$fromName} <{$fromEmail}>\r\n";
$msg .= "To: {$to}\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 .= "Date: " . date('r') . "\r\n";
$msg .= "\r\n"; $msg .= "\r\n";
$msg .= chunk_split(base64_encode($body)); $msg .= chunk_split(base64_encode($body));
$msg .= "\r\n.\r\n"; // Lopeta piste omalla rivillä
fwrite($fp, $msg . "\r\n.\r\n");
fwrite($fp, $msg); $resp = smtpReadResponse($fp);
$resp = fgets($fp, 512); $log[] = "send:" . smtpCode($resp);
if (substr($resp, 0, 3) !== '250') return $fail('send', trim($resp)); if (smtpCode($resp) !== '250') return $fail('send', trim($resp));
// QUIT // QUIT
fwrite($fp, "QUIT\r\n"); fwrite($fp, "QUIT\r\n");
fclose($fp); fclose($fp);
error_log("SMTP OK: " . implode(' → ', $log));
return true; return true;
} }