|
|
<?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)
|
|
|
]);
|
|
|
}
|
|
|
}
|