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("IMAP 连接失败:". imap_last_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("所有 UID 均已处理完毕。"); 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("UID={$uid} overview 失败:{$err}"); $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("UID={$uid} 写入 emails 成功"); } catch (\Throwable $e) { $output->writeln("UID={$uid} 写入失败:{$e->getMessage()}"); $this->logError("UID={$uid} 写入失败:{$e->getMessage()}"); continue; } // 4. 标记已处理 try { Db::table('email_uids') ->where('uid', $uid) ->update([ 'status' => 1, 'processed_at' => $now, ]); $output->writeln("UID={$uid} 标记为已处理"); } catch (\Throwable $e) { $output->writeln("UID={$uid} 标记状态失败:{$e->getMessage()}"); $this->logError("UID={$uid} 标记状态失败:{$e->getMessage()}"); // 即使更新 status 失败,也继续下一条 } } catch (\Throwable $e) { // 捕获任何非预期错误 $output->writeln("UID={$uid} 处理异常:{$e->getMessage()}"); $this->logError("UID={$uid} 处理异常:{$e->getMessage()}"); continue; } } // 批次结束后稍作休眠,防止服务器过载 sleep(1); } imap_close($inbox); $output->writeln("\n✅ 全部邮件处理完成。"); 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; } } }