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