This commit is contained in:
2025-12-28 02:10:02 +01:00
parent b3e39ea10d
commit 5b05e79700

View File

@@ -193,51 +193,131 @@ final class Mailer
stream_set_timeout($fp, 15);
$transcript = [];
$read = function (string $label = 'read') use ($fp, &$transcript) {
$line = fgets($fp, 515);
if ($line !== false) {
$transcript[] = $label . ': ' . trim($line);
$readResponse = function (array $expectCodes = [], string $label = 'read') use ($fp, &$transcript): array {
$lines = [];
while (($line = fgets($fp, 515)) !== false) {
$line = rtrim($line, "\r\n");
$lines[] = $line;
$transcript[] = $label . ': ' . $line;
// SMTP multiline: code + '-' means more lines, code + ' ' means end
if (strlen($line) >= 4 && $line[3] === ' ') {
break;
}
return $line;
}
$code = 0;
if ($lines) {
$code = (int)substr($lines[0], 0, 3);
}
return [
'ok' => !$expectCodes || in_array($code, $expectCodes, true),
'code' => $code,
'lines' => $lines,
];
};
$write = function (string $cmd, string $label = 'write', bool $mask = false) use ($fp, &$transcript) {
$write = function (string $cmd, string $label = 'write', bool $mask = false) use ($fp, &$transcript): void {
$transcript[] = $label . ': ' . ($mask ? '[omitted]' : $cmd);
fwrite($fp, $cmd . "\r\n");
};
$read();
$resp = $readResponse([220], 'greeting');
if (!$resp['ok']) {
fclose($fp);
$this->log('mail_smtp_greeting_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]);
$this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]);
$this->sendMailFn($to, $subject, $html, $from, $fromName);
return;
}
$write('EHLO ' . $this->app->config()->primaryDomain);
$read();
$resp = $readResponse([250], 'ehlo');
if (!$resp['ok']) {
fclose($fp);
$this->log('mail_smtp_ehlo_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]);
$this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]);
$this->sendMailFn($to, $subject, $html, $from, $fromName);
return;
}
if ($secure === 'tls') {
$write('STARTTLS');
$read();
if (!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
$resp = $readResponse([220], 'starttls');
if (!$resp['ok'] || !stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
fclose($fp);
$this->log('mail_smtp_starttls_failed', ['host' => $host, 'port' => $port]);
$this->log('mail_smtp_starttls_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]);
$this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]);
$this->sendMailFn($to, $subject, $html, $from, $fromName);
return;
}
$write('EHLO ' . $this->app->config()->primaryDomain);
$read();
$resp = $readResponse([250], 'ehlo-tls');
if (!$resp['ok']) {
fclose($fp);
$this->log('mail_smtp_ehlo_tls_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]);
$this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]);
$this->sendMailFn($to, $subject, $html, $from, $fromName);
return;
}
}
if ($user !== '') {
$write('AUTH LOGIN');
$read();
$resp = $readResponse([334], 'auth-login');
if (!$resp['ok']) {
fclose($fp);
$this->log('mail_smtp_auth_login_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]);
$this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]);
$this->sendMailFn($to, $subject, $html, $from, $fromName);
return;
}
$write(base64_encode($user), 'auth-user', true);
$read();
$resp = $readResponse([334], 'auth-user');
if (!$resp['ok']) {
fclose($fp);
$this->log('mail_smtp_auth_user_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]);
$this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]);
$this->sendMailFn($to, $subject, $html, $from, $fromName);
return;
}
$write(base64_encode($pass), 'auth-pass', true);
$read();
$resp = $readResponse([235], 'auth-pass');
if (!$resp['ok']) {
fclose($fp);
$this->log('mail_smtp_auth_pass_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]);
$this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]);
$this->sendMailFn($to, $subject, $html, $from, $fromName);
return;
}
}
$write('MAIL FROM: <' . $from . '>');
$read();
$resp = $readResponse([250], 'mail-from');
if (!$resp['ok']) {
fclose($fp);
$this->log('mail_smtp_mailfrom_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]);
$this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]);
$this->sendMailFn($to, $subject, $html, $from, $fromName);
return;
}
$write('RCPT TO: <' . $to . '>');
$read();
$resp = $readResponse([250, 251], 'rcpt-to');
if (!$resp['ok']) {
fclose($fp);
$this->log('mail_smtp_rcpt_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]);
$this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]);
$this->sendMailFn($to, $subject, $html, $from, $fromName);
return;
}
$write('DATA');
$read();
$resp = $readResponse([354], 'data-start');
if (!$resp['ok']) {
fclose($fp);
$this->log('mail_smtp_data_start_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]);
$this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]);
$this->sendMailFn($to, $subject, $html, $from, $fromName);
return;
}
$msg = "From: {$fromName} <{$from}>\r\n";
$msg .= "To: <{$to}>\r\n";
@@ -245,14 +325,16 @@ final class Mailer
$msg .= "MIME-Version: 1.0\r\n";
$msg .= "Content-Type: text/html; charset=utf-8\r\n\r\n";
$msg .= $html . "\r\n.\r\n";
$write($msg, 'data');
$resp = $read('data-response');
$write($msg, 'data', false);
$resp = $readResponse([250], 'data-end');
$write('QUIT');
$readResponse([221], 'quit');
fclose($fp);
$this->log('mail_smtp_transcript', ['host' => $host, 'port' => $port, 'secure' => $secure, 'steps' => $transcript]);
if ($resp === false || !str_starts_with((string)$resp, '250')) {
if (!$resp['ok']) {
$this->log('mail_smtp_send_failed', ['host' => $host, 'port' => $port, 'resp' => $resp]);
$this->sendMailFn($to, $subject, $html, $from, $fromName);
return;