first commit
This commit is contained in:
@@ -0,0 +1,746 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Kiri\MailServer;
|
||||
|
||||
use Kiri\MailServer\Auth\AuthInterface;
|
||||
use Kiri\MailServer\Storage\StorageInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* IMAP 会话状态机 — RFC 3501 核心实现
|
||||
*
|
||||
* 支持的 IMAP 命令:
|
||||
* CAPABILITY, LOGIN, LOGOUT, NOOP
|
||||
* SELECT, EXAMINE, LIST, LSUB, STATUS
|
||||
* FETCH, UID FETCH, STORE, UID STORE
|
||||
* SEARCH, UID SEARCH
|
||||
* EXPUNGE, CLOSE
|
||||
* CREATE, DELETE, RENAME
|
||||
* SUBSCRIBE, UNSUBSCRIBE
|
||||
*/
|
||||
class ImapSession
|
||||
{
|
||||
|
||||
/** @var string 会话 ID */
|
||||
private string $sessionId;
|
||||
|
||||
/** @var string 当前状态 */
|
||||
private string $state = ImapProtocol::STATE_NOT_AUTHENTICATED;
|
||||
|
||||
/** @var bool 是否已认证 */
|
||||
private bool $authenticated = false;
|
||||
|
||||
/** @var string|null 认证用户名 */
|
||||
private ?string $username = null;
|
||||
|
||||
/** @var string|null 当前选中的邮箱 */
|
||||
private ?string $selectedMailbox = null;
|
||||
|
||||
/** @var bool 当前邮箱是否只读 */
|
||||
private bool $readonly = false;
|
||||
|
||||
/** @var int UIDVALIDITY (邮箱创建时间戳) */
|
||||
private int $uidvalidity = 0;
|
||||
|
||||
/** @var array<int, MailMessage> 当前邮箱邮件缓存 (seq => message) */
|
||||
private array $messages = [];
|
||||
|
||||
/** @var array<string> 订阅的邮箱列表 */
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user