文件
2026-08-16 17:03:10 +08:00

158 行
5.2 KiB
PHP

<?php
if (!defined('IN_APP')) {
http_response_code(403);
exit('Forbidden');
}
class SmtpMailer
{
public $log = [];
public $lastError = '';
private $host;
private $port;
private $enc;
private $user;
private $pass;
public function __construct($host, $port, $enc, $user, $pass)
{
$this->host = (string) $host;
$this->port = (int) $port;
$this->enc = (string) $enc;
$this->user = (string) $user;
$this->pass = (string) $pass;
}
public function send($to, $subject, $bodyHtml, $fromEmail, $fromName)
{
if ($this->host === '') {
$this->lastError = 'SMTP 主机未配置';
return false;
}
$port = $this->port ?: ($this->enc === 'ssl' ? 465 : 587);
$timeout = 20;
$scheme = $this->enc === 'ssl' ? 'ssl' : 'tcp';
$opts = [];
if ($this->enc === 'ssl' || $this->enc === 'tls') {
$opts['ssl'] = [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
];
}
$ctx = $opts ? stream_context_create($opts) : null;
$errno = 0;
$errstr = '';
$sock = @stream_socket_client(
$scheme . ':
$errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $ctx
);
if (!$sock) {
$this->lastError = "无法连接 SMTP 服务器({$this->host}:{$port}):$errstr ($errno)";
return false;
}
stream_set_timeout($sock, $timeout);
if ($this->talk($sock, null, [220]) === false) {
fclose($sock);
return false;
}
$hostHeader = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost';
if ($this->talk($sock, 'EHLO ' . $hostHeader, [250]) === false) {
fclose($sock);
return false;
}
if ($this->enc === 'tls') {
if ($this->talk($sock, 'STARTTLS', [220]) === false) {
fclose($sock);
return false;
}
if (!@stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
$this->lastError = 'STARTTLS 加密协商失败';
fclose($sock);
return false;
}
if ($this->talk($sock, 'EHLO ' . $hostHeader, [250]) === false) {
fclose($sock);
return false;
}
}
if ($this->talk($sock, 'AUTH LOGIN', [334]) === false) {
fclose($sock);
return false;
}
if ($this->talk($sock, base64_encode($this->user), [334]) === false) {
fclose($sock);
return false;
}
if ($this->talk($sock, base64_encode($this->pass), [235]) === false) {
fclose($sock);
return false;
}
if ($this->talk($sock, 'MAIL FROM:<' . $fromEmail . '>', [250]) === false) {
fclose($sock);
return false;
}
$recipients = array_filter(array_map('trim', explode(',', $to)));
foreach ($recipients as $rcpt) {
$this->talk($sock, 'RCPT TO:<' . $rcpt . '>', [250, 251]);
}
if ($this->talk($sock, 'DATA', [354]) === false) {
fclose($sock);
return false;
}
$data = $this->buildData($to, $subject, $bodyHtml, $fromEmail, $fromName);
fwrite($sock, $data);
if ($this->talk($sock, '.', [250]) === false) {
fclose($sock);
return false;
}
$this->talk($sock, 'QUIT', [221]);
fclose($sock);
return true;
}
private function buildData($to, $subject, $body, $fromEmail, $fromName)
{
$eol = "\r\n";
$headers = [];
$headers[] = 'From: ' . $this->encodeHeader($fromName) . ' <' . $fromEmail . '>';
$headers[] = 'To: ' . $to;
$headers[] = 'Subject: ' . $this->encodeHeader($subject);
$headers[] = 'Date: ' . date('r');
$headers[] = 'MIME-Version: 1.0';
$headers[] = 'Content-Type: text/html; charset=UTF-8';
$headers[] = 'Content-Transfer-Encoding: base64';
$msg = implode($eol, $headers) . $eol . $eol;
$msg .= chunk_split(base64_encode($body), 76, $eol);
return $msg;
}
private function encodeHeader($s)
{
if (preg_match('/[^\x00-\x7F]/', (string) $s)) {
return '=?UTF-8?B?' . base64_encode($s) . '?=';
}
return $s;
}
private function talk($sock, $cmd, $expectCodes)
{
if ($cmd !== null) {
fwrite($sock, $cmd . "\r\n");
$this->log[] = 'C: ' . $cmd;
}
$resp = '';
while (true) {
$line = @fgets($sock, 515);
if ($line === false) {
break;
}
$resp .= $line;
if (isset($line[3]) && $line[3] === ' ') {
break;
}
}
$this->log[] = 'S: ' . trim($resp);
$code = (int) substr($resp, 0, 3);
if (!in_array($code, (array) $expectCodes, true)) {
$this->lastError = '服务器返回错误:' . trim($resp);
return false;
}
return $resp;
}
}