206 lines
7.7 KiB
PHP
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;
|
|
}
|
|
}
|
|
} |