You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

434 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
/**
* SMTP协议处理类
*/
require_once __DIR__ . '/../utils/Config.php';
class SmtpHandler {
private $socket;
private $clientIp;
private $logger;
private $config;
private $state = 'init'; // 状态init, helo, mail, rcpt, data, quit
private $heloHost = '';
private $fromAddress = '';
private $toAddresses = [];
private $dataBuffer = '';
private $dataSize = 0;
private $maxDataSize = 10 * 1024 * 1024; // 10MB
/**
* 构造函数
* @param resource $socket 客户端socket
* @param string $clientIp 客户端IP地址
* @param Logger $logger 日志记录器
*/
public function __construct($socket, $clientIp, $logger) {
$this->socket = $socket;
$this->clientIp = $clientIp;
$this->logger = $logger;
$this->config = Config::getInstance();
$this->maxDataSize = $this->config->get('server.max_email_size', 10 * 1024 * 1024);
}
/**
* 处理客户端数据
* @param string $data 客户端发送的数据
*/
public function handle($data) {
$this->logger->debug("Received data from {ip}: {data}", [
'ip' => $this->clientIp,
'data' => rtrim($data)
]);
// 如果处于数据状态,直接处理数据
if ($this->state === 'data') {
$this->handleDataContent($data);
} else {
// 按行处理命令
$lines = explode("\r\n", $data);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
$this->processCommand($line);
}
}
}
/**
* 处理SMTP命令
* @param string $command SMTP命令
*/
private function processCommand($command) {
// 解析命令和参数
$parts = preg_split('/\s+/', $command, 2);
$cmd = strtoupper($parts[0]);
$params = isset($parts[1]) ? $parts[1] : '';
// 根据命令调用相应的处理方法
switch ($cmd) {
case 'HELO':
case 'EHLO':
$this->handleHelo($params);
break;
case 'MAIL':
$this->handleMail($params);
break;
case 'RCPT':
$this->handleRcpt($params);
break;
case 'DATA':
$this->handleData();
break;
case 'RSET':
$this->handleRset();
break;
case 'NOOP':
$this->handleNoop();
break;
case 'QUIT':
$this->handleQuit();
break;
default:
$this->sendResponse(500, "500 Syntax error, command unrecognized");
break;
}
}
/**
* 处理HELO/EHLO命令
* @param string $params 命令参数
*/
private function handleHelo($params) {
$this->heloHost = $params;
$this->state = 'helo';
$this->sendResponse(250, "250 {domain} Hello {host} [{ip}]");
$this->logger->info("HELO/EHLO command received from {ip}, host: {host}", [
'ip' => $this->clientIp,
'host' => $this->heloHost
]);
}
/**
* 处理MAIL FROM命令
* @param string $params 命令参数
*/
private function handleMail($params) {
if ($this->state !== 'helo') {
$this->sendResponse(503, "503 Bad sequence of commands");
$this->logger->warning("MAIL FROM command out of sequence from {ip}", [
'ip' => $this->clientIp
]);
return;
}
// 解析发件人地址
preg_match('/^FROM:\s*<([^>]*)>/i', $params, $matches);
if (empty($matches)) {
$this->sendResponse(501, "501 Syntax error in parameters or arguments");
$this->logger->warning("Invalid MAIL FROM syntax from {ip}: {params}", [
'ip' => $this->clientIp,
'params' => $params
]);
return;
}
$sender = $matches[1];
// 验证发件人邮箱格式
if (!filter_var($sender, FILTER_VALIDATE_EMAIL)) {
$this->sendResponse(553, "553 5.1.8 Invalid sender email address");
$this->logger->warning("Invalid sender email from {ip}: {sender}", [
'ip' => $this->clientIp,
'sender' => $sender
]);
return;
}
$this->fromAddress = $sender;
$this->state = 'mail';
$this->toAddresses = [];
$this->sendResponse(250, "250 2.1.0 Sender OK");
$this->logger->info("MAIL FROM command received from {ip}, from: {from}", [
'ip' => $this->clientIp,
'from' => $this->fromAddress
]);
}
/**
* 处理RCPT TO命令
* @param string $params 命令参数
*/
private function handleRcpt($params) {
if ($this->state !== 'mail') {
$this->sendResponse(503, "503 Bad sequence of commands");
$this->logger->warning("RCPT TO command out of sequence from {ip}", [
'ip' => $this->clientIp
]);
return;
}
// 解析收件人地址
preg_match('/^TO:\s*<([^>]*)>/i', $params, $matches);
if (empty($matches)) {
$this->sendResponse(501, "501 Syntax error in parameters or arguments");
$this->logger->warning("Invalid RCPT TO syntax from {ip}: {params}", [
'ip' => $this->clientIp,
'params' => $params
]);
return;
}
$toAddress = $matches[1];
// 验证收件人邮箱格式
if (!filter_var($toAddress, FILTER_VALIDATE_EMAIL)) {
$this->sendResponse(553, "553 5.1.8 Invalid recipient email address");
$this->logger->warning("Invalid recipient email from {ip}: {to}", [
'ip' => $this->clientIp,
'to' => $toAddress
]);
return;
}
// 检查收件人数量是否超过限制
if (count($this->toAddresses) >= 100) {
$this->sendResponse(452, "452 4.5.3 Too many recipients");
$this->logger->warning("Too many recipients from {ip}", [
'ip' => $this->clientIp
]);
return;
}
$this->toAddresses[] = $toAddress;
$this->sendResponse(250, "250 2.1.5 Recipient OK");
$this->logger->info("RCPT TO command received from {ip}, to: {to}", [
'ip' => $this->clientIp,
'to' => $toAddress
]);
}
/**
* 处理DATA命令
*/
private function handleData() {
if ($this->state !== 'mail' || empty($this->toAddresses)) {
$this->sendResponse(503, "503 Bad sequence of commands");
return;
}
$this->state = 'data';
$this->dataBuffer = '';
$this->dataSize = 0;
$this->sendResponse(354, "354 Start mail input; end with <CRLF>.<CRLF>");
$this->logger->info("DATA command received from {ip}", [
'ip' => $this->clientIp
]);
}
/**
* 处理RSET命令
*/
private function handleRset() {
$this->state = 'helo';
$this->fromAddress = '';
$this->toAddresses = [];
$this->dataBuffer = '';
$this->dataSize = 0;
$this->sendResponse(250, "250 2.0.0 OK");
$this->logger->info("RSET command received from {ip}", [
'ip' => $this->clientIp
]);
}
/**
* 处理NOOP命令
*/
private function handleNoop() {
$this->sendResponse(250, "250 2.0.0 OK");
$this->logger->info("NOOP command received from {ip}", [
'ip' => $this->clientIp
]);
}
/**
* 处理DATA内容
* @param string $data 邮件内容
*/
private function handleDataContent($data) {
// 添加到缓冲区
$this->dataBuffer .= $data;
$this->dataSize += strlen($data);
// 检查数据大小是否超过限制
if ($this->dataSize > $this->maxDataSize) {
$this->sendResponse(552, "552 5.2.3 Message exceeds maximum size");
$this->state = 'helo';
$this->dataBuffer = '';
$this->dataSize = 0;
$this->logger->warning("Message from {ip} exceeds maximum size", [
'ip' => $this->clientIp
]);
return;
}
// 检查是否收到结束标记
if (substr($this->dataBuffer, -5) === "\r\n.\r\n") {
// 去除结束标记
$emailContent = substr($this->dataBuffer, 0, -5);
// 处理邮件
$this->processEmail($emailContent);
// 重置状态
$this->state = 'helo';
$this->dataBuffer = '';
$this->dataSize = 0;
}
}
/**
* 处理并保存邮件
* @param string $emailContent 邮件内容
*/
private function processEmail($emailContent) {
// 解析邮件头
$headers = $this->parseEmailHeaders($emailContent);
// 提取邮件主题和正文
$subject = $headers['Subject'] ?? '无主题';
$body = $this->extractEmailBody($emailContent);
// 保存邮件到数据库
$this->saveEmailToDatabase($headers, $subject, $body);
// 发送成功响应
$this->sendResponse(250, "250 2.0.0 OK: queued as {message_id}");
$this->logger->info("Email from {from} to {to} saved successfully", [
'from' => $this->fromAddress,
'to' => implode(', ', $this->toAddresses)
]);
}
/**
* 解析邮件头
* @param string $emailContent 邮件内容
* @return array 邮件头
*/
private function parseEmailHeaders($emailContent) {
$headers = [];
$lines = explode("\r\n", $emailContent);
// 解析邮件头
foreach ($lines as $line) {
if (empty($line)) {
break; // 邮件头结束
}
// 处理多行邮件头
if (preg_match('/^\s+/', $line)) {
// 多行邮件头,追加到上一个头
$lastHeader = array_key_last($headers);
$headers[$lastHeader] .= ' ' . trim($line);
} else {
// 新的邮件头
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$name = trim($parts[0]);
$value = trim($parts[1]);
$headers[$name] = $value;
}
}
}
return $headers;
}
/**
* 提取邮件正文
* @param string $emailContent 邮件内容
* @return string 邮件正文
*/
private function extractEmailBody($emailContent) {
$parts = explode("\r\n\r\n", $emailContent, 2);
return isset($parts[1]) ? $parts[1] : '';
}
/**
* 保存邮件到数据库
* @param array $headers 邮件头
* @param string $subject 邮件主题
* @param string $body 邮件正文
*/
private function saveEmailToDatabase($headers, $subject, $body) {
try {
// 包含数据库类
require_once __DIR__ . '/../utils/Database.php';
// 连接数据库
$db = Database::getInstance();
// 遍历收件人地址
foreach ($this->toAddresses as $toAddress) {
// 根据收件人邮箱查找对应的用户名
$sql = "SELECT username FROM user WHERE email = ? AND is_deleted = 0";
$user = $db->fetchOne($sql, [$toAddress]);
$userId = $user ? $user['username'] : 'admin'; // 如果找不到使用默认用户名admin为管理员
// 保存邮件
$sql = "INSERT INTO emails (from_address, to_address, subject, content, user_id, send_time) VALUES (?, ?, ?, ?, ?, NOW())";
$db->insert($sql, [
$this->fromAddress,
$toAddress,
$subject,
$body,
$userId
]);
}
} catch (Exception $e) {
$this->logger->error("Failed to save email: {error}", [
'error' => $e->getMessage()
]);
}
}
/**
* 处理QUIT命令
*/
private function handleQuit() {
$this->sendResponse(221, "221 2.0.0 Bye");
$this->logger->info("QUIT command received from {ip}", [
'ip' => $this->clientIp
]);
// 不再直接关闭socket而是让SmtpServer通过检测连接关闭来处理
}
/**
* 发送响应给客户端
* @param int $code 响应代码
* @param string $message 响应消息
*/
private function sendResponse($code, $message) {
// 替换占位符
$domain = $this->config->get('server.domain', 'test.com');
$message = str_replace('{domain}', $domain, $message);
$message = str_replace('{ip}', $this->clientIp, $message);
$message = str_replace('{host}', $this->heloHost, $message);
// 添加换行符
$response = $message . "\r\n";
// 发送响应
socket_write($this->socket, $response, strlen($response));
$this->logger->debug("Sent response to {ip}: {response}", [
'ip' => $this->clientIp,
'response' => rtrim($response)
]);
}
}