CRMEB/backend/driver/email/Qq/Driver.php
2026-09-23 15:15:02 +08:00

197 lines
7.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace driver\email\Qq;
use crmeb\services\email\BaseEmail;
use crmeb\services\email\EmailInterface;
class Driver extends BaseEmail implements EmailInterface
{
protected $config = [];
protected function initialize(array $config)
{
$this->config = $config;
}
public function send(string $to, string $subject, string $content, array $options = []): bool
{
$host = $this->config['qq_host'] ?? 'smtp.qq.com';
$port = (int)($this->config['qq_port'] ?? 465);
$username = $this->config['qq_username'] ?? '';
$authCode = $this->config['qq_auth_code'] ?? '';
$fromName = $this->config['from_name'] ?? 'CRMEB';
if (empty($host) || empty($username) || empty($authCode)) {
return $this->setError('QQ邮箱配置不完整');
}
try {
$socketType = $port === 465 ? 'ssl' : ($port === 587 ? 'tls' : 'tcp');
$context = stream_context_create([
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
],
]);
// 出口网络对 465/587 的 TLS 握手存在秒级瞬断/风控限流窗口(stream_socket_client
// 失败且 errno=0,如 Failed to enable crypto),连接失败按递增间隔自动重试
$socket = false;
$lastError = '';
for ($attempt = 1; $attempt <= 3; $attempt++) {
$socket = @stream_socket_client(
"{$socketType}://{$host}:{$port}",
$errno,
$errstr,
30,
STREAM_CLIENT_CONNECT,
$context
);
if ($socket) {
break;
}
$lastError = "QQ邮箱SMTP连接失败: {$errstr} ({$errno})";
if ($attempt < 3) {
usleep($attempt * 600000);
}
}
if (!$socket) {
return $this->setError($lastError);
}
stream_set_timeout($socket, 30);
if (!$this->checkResponse($socket, 220)) {
fclose($socket);
return $this->setError('QQ邮箱SMTP服务器响应异常');
}
$this->sendCommand($socket, "EHLO " . gethostname());
if (!$this->checkResponse($socket, 250)) {
fclose($socket);
return $this->setError('QQ邮箱SMTP EHLO失败');
}
if ($port === 587) {
$this->sendCommand($socket, "STARTTLS");
if (!$this->checkResponse($socket, 220)) {
fclose($socket);
return $this->setError('QQ邮箱SMTP STARTTLS失败');
}
stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
$this->sendCommand($socket, "EHLO " . gethostname());
if (!$this->checkResponse($socket, 250)) {
fclose($socket);
return $this->setError('QQ邮箱SMTP EHLO(加密后)失败');
}
}
$this->sendCommand($socket, "AUTH LOGIN");
if (!$this->checkResponse($socket, 334)) {
fclose($socket);
return $this->setError('QQ邮箱SMTP AUTH失败');
}
$this->sendCommand($socket, base64_encode($username));
if (!$this->checkResponse($socket, 334)) {
fclose($socket);
return $this->setError('QQ邮箱SMTP用户名认证失败');
}
$this->sendCommand($socket, base64_encode($authCode));
if (!$this->checkResponse($socket, 235)) {
fclose($socket);
return $this->setError('QQ邮箱SMTP授权码认证失败');
}
$this->sendCommand($socket, "MAIL FROM:<{$username}>");
if (!$this->checkResponse($socket, 250)) {
fclose($socket);
return $this->setError('QQ邮箱SMTP MAIL FROM失败');
}
$this->sendCommand($socket, "RCPT TO:<{$to}>");
if (!$this->checkResponse($socket, 250)) {
fclose($socket);
return $this->setError('QQ邮箱SMTP RCPT TO失败');
}
$this->sendCommand($socket, "DATA");
if (!$this->checkResponse($socket, 354)) {
fclose($socket);
return $this->setError('QQ邮箱SMTP DATA失败');
}
$headers = $this->buildHeaders($username, $fromName, $to, $subject);
// 正文按声明的 Content-Transfer-Encoding: base64 真实编码(chunk_split 每 76 字符换行,
// 避免 SMTP 998 字节行限制与点透明问题);headers 与正文之间以空行分隔
$body = chunk_split(base64_encode($content));
$message = $headers . "\r\n\r\n" . $body . "\r\n.";
$this->sendCommand($socket, $message);
if (!$this->checkResponse($socket, 250)) {
fclose($socket);
return $this->setError('QQ邮箱邮件发送失败');
}
$this->sendCommand($socket, "QUIT");
fclose($socket);
return true;
} catch (\Throwable $e) {
return $this->setError('QQ邮箱邮件发送异常: ' . $e->getMessage());
}
}
protected function sendCommand($socket, string $command): void
{
fwrite($socket, $command . "\r\n");
}
protected function checkResponse($socket, int $expectedCode): bool
{
$response = '';
do {
$line = fgets($socket, 512);
if ($line === false) {
return false;
}
$response .= $line;
} while (substr($line, 3, 1) === '-');
$code = (int)substr($response, 0, 3);
return $code === $expectedCode;
}
protected function buildHeaders(string $username, string $fromName, string $to, string $subject): string
{
$fromNameEncoded = $this->encodeHeader($fromName);
$subjectEncoded = $this->encodeHeader($subject);
$headers = [];
$headers[] = "MIME-Version: 1.0";
$headers[] = "Content-Type: text/html; charset=UTF-8";
$headers[] = "Content-Transfer-Encoding: base64";
$headers[] = "From: {$fromNameEncoded} <{$username}>";
$headers[] = "To: <{$to}>";
$headers[] = "Subject: {$subjectEncoded}";
$headers[] = "Date: " . date('r');
$headers[] = "Message-ID: <" . md5(uniqid((string)time())) . "@qq.com>";
$headers[] = "Reply-To: {$username}";
return implode("\r\n", $headers);
}
protected function encodeHeader(string $text): string
{
if (preg_match('/[\x80-\xff]/', $text)) {
return "=?UTF-8?B?" . base64_encode($text) . "?=";
}
return $text;
}
}