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