登录
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user