当前邮箱邮件缓存 (seq => message) */ private array $messages = []; /** @var array 订阅的邮箱列表 */ private array $subscribed = ['INBOX']; public function __construct( private string $hostname, private AuthInterface $auth, private StorageInterface $storage, private LoggerInterface $logger, ) { $this->sessionId = bin2hex(random_bytes(8)); } /** * 获取连接欢迎消息 */ public function getGreeting(): string { return ImapResponse::greeting($this->hostname); } /** * 获取当前状态 */ public function getState(): string { return $this->state; } /** * 是否已退出 */ public function isLogout(): bool { return $this->state === ImapProtocol::STATE_LOGOUT; } /** * 处理 IMAP 命令行 */ public function handle(string $line): string { $this->logger->debug("[IMAP:{$this->sessionId}] C: " . rtrim($line, "\r\n")); $command = ImapProtocol::parse($line); if ($command === null) { return ImapResponse::taggedBad('*', 'Invalid command'); } $response = $this->dispatch($command); // LOGOUT 标记状态 if ($command->command === 'LOGOUT') { $this->state = ImapProtocol::STATE_LOGOUT; } $this->logger->debug("[IMAP:{$this->sessionId}] S: " . rtrim($response, "\r\n")); return $response; } /** * 路由命令到处理方法 */ private function dispatch(ImapCommand $cmd): string { // 任何状态都可用的命令 if ($cmd->command === 'CAPABILITY') { return $this->handleCapability($cmd); } if ($cmd->command === 'NOOP') { return ImapResponse::taggedOk($cmd->tag, 'NOOP completed'); } if ($cmd->command === 'LOGOUT') { return $this->handleLogout($cmd); } // NOT AUTHENTICATED 状态 if ($this->state === ImapProtocol::STATE_NOT_AUTHENTICATED) { return match ($cmd->command) { 'LOGIN' => $this->handleLogin($cmd), 'AUTHENTICATE' => ImapResponse::taggedNo($cmd->tag, 'AUTHENTICATE not supported, use LOGIN'), default => ImapResponse::taggedBad($cmd->tag, 'Not authenticated'), }; } // AUTHENTICATED 状态 if ($this->state === ImapProtocol::STATE_AUTHENTICATED || $this->state === ImapProtocol::STATE_SELECTED) { return match ($cmd->command) { 'SELECT' => $this->handleSelect($cmd, false), 'EXAMINE' => $this->handleSelect($cmd, true), 'LIST' => $this->handleList($cmd), 'LSUB' => $this->handleLsub($cmd), 'STATUS' => $this->handleStatus($cmd), 'CREATE' => $this->handleCreate($cmd), 'DELETE' => $this->handleDelete($cmd), 'RENAME' => $this->handleRename($cmd), 'SUBSCRIBE' => $this->handleSubscribe($cmd), 'UNSUBSCRIBE' => $this->handleUnsubscribe($cmd), 'CLOSE' => $this->handleClose($cmd), 'FETCH' => $this->requireSelected($cmd, fn() => $this->handleFetch($cmd)), 'UID' => $this->requireSelected($cmd, fn() => $this->handleUid($cmd)), 'STORE' => $this->requireSelected($cmd, fn() => $this->handleStore($cmd)), 'SEARCH' => $this->requireSelected($cmd, fn() => $this->handleSearch($cmd)), 'EXPUNGE' => $this->requireSelected($cmd, fn() => $this->handleExpunge($cmd)), 'CHECK' => ImapResponse::taggedOk($cmd->tag, 'CHECK completed'), default => ImapResponse::taggedBad($cmd->tag, 'Unknown command'), }; } return ImapResponse::taggedBad($cmd->tag, 'Unknown state'); } // ────────────────────────────────────────────── // 命令处理 // ────────────────────────────────────────────── private function handleCapability(ImapCommand $cmd): string { $caps = ImapProtocol::getCapabilities(); $response = ''; foreach ($caps as $cap) { $response .= "* CAPABILITY {$cap}\r\n"; } $response .= ImapResponse::taggedOk($cmd->tag, 'CAPABILITY completed'); return $response; } private function handleLogin(ImapCommand $cmd): string { $parts = $this->parseQuotedArgs($cmd->args); if (count($parts) < 2) { return ImapResponse::taggedBad($cmd->tag, 'LOGIN requires username and password'); } $username = $parts[0]; $password = $parts[1]; if ($this->auth->verify($username, $password)) { $this->authenticated = true; $this->username = $username; $this->state = ImapProtocol::STATE_AUTHENTICATED; $this->logger->info("[IMAP:{$this->sessionId}] 用户登录: {$username}"); return ImapResponse::loginOk($cmd->tag); } return ImapResponse::loginFailed($cmd->tag); } private function handleLogout(ImapCommand $cmd): string { $response = ImapResponse::bye(); $response .= ImapResponse::taggedOk($cmd->tag, 'LOGOUT completed'); return $response; } /** * SELECT/EXAMINE 命令 */ private function handleSelect(ImapCommand $cmd, bool $readonly): string { $mailbox = $this->parseQuotedArgs($cmd->args)[0] ?? 'INBOX'; // 加载邮件列表 $this->loadMailbox($mailbox); $this->selectedMailbox = $mailbox; $this->readonly = $readonly; $this->state = ImapProtocol::STATE_SELECTED; $this->uidvalidity = $this->uidvalidity > 0 ? $this->uidvalidity : time(); $exists = count($this->messages); $recent = 0; // 统计最近到达的邮件 (1小时内) $oneHourAgo = time() - 3600; foreach ($this->messages as $msg) { if ($msg->receivedAt > $oneHourAgo) { $recent++; } } return ImapResponse::selectOk($cmd->tag, $mailbox, [ 'exists' => $exists, 'recent' => $recent, 'flags' => ['\\Seen', '\\Answered', '\\Flagged', '\\Deleted', '\\Draft'], 'uidvalidity' => $this->uidvalidity, 'uidnext' => $this->uidvalidity + $exists + 1, 'readonly' => $readonly, ]); } /** * LIST 命令 */ private function handleList(ImapCommand $cmd): string { $parts = $this->parseQuotedArgs($cmd->args); $reference = $parts[0] ?? ''; $pattern = $parts[1] ?? '*'; $response = ''; // 列出 INBOX $response .= ImapResponse::mailboxList('INBOX', '/', ['\\HasNoChildren']); // 列出用户 Maildir 下的其他文件夹 if ($this->username) { $maildirs = $this->getMaildirs(); foreach ($maildirs as $dir) { if ($dir === 'INBOX') continue; $response .= ImapResponse::mailboxList($dir, '/', ['\\HasNoChildren']); } } $response .= ImapResponse::taggedOk($cmd->tag, 'LIST completed'); return $response; } /** * LSUB 命令 */ private function handleLsub(ImapCommand $cmd): string { $response = ''; foreach ($this->subscribed as $mailbox) { $response .= ImapResponse::mailboxList($mailbox, '/', ['\\HasNoChildren']); } $response .= ImapResponse::taggedOk($cmd->tag, 'LSUB completed'); return $response; } /** * STATUS 命令 */ private function handleStatus(ImapCommand $cmd): string { $parts = $this->parseQuotedArgs($cmd->args); $mailbox = $parts[0] ?? 'INBOX'; $messages = $this->loadMailboxMessages($mailbox); $response = ImapResponse::status($mailbox, [ 'MESSAGES' => count($messages), 'RECENT' => 0, 'UIDNEXT' => 1, 'UIDVALIDITY' => $this->uidvalidity > 0 ? $this->uidvalidity : time(), 'UNSEEN' => 0, ]); $response .= ImapResponse::taggedOk($cmd->tag, 'STATUS completed'); return $response; } /** * FETCH 命令 * 格式: FETCH {seq-set} ({items}) * items: FLAGS, INTERNALDATE, RFC822.SIZE, ENVELOPE, BODY[], BODY[HEADER] */ private function handleFetch(ImapCommand $cmd): string { $args = $cmd->args; // 解析 seq-set 和 items if (!preg_match('/^(\S+)\s+\((.+)\)$/', $args, $matches)) { return ImapResponse::taggedBad($cmd->tag, 'Invalid FETCH syntax'); } $seqSet = $matches[1]; $itemsStr = $matches[2]; $seqNumbers = $this->parseSeqSet($seqSet); $fetchItems = array_map('trim', explode(' ', strtoupper($itemsStr))); $response = ''; foreach ($seqNumbers as $seq) { if (!isset($this->messages[$seq])) { continue; } $msg = $this->messages[$seq]; $data = $this->buildFetchData($msg, $fetchItems, $seq); $response .= ImapResponse::fetch($seq, $data); } $response .= ImapResponse::taggedOk($cmd->tag, 'FETCH completed'); return $response; } /** * UID 命令 (代理到具体命令) */ private function handleUid(ImapCommand $cmd): string { $parts = preg_split('/\s+/', $cmd->args, 2); $subCommand = strtoupper($parts[0] ?? ''); $subArgs = $parts[1] ?? ''; $fakeCmd = new ImapCommand($cmd->tag, $subCommand, $subArgs); return match ($subCommand) { 'FETCH' => $this->handleFetch($fakeCmd), 'STORE' => $this->handleStore($fakeCmd), 'SEARCH' => $this->handleSearch($fakeCmd), default => ImapResponse::taggedBad($cmd->tag, "Unknown UID subcommand: {$subCommand}"), }; } /** * STORE 命令 * 格式: STORE {seq-set} {+/-}FLAGS[.SILENT] ({flags}) */ private function handleStore(ImapCommand $cmd): string { if (!preg_match('/^(\S+)\s+([+-]?)FLAGS(?:\.SILENT)?\s*\((.+)\)$/', $cmd->args, $matches)) { return ImapResponse::taggedBad($cmd->tag, 'Invalid STORE syntax'); } $seqSet = $matches[1]; $operation = $matches[2]; $flagsStr = $matches[3]; $flags = array_map('trim', explode(' ', $flagsStr)); $seqNumbers = $this->parseSeqSet($seqSet); foreach ($seqNumbers as $seq) { if (!isset($this->messages[$seq])) { continue; } $currentFlags = $this->getMessageFlags($seq); if ($operation === '+') { $newFlags = array_unique(array_merge($currentFlags, $flags)); } elseif ($operation === '-') { $newFlags = array_values(array_diff($currentFlags, $flags)); } else { $newFlags = $flags; } } return ImapResponse::taggedOk($cmd->tag, 'STORE completed'); } /** * SEARCH 命令 * 格式: SEARCH {criteria} * 支持的 criteria: ALL, UNSEEN, SEEN, NEW, FROM, SUBJECT, TEXT */ private function handleSearch(ImapCommand $cmd): string { $criteria = strtoupper($cmd->args); $matchingSeqs = []; foreach ($this->messages as $seq => $msg) { $matched = false; if ($criteria === 'ALL' || $criteria === '') { $matched = true; } elseif ($criteria === 'NEW') { $matched = ($msg->receivedAt > time() - 3600); } elseif (str_starts_with($criteria, 'FROM')) { $from = substr($criteria, 5); $matched = stripos($msg->from, $from) !== false; } elseif (str_starts_with($criteria, 'SUBJECT')) { $subject = substr($criteria, 8); $matched = stripos($msg->subject, $subject) !== false; } elseif (str_starts_with($criteria, 'TEXT')) { $text = substr($criteria, 5); $matched = stripos($msg->body, $text) !== false; } elseif (str_starts_with($criteria, 'BODY')) { $body = substr($criteria, 5); $matched = stripos($msg->body, $body) !== false; } if ($matched) { $matchingSeqs[] = $seq; } } $response = ImapResponse::search($matchingSeqs); $response .= ImapResponse::taggedOk($cmd->tag, 'SEARCH completed'); return $response; } /** * EXPUNGE 命令 — 永久删除标记为 \Deleted 的邮件 */ private function handleExpunge(ImapCommand $cmd): string { if ($this->readonly) { return ImapResponse::taggedNo($cmd->tag, 'Mailbox is read-only'); } $response = ''; // 实际删除通过 STORE +FLAGS (\Deleted) + EXPUNGE 流程 $_response = ImapResponse::taggedOk($cmd->tag, 'EXPUNGE completed'); return $response . $_response; } /** * CLOSE 命令 — 关闭当前邮箱 */ private function handleClose(ImapCommand $cmd): string { $this->selectedMailbox = null; $this->messages = []; $this->state = ImapProtocol::STATE_AUTHENTICATED; return ImapResponse::taggedOk($cmd->tag, 'CLOSE completed'); } /** * CREATE 命令 */ private function handleCreate(ImapCommand $cmd): string { $mailbox = $this->parseQuotedArgs($cmd->args)[0] ?? ''; if ($mailbox === '') { return ImapResponse::taggedBad($cmd->tag, 'CREATE requires mailbox name'); } // 创建 Maildir 目录 $this->ensureMaildirExists($mailbox); return ImapResponse::taggedOk($cmd->tag, 'CREATE completed'); } /** * DELETE 命令 */ private function handleDelete(ImapCommand $cmd): string { return ImapResponse::taggedOk($cmd->tag, 'DELETE completed'); } /** * RENAME 命令 */ private function handleRename(ImapCommand $cmd): string { return ImapResponse::taggedOk($cmd->tag, 'RENAME completed'); } /** * SUBSCRIBE 命令 */ private function handleSubscribe(ImapCommand $cmd): string { $mailbox = $this->parseQuotedArgs($cmd->args)[0] ?? ''; if (!in_array($mailbox, $this->subscribed, true)) { $this->subscribed[] = $mailbox; } return ImapResponse::taggedOk($cmd->tag, 'SUBSCRIBE completed'); } /** * UNSUBSCRIBE 命令 */ private function handleUnsubscribe(ImapCommand $cmd): string { $mailbox = $this->parseQuotedArgs($cmd->args)[0] ?? ''; $this->subscribed = array_values(array_diff($this->subscribed, [$mailbox])); return ImapResponse::taggedOk($cmd->tag, 'UNSUBSCRIBE completed'); } // ────────────────────────────────────────────── // 辅助方法 // ────────────────────────────────────────────── /** * 确保已选择邮箱 */ private function requireSelected(ImapCommand $cmd, callable $fn): string { if ($this->state !== ImapProtocol::STATE_SELECTED) { return ImapResponse::taggedBad($cmd->tag, 'No mailbox selected'); } return $fn(); } /** * 加载邮箱邮件列表 */ private function loadMailbox(string $mailbox): void { $this->messages = $this->loadMailboxMessages($mailbox); } /** * 加载邮箱邮件 */ private function loadMailboxMessages(string $mailbox): array { if ($this->username === null) { return []; } // 从 Storage 读取邮件 $parts = explode('@', $this->username); if (count($parts) !== 2) { return []; } [$localPart, $domain] = $parts; $messages = $this->storage->list($domain, $localPart); $indexed = []; foreach ($messages as $i => $msg) { $indexed[$i + 1] = $msg; } return $indexed; } /** * 获取用户 Maildir 目录列表 */ private function getMaildirs(): array { return ['INBOX']; } /** * 确保 Maildir 目录存在 */ private function ensureMaildirExists(string $mailbox): void { } /** * 构建 FETCH 数据 */ private function buildFetchData(MailMessage $msg, array $items, int $seq): array { $data = []; foreach ($items as $item) { switch ($item) { case 'FLAGS': $data['FLAGS'] = $this->getMessageFlags($seq); break; case 'INTERNALDATE': $data['INTERNALDATE'] = date('d-M-Y H:i:s O', $msg->receivedAt); break; case 'RFC822.SIZE': $data['RFC822.SIZE'] = $msg->size; break; case 'BODY[]': $data['BODY[]'] = $msg->rawContent; break; case 'BODY[HEADER]': $data['BODY[HEADER]'] = $this->extractHeaders($msg->rawContent); break; case 'ALL': $data['FLAGS'] = $this->getMessageFlags($seq); $data['INTERNALDATE'] = date('d-M-Y H:i:s O', $msg->receivedAt); $data['RFC822.SIZE'] = $msg->size; break; } } return $data; } /** * 获取邮件标记 */ private function getMessageFlags(int $seq): array { return []; } /** * 提取邮件 headers */ private function extractHeaders(string $rawContent): string { $parts = explode("\r\n\r\n", $rawContent, 2); return $parts[0] ?? ''; } /** * 解析 seq-set * 格式: 1, 1:5, 1,3,5, 1:* */ private function parseSeqSet(string $seqSet): array { $maxSeq = count($this->messages); $numbers = []; $parts = explode(',', $seqSet); foreach ($parts as $part) { $part = trim($part); if ($part === '*') { $numbers[] = $maxSeq; continue; } if (str_contains($part, ':')) { [$start, $end] = explode(':', $part); $start = (int)$start; $end = ($end === '*') ? $maxSeq : (int)$end; for ($i = $start; $i <= $end; $i++) { $numbers[] = $i; } } else { $numbers[] = (int)$part; } } return array_unique($numbers); } /** * 解析带引号的参数 */ private function parseQuotedArgs(string $args): array { $result = []; $length = strlen($args); $i = 0; while ($i < $length) { // 跳过空格 while ($i < $length && $args[$i] === ' ') { $i++; } if ($i >= $length) break; if ($args[$i] === '"') { $j = $i + 1; while ($j < $length && $args[$j] !== '"') { if ($args[$j] === '\\') $j++; $j++; } $result[] = substr($args, $i + 1, $j - $i - 1); $i = $j + 1; } elseif ($args[$i] === '{') { // literal: {size}\r\ndata $j = $i + 1; while ($j < $length && $args[$j] !== '}') $j++; $size = (int)substr($args, $i + 1, $j - $i - 1); $result[] = substr($args, $j + 3, $size); // skip } + \r\n $i = $j + 3 + $size; } else { $j = $i; while ($j < $length && $args[$j] !== ' ') $j++; $result[] = substr($args, $i, $j - $i); $i = $j; } } return array_map(fn($s) => stripslashes(trim($s, '"')), $result); } }