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.

109 lines
5.9 KiB

<?php
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;
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);
}
public function start() {
$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_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;
}
public function stop() {
foreach ($this->connections as $connection) { socket_close($connection['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; }
foreach ($readSockets as $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()
];
static $socketToConnection = [];
$socketToConnection[$socketKey] = $connectionId;
$welcomeMsg = "220 {domain} ESMTP Service Ready\r\n";
socket_write($clientSocket, $welcomeMsg, strlen($welcomeMsg));
}
private function handleClientRequest($socket) {
$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);
}
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;
$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);
}
}
}
}