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.

261 lines
8.6 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';
require_once __DIR__ . '/../utils/Logger.php';
require_once __DIR__ . '/SmtpHandler.php';
class SmtpServer {
private $host;
private $port;
private $maxConnections;
private $socket;
private $connections = [];
private $logger;
private $config;
/**
* 构造函数
* @param string $host 监听地址
* @param int $port 监听端口
* @param int $maxConnections 最大连接数
*/
public function __construct($host = '0.0.0.0', $port = 25, $maxConnections = 100) {
$this->config = Config::getInstance();
$this->host = $host;
$this->port = $port;
$this->maxConnections = $maxConnections;
// 初始化日志记录器
$logPath = $this->config->get('log.path', '../logs/');
$logLevel = $this->config->get('log.level', 'info');
$this->logger = new Logger($logPath, $logLevel);
}
/**
* 启动SMTP服务器
* @return bool 是否启动成功
*/
public function start() {
// 创建socket
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($this->socket === false) {
$this->logger->error("Failed to create socket: " . socket_strerror(socket_last_error()));
return false;
}
// 设置socket选项
socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1);
// 绑定地址和端口
if (!socket_bind($this->socket, $this->host, $this->port)) {
$this->logger->error("Failed to bind socket: " . socket_strerror(socket_last_error($this->socket)));
socket_close($this->socket);
return false;
}
// 开始监听,提高监听队列长度
$backlog = $this->config->get('smtp.backlog', 50);
if (!socket_listen($this->socket, $backlog)) {
$this->logger->error("Failed to listen on socket: " . socket_strerror(socket_last_error($this->socket)));
socket_close($this->socket);
return false;
}
$this->logger->info("SMTP server started on {host}:{port}", ['host' => $this->host, 'port' => $this->port]);
// 主循环,处理连接
while (true) {
// 处理客户端连接和请求
$this->handleConnections();
}
return true;
}
/**
* 停止SMTP服务器
*/
public function stop() {
// 关闭所有客户端连接
foreach ($this->connections as $connection) {
socket_close($connection['socket']);
}
// 关闭服务器socket
if ($this->socket) {
socket_close($this->socket);
}
$this->logger->info("SMTP server stopped");
}
/**
* 处理客户端请求
*/
private function handleConnections() {
// 检查是否有客户端数据可读
$readSockets = array_column($this->connections, 'socket');
$readSockets[] = $this->socket;
$writeSockets = null;
$exceptSockets = null;
$activity = socket_select($readSockets, $writeSockets, $exceptSockets, 1);
if ($activity === false) {
$this->logger->error("Socket select error: " . socket_strerror(socket_last_error()));
return;
}
// 处理可读的socket
foreach ($readSockets as $socket) {
// 如果是服务器socket接受新连接
if ($socket === $this->socket) {
$this->acceptNewConnection();
} else {
// 处理客户端请求
$this->handleClientRequest($socket);
}
}
// 清理超时连接
$this->cleanupTimeoutConnections();
}
/**
* 接受新连接
*/
private function acceptNewConnection() {
// 检查连接数是否超过最大值
if (count($this->connections) >= $this->maxConnections) {
$this->logger->warning("Maximum connections reached: {max}", ['max' => $this->maxConnections]);
return;
}
// 接受新连接
$clientSocket = socket_accept($this->socket);
if ($clientSocket === false) {
$this->logger->error("Failed to accept connection: " . socket_strerror(socket_last_error($this->socket)));
return;
}
// 获取客户端信息
socket_getpeername($clientSocket, $clientIp, $clientPort);
$this->logger->info("New connection from {ip}:{port}", ['ip' => $clientIp, 'port' => $clientPort]);
// 添加到连接列表
$connectionId = uniqid();
$socketKey = (int)$clientSocket;
$this->connections[$connectionId] = [
'socket' => $clientSocket,
'ip' => $clientIp,
'port' => $clientPort,
'handler' => new SmtpHandler($clientSocket, $clientIp, $this->logger),
'lastActivity' => time()
];
// 维护socket到connectionId的映射
static $socketToConnection = [];
$socketToConnection[$socketKey] = $connectionId;
// 发送欢迎消息
$welcomeMsg = "220 {domain} ESMTP Service Ready\r\n";
socket_write($clientSocket, $welcomeMsg, strlen($welcomeMsg));
}
/**
* 处理客户端请求
* @param resource $socket 客户端socket
*/
private function handleClientRequest($socket) {
// 优化使用socket资源作为键直接查找
// 在acceptNewConnection中维护socket到connectionId的映射
$socketKey = (int)$socket;
static $socketToConnection = [];
// 初始化映射表(如果为空)
if (empty($socketToConnection) && !empty($this->connections)) {
foreach ($this->connections as $id => $connection) {
$socketToConnection[(int)$connection['socket']] = $id;
}
}
// 查找对应的连接
if (!isset($socketToConnection[$socketKey])) {
// 重建映射表并再次查找
$socketToConnection = [];
foreach ($this->connections as $id => $connection) {
$socketToConnection[(int)$connection['socket']] = $id;
}
if (!isset($socketToConnection[$socketKey])) {
return;
}
}
$connectionId = $socketToConnection[$socketKey];
// 读取客户端数据
$data = socket_read($socket, 1024);
if ($data === false) {
$this->logger->error("Failed to read from socket: " . socket_strerror(socket_last_error($socket)));
$this->closeConnection($connectionId);
return;
}
// 如果客户端关闭连接
if (empty($data)) {
$this->logger->info("Connection closed by client: {ip}:{port}", [
'ip' => $this->connections[$connectionId]['ip'],
'port' => $this->connections[$connectionId]['port']
]);
$this->closeConnection($connectionId);
return;
}
// 更新最后活动时间
$this->connections[$connectionId]['lastActivity'] = time();
// 处理数据
$this->connections[$connectionId]['handler']->handle($data);
}
/**
* 关闭客户端连接
* @param string $connectionId 连接ID
*/
private function closeConnection($connectionId) {
if (isset($this->connections[$connectionId])) {
$socket = $this->connections[$connectionId]['socket'];
$socketKey = (int)$socket;
// 从映射表中移除
static $socketToConnection = [];
if (isset($socketToConnection[$socketKey])) {
unset($socketToConnection[$socketKey]);
}
socket_close($socket);
unset($this->connections[$connectionId]);
}
}
/**
* 清理超时连接
*/
private function cleanupTimeoutConnections() {
$timeout = 300; // 5分钟超时
$now = time();
foreach ($this->connections as $id => $connection) {
if ($now - $connection['lastActivity'] > $timeout) {
$this->logger->info("Connection timed out: {ip}:{port}", [
'ip' => $connection['ip'],
'port' => $connection['port']
]);
$this->closeConnection($id);
}
}
}
}