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