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:
178
api.php
178
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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})");
|
||||
if (!$authOk) {
|
||||
$passHint = strlen($pass) > 0 ? strlen($pass) . ' chars' : 'EMPTY';
|
||||
return $fail('AUTH', trim($resp) . " (user={$user}, pass={$passHint})");
|
||||
}
|
||||
$log[] = "auth:ok";
|
||||
}
|
||||
|
||||
// RCPT TO
|
||||
// 6. MAIL FROM
|
||||
$resp = smtpCommand($fp, "MAIL FROM:<{$fromEmail}>");
|
||||
$log[] = "from:" . smtpCode($resp);
|
||||
if (smtpCode($resp) !== '250') return $fail('MAIL FROM', trim($resp));
|
||||
|
||||
// 7. RCPT TO
|
||||
$allRecipients = array_filter(array_map('trim', explode(',', $to)));
|
||||
if ($cc) {
|
||||
$allRecipients = array_merge($allRecipients, array_filter(array_map('trim', explode(',', $cc))));
|
||||
}
|
||||
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})");
|
||||
}
|
||||
$resp = smtpCommand($fp, "RCPT TO:<{$rcpt}>");
|
||||
$log[] = "rcpt:" . smtpCode($resp);
|
||||
if (!in_array(smtpCode($resp), ['250', '251'])) return $fail('RCPT TO', trim($resp) . " ({$rcpt})");
|
||||
}
|
||||
|
||||
// DATA
|
||||
fwrite($fp, "DATA\r\n");
|
||||
$resp = fgets($fp, 512);
|
||||
if (substr($resp, 0, 3) !== '354') return $fail('DATA', trim($resp));
|
||||
// 8. DATA
|
||||
$resp = smtpCommand($fp, "DATA");
|
||||
$log[] = "data:" . smtpCode($resp);
|
||||
if (smtpCode($resp) !== '354') return $fail('DATA', trim($resp));
|
||||
|
||||
// Rakennetaan viesti
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user