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.

171 lines
6.8 KiB

<?php
require_once __DIR__ . '/../utils/Config.php';
class Pop3Handler {
private $socket;
private $clientIp;
private $logger;
private $config;
private $state = 'auth';
private $username = '';
private $password = '';
private $authenticated = false;
private $messages = [];
private $deletedMessages = [];
private $messageCount = 0;
private $mailboxSize = 0;
public function __construct($socket, $clientIp, $logger) {
$this->socket = $socket;
$this->clientIp = $clientIp;
$this->logger = $logger;
$this->config = Config::getInstance();
}
public function handle($data) {
$lines = explode("\r\n", $data);
foreach ($lines as $line) {
$line = trim($line);
if ($line === '') { continue; }
$this->processCommand($line);
}
}
private function processCommand($line) {
$parts = preg_split('/\\s+/', $line, 2);
$cmd = strtoupper($parts[0]);
$arg = $parts[1] ?? '';
switch ($cmd) {
case 'USER':
$this->username = $arg;
$this->send("+OK");
break;
case 'PASS':
$this->password = $arg;
if ($this->authenticate()) {
$this->authenticated = true;
$this->state = 'transaction';
$this->initMessages($this->username);
$this->send("+OK {$this->messageCount} {$this->mailboxSize}");
} else {
$this->send("-ERR");
}
break;
case 'STAT':
if (!$this->authenticated) { $this->send("-ERR"); break; }
$this->send("+OK {$this->messageCount} {$this->mailboxSize}");
break;
case 'LIST':
if (!$this->authenticated) { $this->send("-ERR"); break; }
if (!empty($arg)) {
$id = (int)$arg;
if (isset($this->messages[$id])) { $this->send("+OK {$id} {$this->messages[$id]['size']}"); }
else { $this->send("-ERR"); }
} else {
$this->send("+OK {$this->messageCount} messages");
foreach ($this->messages as $id => $msg) { $this->send("{$id} {$msg['size']}"); }
$this->send(".");
}
break;
case 'UIDL':
if (!$this->authenticated) { $this->send("-ERR"); break; }
if (!empty($arg)) {
$id = (int)$arg;
if (isset($this->messages[$id])) { $this->send("+OK {$id} {$this->messages[$id]['uid']}"); }
else { $this->send("-ERR"); }
} else {
$this->send("+OK");
foreach ($this->messages as $id => $msg) { $this->send("{$id} {$msg['uid']}"); }
$this->send(".");
}
break;
case 'RETR':
if (!$this->authenticated) { $this->send("-ERR"); break; }
$id = (int)$arg;
if (isset($this->messages[$id])) {
$this->send("+OK {$this->messages[$id]['size']} octets");
$this->sendRaw($this->messages[$id]['content']);
$this->send(".");
} else { $this->send("-ERR"); }
break;
case 'DELE':
if (!$this->authenticated) { $this->send("-ERR"); break; }
$id = (int)$arg;
if (isset($this->messages[$id])) { $this->deletedMessages[$id] = true; $this->send("+OK"); }
else { $this->send("-ERR"); }
break;
case 'NOOP':
$this->send("+OK");
break;
case 'RSET':
$this->deletedMessages = [];
$this->send("+OK");
break;
case 'QUIT':
if ($this->authenticated) { $this->applyDeletes(); }
$this->send("+OK bye");
break;
default:
$this->send("-ERR");
break;
}
}
private function authenticate() {
try {
require_once __DIR__ . '/../utils/Database.php';
$db = Database::getInstance();
$row = $db->fetchOne("SELECT password FROM user WHERE username = ? AND is_deleted = 0", [$this->username]);
if (!$row) { return false; }
return password_verify($this->password, $row['password']);
} catch (Exception $e) {
return false;
}
}
private function initMessages($username) {
try {
require_once __DIR__ . '/../utils/Database.php';
$db = Database::getInstance();
$emails = $db->fetchAll("SELECT * FROM email WHERE rcpt_to = ? AND folder = 'inbox' AND is_deleted = 0 ORDER BY `date` ASC", [$username]);
$this->messages = [];
$this->messageCount = 0;
$this->mailboxSize = 0;
$this->deletedMessages = [];
if ($emails) {
$id = 1;
foreach ($emails as $email) {
$content = "From: {$email['from']}\r\n" .
"To: {$email['to']}\r\n" .
"Subject: {$email['subject']}\r\n" .
"Date: {$email['date']}\r\n\r\n" .
"{$email['data']}\r\n";
$size = strlen($content);
$this->messages[$id] = [
'uid' => $email['id'],
'size' => $size,
'content' => $content,
'email_id' => $email['id']
];
$this->mailboxSize += $size;
$id++;
}
}
$this->messageCount = count($this->messages);
} catch (Exception $e) {
$this->messages = [];
$this->messageCount = 0;
$this->mailboxSize = 0;
$this->deletedMessages = [];
}
}
private function applyDeletes() {
if (empty($this->deletedMessages)) { return; }
try {
require_once __DIR__ . '/../utils/Database.php';
$db = Database::getInstance();
foreach ($this->deletedMessages as $id => $_) {
if (isset($this->messages[$id])) {
$db->update("UPDATE email SET is_deleted = 1, folder = 'trash' WHERE id = ?", [$this->messages[$id]['email_id']]);
}
}
} catch (Exception $e) {}
}
private function send($line) { $out = $line . "\r\n"; socket_write($this->socket, $out, strlen($out)); }
private function sendRaw($raw) { socket_write($this->socket, $raw, strlen($raw)); }
}