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 ."); $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) ]); } }