|
|
<?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()
|
|
|
]);
|
|
|
}
|
|
|
}
|
|
|
}
|