" : $fromEmail; if ($host === '') { $headers = [ 'MIME-Version: 1.0', 'Content-type: text/html; charset=utf-8', "From: {$fromHeader}", ]; $ok = mail($to, $subject, $html, implode("\r\n", $headers)); return ['ok' => $ok, 'error' => $ok ? '' : 'mail() failed', 'debug' => 'transport=mail()']; } $remote = $encryption === 'ssl' ? "ssl://{$host}:{$port}" : "{$host}:{$port}"; $socket = stream_socket_client($remote, $errno, $errstr, 10); if (!$socket) { return ['ok' => false, 'error' => "SMTP connect failed: {$errstr}", 'debug' => "connect={$remote} errno={$errno} err={$errstr}"]; } $debug = []; $read = function () use ($socket): string { $data = ''; while (!feof($socket)) { $line = fgets($socket, 515); if ($line === false) { break; } $data .= $line; if (isset($line[3]) && $line[3] === ' ') { break; } } return $data; }; $send = function (string $command) use ($socket, $read): string { fwrite($socket, $command . "\r\n"); return $read(); }; $resp = $read(); $debug[] = 'S: ' . trim($resp); if (!self::isOkResponse($resp)) { fclose($socket); return ['ok' => false, 'error' => 'SMTP greeting failed', 'debug' => implode("\n", $debug)]; } $resp = $send("EHLO localhost"); $debug[] = 'C: EHLO localhost'; $debug[] = 'S: ' . trim($resp); if (!self::isOkResponse($resp)) { fclose($socket); return ['ok' => false, 'error' => 'SMTP EHLO failed', 'debug' => implode("\n", $debug)]; } $authCaps = self::parseAuthCapabilities($resp); if ($encryption === 'tls') { $resp = $send("STARTTLS"); $debug[] = 'C: STARTTLS'; $debug[] = 'S: ' . trim($resp); if (substr(trim($resp), 0, 3) !== '220') { fclose($socket); return ['ok' => false, 'error' => 'SMTP STARTTLS failed', 'debug' => implode("\n", $debug)]; } if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { fclose($socket); return ['ok' => false, 'error' => 'SMTP TLS negotiation failed', 'debug' => implode("\n", $debug)]; } $resp = $send("EHLO localhost"); $debug[] = 'C: EHLO localhost'; $debug[] = 'S: ' . trim($resp); if (!self::isOkResponse($resp)) { fclose($socket); return ['ok' => false, 'error' => 'SMTP EHLO after TLS failed', 'debug' => implode("\n", $debug)]; } $authCaps = self::parseAuthCapabilities($resp); } if ($user !== '' && $pass !== '') { $authOk = false; $authErrors = []; // Prefer advertised AUTH mechanisms when available. if (in_array('PLAIN', $authCaps, true)) { $authResp = $send("AUTH PLAIN " . base64_encode("\0{$user}\0{$pass}")); $debug[] = 'C: AUTH PLAIN [credentials]'; $debug[] = 'S: ' . trim($authResp); if (substr(trim($authResp), 0, 3) === '235') { $authOk = true; } else { $authErrors[] = 'PLAIN rejected'; } } if (!$authOk && (in_array('LOGIN', $authCaps, true) || !$authCaps)) { $resp = $send("AUTH LOGIN"); $debug[] = 'C: AUTH LOGIN'; $debug[] = 'S: ' . trim($resp); if (substr(trim($resp), 0, 3) === '334') { $resp = $send(base64_encode($user)); $debug[] = 'C: [username]'; $debug[] = 'S: ' . trim($resp); if (substr(trim($resp), 0, 3) === '334') { $resp = $send(base64_encode($pass)); $debug[] = 'C: [password]'; $debug[] = 'S: ' . trim($resp); if (substr(trim($resp), 0, 3) === '235') { $authOk = true; } else { $authErrors[] = 'LOGIN password rejected'; } } else { $authErrors[] = 'LOGIN username rejected'; } } else { $authErrors[] = 'LOGIN command rejected'; } } if (!$authOk) { fclose($socket); $err = $authErrors ? implode(', ', $authErrors) : 'No supported AUTH method'; return ['ok' => false, 'error' => 'SMTP authentication failed: ' . $err, 'debug' => implode("\n", $debug)]; } } $resp = $send("MAIL FROM:<{$fromEmail}>"); $debug[] = "C: MAIL FROM:<{$fromEmail}>"; $debug[] = 'S: ' . trim($resp); if (!self::isOkResponse($resp)) { fclose($socket); return ['ok' => false, 'error' => 'SMTP MAIL FROM failed', 'debug' => implode("\n", $debug)]; } $resp = $send("RCPT TO:<{$to}>"); $debug[] = "C: RCPT TO:<{$to}>"; $debug[] = 'S: ' . trim($resp); if (!self::isOkResponse($resp)) { fclose($socket); return ['ok' => false, 'error' => 'SMTP RCPT TO failed', 'debug' => implode("\n", $debug)]; } $resp = $send("DATA"); $debug[] = 'C: DATA'; $debug[] = 'S: ' . trim($resp); if (substr(trim($resp), 0, 3) !== '354') { fclose($socket); return ['ok' => false, 'error' => 'SMTP DATA failed', 'debug' => implode("\n", $debug)]; } $headers = [ "From: {$fromHeader}", "To: {$to}", "Subject: {$subject}", "MIME-Version: 1.0", "Content-Type: text/html; charset=utf-8", ]; $message = implode("\r\n", $headers) . "\r\n\r\n" . $html . "\r\n."; fwrite($socket, $message . "\r\n"); $resp = $read(); $debug[] = 'C: [message body]'; $debug[] = 'S: ' . trim($resp); if (!self::isOkResponse($resp)) { fclose($socket); return ['ok' => false, 'error' => 'SMTP message rejected', 'debug' => implode("\n", $debug)]; } $resp = $send("QUIT"); $debug[] = 'C: QUIT'; $debug[] = 'S: ' . trim($resp); fclose($socket); return ['ok' => true, 'error' => '', 'debug' => implode("\n", $debug)]; } private static function isOkResponse(string $response): bool { $code = substr(trim($response), 0, 3); if ($code === '') { return false; } return $code[0] === '2' || $code[0] === '3'; } /** * @return string[] */ private static function parseAuthCapabilities(string $ehloResponse): array { $caps = []; foreach (preg_split('/\r?\n/', $ehloResponse) as $line) { $line = trim($line); if ($line === '') { continue; } $line = preg_replace('/^\d{3}[ -]/', '', $line) ?? $line; if (stripos($line, 'AUTH ') === 0) { $parts = preg_split('/\s+/', substr($line, 5)) ?: []; foreach ($parts as $p) { $p = strtoupper(trim($p)); if ($p !== '') { $caps[] = $p; } } } } return array_values(array_unique($caps)); } }