socket = $socket; $this->clientIp = $clientIp; $this->logger = $logger; $this->config = Config::getInstance(); $this->maxDataSize = $this->config->get('server.max_email_size', 10485760); } public function handle($data) { if ($this->state === 'data') { $this->handleDataContent($data); } else { $lines = explode("\r\n", $data); foreach ($lines as $line) { $line = trim($line); if (empty($line)) { continue; } $this->processCommand($line); } } } private function processCommand($command) { $parts = preg_split('/\\s+/', $command, 2); $cmd = strtoupper($parts[0]); $params = isset($parts[1]) ? $parts[1] : ''; switch ($cmd) { case 'HELO': case 'EHLO': $this->heloHost = $params; $this->state = 'helo'; $this->sendResponse(250, "250 {domain} Hello {host} [{ip}]"); break; case 'MAIL': if ($this->state !== 'helo') { $this->sendResponse(503, "503 Bad sequence of commands"); return; } preg_match('/^FROM:\\s*<([^>]*)>/i', $params, $matches); if (empty($matches)) { $this->sendResponse(501, "501 Syntax error in parameters or arguments"); return; } $sender = $matches[1]; if (!filter_var($sender, FILTER_VALIDATE_EMAIL)) { $this->sendResponse(553, "553 5.1.8 Invalid sender email address"); return; } $this->fromAddress = $sender; $this->state = 'mail'; $this->toAddresses = []; $this->sendResponse(250, "250 2.1.0 Sender OK"); break; case 'RCPT': if ($this->state !== 'mail') { $this->sendResponse(503, "503 Bad sequence of commands"); return; } preg_match('/^TO:\\s*<([^>]*)>/i', $params, $matches); if (empty($matches)) { $this->sendResponse(501, "501 Syntax error in parameters or arguments"); return; } $toAddress = $matches[1]; if (!filter_var($toAddress, FILTER_VALIDATE_EMAIL)) { $this->sendResponse(553, "553 5.1.8 Invalid recipient email address"); return; } if (count($this->toAddresses) >= 100) { $this->sendResponse(452, "452 4.5.3 Too many recipients"); return; } $this->toAddresses[] = $toAddress; $this->sendResponse(250, "250 2.1.5 Recipient OK"); break; case 'DATA': if ($this->state !== 'mail' || empty($this->toAddresses)) { $this->sendResponse(503, "503 Bad sequence of commands"); return; } $this->state = 'data'; $this->dataBuffer = ''; $this->dataSize = 0; $this->sendResponse(354, "354 Start mail input; end with ."); break; case 'RSET': $this->state = 'helo'; $this->fromAddress = ''; $this->toAddresses = []; $this->dataBuffer = ''; $this->dataSize = 0; $this->sendResponse(250, "250 2.0.0 OK"); break; case 'NOOP': $this->sendResponse(250, "250 2.0.0 OK"); break; case 'QUIT': $this->sendResponse(221, "221 2.0.0 Bye"); break; default: $this->sendResponse(500, "500 Syntax error, command unrecognized"); break; } } private function handleDataContent($data) { $this->dataBuffer .= $data; $this->dataSize += strlen($data); if ($this->dataSize > $this->maxDataSize) { $this->sendResponse(552, "552 5.2.3 Message exceeds maximum size"); $this->state = 'helo'; $this->dataBuffer = ''; $this->dataSize = 0; return; } if (substr($this->dataBuffer, -5) === "\r\n.\r\n") { $emailContent = substr($this->dataBuffer, 0, -5); $this->processEmail($emailContent); $this->state = 'helo'; $this->dataBuffer = ''; $this->dataSize = 0; } } private function processEmail($emailContent) { $headers = $this->parseHeaders($emailContent); $subject = $headers['Subject'] ?? ''; $body = $this->extractBody($emailContent); $this->saveEmail($headers, $subject, $body, $emailContent); $this->sendResponse(250, "250 2.0.0 OK: queued as {message_id}"); } private function parseHeaders($emailContent) { $headers = []; $lines = explode("\r\n", $emailContent); foreach ($lines as $line) { if (empty($line)) { break; } if (preg_match('/^\\s+/', $line)) { $last = array_key_last($headers); $headers[$last] = isset($headers[$last]) ? $headers[$last] . ' ' . trim($line) : trim($line); } else { $parts = explode(':', $line, 2); if (count($parts) === 2) { $name = trim($parts[0]); $value = trim($parts[1]); $headers[$name] = $value; } } } return $headers; } private function extractBody($emailContent) { $parts = explode("\r\n\r\n", $emailContent, 2); return isset($parts[1]) ? $parts[1] : ''; } private function saveEmail($headers, $subject, $body, $raw) { try { require_once __DIR__ . '/../utils/Database.php'; $db = Database::getInstance(); foreach ($this->toAddresses as $toAddress) { $username = null; $user = $db->fetchOne("SELECT username FROM user WHERE email = ? AND is_deleted = 0", [$toAddress]); if ($user && isset($user['username'])) { $username = $user['username']; } if (!$username) { $parts = explode('@', $toAddress); $username = $parts[0] ?? $toAddress; } $id = uniqid('', true); $dateHeader = $headers['Date'] ?? date('Y-m-d H:i:s'); $toHeader = $headers['To'] ?? implode(', ', $this->toAddresses); $fromHeader = $headers['From'] ?? $this->fromAddress; $length = strlen($raw); $sql = "INSERT INTO email (id, ip, helo, mail_from, rcpt_to, `from`, `to`, subject, `date`, `data`, datagram, `length`, `state`, is_read, folder, created_at, is_deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), 0)"; $db->insert($sql, [$id, $this->clientIp, $this->heloHost, $this->fromAddress, $username, $fromHeader, $toHeader, $subject, $dateHeader, $body, $raw, $length, 1, 0, 'inbox']); } } catch (Exception $e) { $this->logger->error("Failed to save email: {error}", ['error' => $e->getMessage()]); } } private function sendResponse($code, $message) { $domain = $this->config->get('server.domain', 'test.com'); $message = str_replace('{domain}', $domain, $message); $message = str_replace('{ip}', $this->clientIp, $message); $message = str_replace('{host}', $this->heloHost, $message); $response = $message . "\r\n"; socket_write($this->socket, $response, strlen($response)); } }