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.

527 lines
17 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
/**
* POP3协议处理类
*/
require_once __DIR__ . '/../utils/Config.php';
class Pop3Handler {
private $socket;
private $clientIp;
private $logger;
private $config;
private $state = 'auth'; // 状态auth, transaction, update
private $username = '';
private $password = '';
private $authenticated = false;
private $messages = [];
private $deletedMessages = [];
private $messageCount = 0;
private $mailboxSize = 0;
/**
* 构造函数
* @param resource $socket 客户端socket
* @param string $clientIp 客户端IP地址
* @param Logger $logger 日志记录器
*/
public function __construct($socket, $clientIp, $logger) {
$this->socket = $socket;
$this->clientIp = $clientIp;
$this->logger = $logger;
$this->config = Config::getInstance();
}
/**
* 处理客户端数据
* @param string $data 客户端发送的数据
*/
public function handle($data) {
$this->logger->debug("Received data from {ip}: {data}", [
'ip' => $this->clientIp,
'data' => rtrim($data)
]);
// 按行处理数据
$lines = explode("\r\n", $data);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
$this->processCommand($line);
}
}
/**
* 处理POP3命令
* @param string $command POP3命令
*/
private function processCommand($command) {
// 解析命令和参数
$parts = preg_split('/\s+/', $command, 2);
$cmd = strtoupper($parts[0]);
$params = isset($parts[1]) ? $parts[1] : '';
// 根据命令调用相应的处理方法
switch ($cmd) {
case 'USER':
$this->handleUser($params);
break;
case 'PASS':
$this->handlePass($params);
break;
case 'STAT':
$this->handleStat();
break;
case 'LIST':
$this->handleList($params);
break;
case 'RETR':
$this->handleRetr($params);
break;
case 'DELE':
$this->handleDele($params);
break;
case 'NOOP':
$this->handleNoop();
break;
case 'RSET':
$this->handleRset();
break;
case 'QUIT':
$this->handleQuit();
break;
case 'TOP':
$this->handleTop($params);
break;
case 'UIDL':
$this->handleUidl($params);
break;
default:
$this->sendResponse(false, "Unknown command");
break;
}
}
/**
* 处理USER命令
* @param string $username 用户名
*/
private function handleUser($username) {
if ($this->state !== 'auth') {
$this->sendResponse(false, "Bad sequence of commands");
$this->logger->warning("USER command out of sequence from {ip}", [
'ip' => $this->clientIp
]);
return;
}
// 验证用户名格式
if (empty($username) || strlen($username) > 50) {
$this->sendResponse(false, "Invalid username");
$this->logger->warning("Invalid username format from {ip}: {username}", [
'ip' => $this->clientIp,
'username' => $username
]);
return;
}
$this->username = $username;
$this->sendResponse(true, "User accepted");
$this->logger->info("USER command received from {ip}, username: {username}", [
'ip' => $this->clientIp,
'username' => $this->username
]);
}
/**
* 处理PASS命令
* @param string $password 密码
*/
private function handlePass($password) {
if ($this->state !== 'auth' || empty($this->username)) {
$this->sendResponse(false, "Bad sequence of commands");
$this->logger->warning("PASS command out of sequence from {ip}", [
'ip' => $this->clientIp
]);
return;
}
$this->password = $password;
try {
// 调用数据库接口验证用户身份
require_once __DIR__ . '/../utils/Database.php';
$db = Database::getInstance();
// 查询用户
$sql = "SELECT * FROM user WHERE username = ? AND is_deleted = 0";
$user = $db->query($sql, [$this->username])->fetch();
if ($user && password_verify($password, $user['password'])) {
$this->authenticated = true;
$this->state = 'transaction';
// 初始化邮件列表(从数据库获取)
$this->initMessages($user['username']);
$this->sendResponse(true, "Authentication successful");
$this->logger->info("PASS command received from {ip}, authentication successful for user: {username}", [
'ip' => $this->clientIp,
'username' => $this->username
]);
} else {
$this->sendResponse(false, "Invalid username or password");
$this->logger->warning("Authentication failed from {ip}, username: {username}", [
'ip' => $this->clientIp,
'username' => $this->username
]);
}
} catch (Exception $e) {
$this->sendResponse(false, "Authentication error");
$this->logger->error("Authentication exception from {ip}: {error}", [
'ip' => $this->clientIp,
'error' => $e->getMessage()
]);
}
}
/**
* 处理STAT命令
*/
private function handleStat() {
if (!$this->authenticated || $this->state !== 'transaction') {
$this->sendResponse(false, "Not authenticated");
return;
}
$this->sendResponse(true, "{$this->messageCount} {$this->mailboxSize}");
$this->logger->info("STAT command received from {ip}", [
'ip' => $this->clientIp
]);
}
/**
* 处理LIST命令
* @param string $messageId 邮件ID可选
*/
private function handleList($messageId = '') {
if (!$this->authenticated || $this->state !== 'transaction') {
$this->sendResponse(false, "Not authenticated");
return;
}
if (empty($messageId)) {
// 列出所有邮件
$response = "{$this->messageCount} messages\r\n";
foreach ($this->messages as $id => $message) {
$response .= "{$id} {$message['size']}\r\n";
}
$response .= ".\r\n";
$this->sendRawResponse("+OK " . $response);
} else {
// 列出指定邮件
$id = (int)$messageId;
if (isset($this->messages[$id])) {
$this->sendResponse(true, "{$id} {$this->messages[$id]['size']}");
} else {
$this->sendResponse(false, "No such message");
}
}
$this->logger->info("LIST command received from {ip}, messageId: {messageId}", [
'ip' => $this->clientIp,
'messageId' => $messageId
]);
}
/**
* 处理RETR命令
* @param string $messageId 邮件ID
*/
private function handleRetr($messageId) {
if (!$this->authenticated || $this->state !== 'transaction') {
$this->sendResponse(false, "Not authenticated");
return;
}
$id = (int)$messageId;
if (!isset($this->messages[$id])) {
$this->sendResponse(false, "No such message");
return;
}
$message = $this->messages[$id];
$response = "{$message['size']} octets\r\n";
$response .= $message['content'] . "\r\n";
$response .= ".\r\n";
$this->sendRawResponse("+OK " . $response);
$this->logger->info("RETR command received from {ip}, messageId: {messageId}", [
'ip' => $this->clientIp,
'messageId' => $messageId
]);
}
/**
* 处理DELE命令
* @param string $messageId 邮件ID
*/
private function handleDele($messageId) {
if (!$this->authenticated || $this->state !== 'transaction') {
$this->sendResponse(false, "Not authenticated");
return;
}
$id = (int)$messageId;
if (!isset($this->messages[$id])) {
$this->sendResponse(false, "No such message");
return;
}
$this->deletedMessages[$id] = true;
$this->sendResponse(true, "Message {$id} marked for deletion");
$this->logger->info("DELE command received from {ip}, messageId: {messageId}", [
'ip' => $this->clientIp,
'messageId' => $messageId
]);
}
/**
* 处理NOOP命令
*/
private function handleNoop() {
if (!$this->authenticated) {
$this->sendResponse(false, "Not authenticated");
return;
}
$this->sendResponse(true, "OK");
$this->logger->info("NOOP command received from {ip}", [
'ip' => $this->clientIp
]);
}
/**
* 处理RSET命令
*/
private function handleRset() {
if (!$this->authenticated || $this->state !== 'transaction') {
$this->sendResponse(false, "Not authenticated");
return;
}
$this->deletedMessages = [];
$this->sendResponse(true, "Reset completed");
$this->logger->info("RSET command received from {ip}", [
'ip' => $this->clientIp
]);
}
/**
* 处理QUIT命令
*/
private function handleQuit() {
$this->state = 'update';
$deletedCount = 0;
// 删除标记的邮件
if (!empty($this->deletedMessages)) {
try {
require_once __DIR__ . '/../utils/Database.php';
$db = Database::getInstance();
$pdo = $db->beginTransaction();
foreach ($this->deletedMessages as $id => $value) {
if (isset($this->messages[$id]) && isset($this->messages[$id]['email_id'])) {
$emailId = $this->messages[$id]['email_id'];
$sql = "DELETE FROM emails WHERE id = ?";
$db->execute($sql, [$emailId]);
$deletedCount++;
}
}
$db->commit($pdo);
} catch (Exception $e) {
$db->rollback($pdo);
$this->logger->error("Failed to delete messages: {error}", [
'error' => $e->getMessage()
]);
}
}
$this->sendResponse(true, "Bye");
$this->logger->info("QUIT command received from {ip}, deleted {count} messages", [
'ip' => $this->clientIp,
'count' => $deletedCount
]);
// 不再直接关闭socket而是让Pop3Server通过检测连接关闭来处理
}
/**
* 处理TOP命令
* @param string $params 命令参数
*/
private function handleTop($params) {
if (!$this->authenticated || $this->state !== 'transaction') {
$this->sendResponse(false, "Not authenticated");
return;
}
$parts = explode(' ', $params);
if (count($parts) !== 2) {
$this->sendResponse(false, "Invalid parameters");
return;
}
$id = (int)$parts[0];
$lines = (int)$parts[1];
if (!isset($this->messages[$id])) {
$this->sendResponse(false, "No such message");
return;
}
$message = $this->messages[$id];
$headers = substr($message['content'], 0, strpos($message['content'], "\r\n\r\n"));
$contentLines = explode("\r\n", substr($message['content'], strpos($message['content'], "\r\n\r\n") + 4));
$topContent = implode("\r\n", array_slice($contentLines, 0, $lines));
$response = "Top of message {$id}\r\n";
$response .= $headers . "\r\n\r\n" . $topContent . "\r\n";
$response .= ".\r\n";
$this->sendRawResponse("+OK " . $response);
$this->logger->info("TOP command received from {ip}, messageId: {id}, lines: {lines}", [
'ip' => $this->clientIp,
'id' => $id,
'lines' => $lines
]);
}
/**
* 处理UIDL命令
* @param string $messageId 邮件ID可选
*/
private function handleUidl($messageId = '') {
if (!$this->authenticated || $this->state !== 'transaction') {
$this->sendResponse(false, "Not authenticated");
return;
}
if (empty($messageId)) {
// 列出所有邮件的UID
$response = "{$this->messageCount} messages\r\n";
foreach ($this->messages as $id => $message) {
$response .= "{$id} {$message['uid']}\r\n";
}
$response .= ".\r\n";
$this->sendRawResponse("+OK " . $response);
} else {
// 列出指定邮件的UID
$id = (int)$messageId;
if (isset($this->messages[$id])) {
$this->sendResponse(true, "{$id} {$this->messages[$id]['uid']}");
} else {
$this->sendResponse(false, "No such message");
}
}
$this->logger->info("UIDL command received from {ip}, messageId: {messageId}", [
'ip' => $this->clientIp,
'messageId' => $messageId
]);
}
/**
* 发送响应给客户端
* @param bool $success 是否成功
* @param string $message 响应消息
*/
private function sendResponse($success, $message) {
$prefix = $success ? "+OK" : "-ERR";
$response = "$prefix $message\r\n";
socket_write($this->socket, $response, strlen($response));
$this->logger->debug("Sent response to {ip}: {response}", [
'ip' => $this->clientIp,
'response' => rtrim($response)
]);
}
/**
* 发送原始响应给客户端
* @param string $response 响应消息
*/
private function sendRawResponse($response) {
socket_write($this->socket, $response, strlen($response));
$this->logger->debug("Sent raw response to {ip}: {response}", [
'ip' => $this->clientIp,
'response' => rtrim($response)
]);
}
/**
* 初始化邮件列表(从数据库获取)
* @param string $username 用户名
*/
private function initMessages($username) {
try {
require_once __DIR__ . '/../utils/Database.php';
$db = Database::getInstance();
// 查询用户邮件
$sql = "SELECT * FROM emails WHERE to_address = (SELECT email FROM user WHERE username = ? AND is_deleted = 0) ORDER BY send_time ASC";
$emails = $db->query($sql, [$username])->fetchAll();
$this->messages = [];
$this->messageCount = 0;
$this->mailboxSize = 0;
$this->deletedMessages = [];
if ($emails) {
$id = 1;
foreach ($emails as $email) {
// 构建完整的邮件内容
$content = "From: {$email['from_address']}\r\n" .
"To: {$email['to_address']}\r\n" .
"Subject: {$email['subject']}\r\n" .
"Date: {$email['created_at']}\r\n" .
"\r\n" .
"{$email['content']}\r\n";
$this->messages[$id] = [
'uid' => $email['id'],
'size' => strlen($content),
'content' => $content,
'email_id' => $email['id'] // 保存数据库中的实际ID
];
$this->mailboxSize += $this->messages[$id]['size'];
$id++;
}
}
$this->messageCount = count($this->messages);
$this->logger->info("Loaded {count} emails for user {username}", [
'count' => $this->messageCount,
'username' => $this->username
]);
} catch (Exception $e) {
$this->messages = [];
$this->messageCount = 0;
$this->mailboxSize = 0;
$this->deletedMessages = [];
$this->logger->error("Failed to load messages for user {username}: {error}", [
'username' => $this->username,
'error' => $e->getMessage()
]);
}
}
}