Files
fdesk/app/command/EmailDetailCommand.php
2025-08-04 14:03:57 +08:00

206 lines
7.7 KiB
PHP

<?php
namespace app\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use support\Db;
/**
* 拉取邮箱的详情入库(根据emai_uid)
*/
class EmailDetailCommand extends Command
{
protected static $defaultName = 'email-detail';
protected static $defaultDescription = '拉取 detail 入库';
protected function configure()
{
$this->addOption(
'per-page', 'p',
InputOption::VALUE_OPTIONAL,
'每批处理多少条详情',
config('imap.per_page', 50)
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
try {
$cfg = config('imap');
$mailbox = $cfg['mailbox'];
$username = $cfg['username'];
$password = $cfg['password'];
$batch = (int)$input->getOption('per-page');
// 打开 IMAP
$inbox = @imap_open($mailbox, $username, $password);
if (!$inbox) {
$output->writeln("<error>IMAP 连接失败:". imap_last_error() ."</error>");
return Command::FAILURE;
}
// 阶段二:逐批拉取详情并写入 emails 表
$output->writeln("依次拉取详情并写入 emails 表 …");
while (true) {
$pending = Db::table('email_uids')
->where('status', 0)
// ->where('uid', '>', 13399)
->orderBy('uid', 'desc')
->limit(2)
->get();
if (count($pending) === 0) {
$output->writeln("<info>所有 UID 均已处理完毕。</info>");
break;
}
$output->writeln("处理下一批 " . count($pending) . " 条 …");
foreach ($pending as $row) {
$uid = $row->uid;
// 包裹整行处理在 try/catch 中,任何异常都不会中断循环
try {
$output->writeln("---- 开始处理 UID={$uid} ----");
// 1. fetch overview
$ov = @imap_fetch_overview($inbox, $uid, FT_UID)[0] ?? null;
if (!$ov) {
$err = imap_last_error() ?: '未知错误';
$output->writeln("<error>UID={$uid} overview 失败:{$err}</error>");
$this->logError("UID={$uid} overview 失败:{$err}");
continue;
}
$subject = mb_convert_encoding($ov->subject ?? '', 'UTF-8', 'GBK,UTF-8');
$from = $ov->from ?? '';
$date = $ov->date ?? '';
$output->writeln("UID={$uid} overview 成功,subject={$subject}");
// 2. fetch body
$body = $this->getMailBody($inbox, $uid, true);
$output->writeln("UID={$uid} body 长度:" . strlen($body));
// 3. sanitize & insert
$body = $this->sanitizeUtf8($body);
$now = time();
try {
Db::table('emails')->insert([
'uid' => $uid,
'subject' => $subject,
'from' => $from,
'date' => $date,
'body' => $body,
'created_at' => $now,
'updated_at' => $now,
]);
$output->writeln("<info>UID={$uid} 写入 emails 成功</info>");
} catch (\Throwable $e) {
$output->writeln("<error>UID={$uid} 写入失败:{$e->getMessage()}</error>");
$this->logError("UID={$uid} 写入失败:{$e->getMessage()}");
continue;
}
// 4. 标记已处理
try {
Db::table('email_uids')
->where('uid', $uid)
->update([
'status' => 1,
'processed_at' => $now,
]);
$output->writeln("<info>UID={$uid} 标记为已处理</info>");
} catch (\Throwable $e) {
$output->writeln("<error>UID={$uid} 标记状态失败:{$e->getMessage()}</error>");
$this->logError("UID={$uid} 标记状态失败:{$e->getMessage()}");
// 即使更新 status 失败,也继续下一条
}
} catch (\Throwable $e) {
// 捕获任何非预期错误
$output->writeln("<error>UID={$uid} 处理异常:{$e->getMessage()}</error>");
$this->logError("UID={$uid} 处理异常:{$e->getMessage()}");
continue;
}
}
// 批次结束后稍作休眠,防止服务器过载
sleep(1);
}
imap_close($inbox);
$output->writeln("\n<info>✅ 全部邮件处理完成。</info>");
return Command::SUCCESS;
} catch (\Throwable $th) {
dpr($th);
return Command::FAILURE;
}
}
/**
* 将任意字符串清洗为有效 UTF-8,移除或替换无效字节
*/
protected function sanitizeUtf8(string $text): string
{
return iconv('UTF-8', 'UTF-8//IGNORE', $text);
}
/**
* 记录错误到日志
*/
protected function logError(string $msg): void
{
$line = '[' . date('Y-m-d H:i:s') . '] ' . $msg . "\n";
file_put_contents(
runtime_path('logs/email_errors.log'),
$line,
FILE_APPEND | LOCK_EX
);
}
/**
* 按编码解码并保留 "\r\n"
*/
protected function getMailBody($inbox, int $id, bool $isUid = false): string
{
$flags = $isUid ? FT_UID : 0;
$structure = imap_fetchstructure($inbox, $id, $flags);
if (!empty($structure->parts)) {
// HTML 部分
foreach ($structure->parts as $i => $part) {
if (isset($part->subtype) && strtolower($part->subtype) === 'html') {
$data = imap_fetchbody($inbox, $id, (string)($i+1), $flags);
return $this->decodeBody($data, $part->encoding);
}
}
// 文本部分
foreach ($structure->parts as $i => $part) {
if (isset($part->subtype) && strtolower($part->subtype) === 'plain') {
$data = imap_fetchbody($inbox, $id, (string)($i+1), $flags);
return $this->decodeBody($data, $part->encoding);
}
}
// 原始 body
return imap_body($inbox, $id, $flags);
}
return imap_body($inbox, $id, $flags);
}
/**
* 按编码解码
*/
protected function decodeBody(string $body, int $encoding): string
{
switch ($encoding) {
case 3: return imap_base64($body);
case 4: return imap_qprint($body);
default: return $body;
}
}
}