import { config } from './config.js'; /** * 设置临时邮箱并获取验证码 * @param {Page} page - Playwright 页面对象 * @returns {Promise<{email: string, code: string}>} */ export async function setupTempMail(page) { console.log('📧 开始设置临时邮箱...'); // 访问 tempmail.plus(增加超时时间,因为网络较慢) await page.goto(config.tempmail.url, { waitUntil: 'domcontentloaded', // 改为 domcontentloaded,不等待所有网络请求 timeout: 60000 // 60秒超时 }); await page.waitForTimeout(3000); // 等待页面稳定 // 输入邮箱用户名(固定为配置的 qichen111) console.log('输入邮箱用户名:', config.tempmail.username); await page.fill('#pre_button', config.tempmail.username); // 点击页面其他位置或按回车来确认输入,触发邮箱地址更新 console.log('确认邮箱用户名...'); await page.click('body'); // 点击页面空白处 await page.waitForTimeout(2000); // 检查 PIN 码弹窗是否已经显示 console.log('[LOG] 检查 PIN 码弹窗状态...'); const pinModal = page.locator('#modal-verify'); await page.waitForTimeout(2000); // 等待页面稳定 const initialModalClass = await pinModal.getAttribute('class'); console.log('[LOG] 弹窗 class:', initialModalClass); // 判断弹窗是否已经显示(包含 "show" class) const isModalShown = initialModalClass && initialModalClass.includes('show'); console.log('[LOG] 弹窗是否已显示:', isModalShown); if (!isModalShown) { // 如果弹窗未显示,才需要点击 PIN 保护元素 console.log('[LOG] 弹窗未显示,点击 PIN 码保护元素...'); const pinProtectSpan = page.locator('span.pin-text[data-tr="box_protected"]'); await pinProtectSpan.waitFor({ state: 'visible', timeout: 15000 }); await pinProtectSpan.click({ force: true }); console.log('[LOG] ✅ 已点击 PIN 保护元素'); await page.waitForTimeout(2000); } else { console.log('[LOG] ✅ 弹窗已自动显示,无需点击'); } // 查找 PIN 码输入框 console.log('[LOG] 查找 PIN 码输入框...'); const pinInput = page.locator('#pin'); const pinInputCount = await pinInput.count(); console.log('[LOG] 找到 PIN 输入框数量:', pinInputCount); if (pinInputCount > 0) { // 检查输入框是否可见 const isVisible = await pinInput.isVisible(); console.log('[LOG] PIN 输入框是否可见:', isVisible); // 输入 PIN 码 console.log('[LOG] 输入 PIN 码:', config.tempmail.pinCode); await pinInput.fill(config.tempmail.pinCode); console.log('[LOG] ✅ PIN 码已输入'); await page.waitForTimeout(500); // 查找提交按钮 console.log('[LOG] 查找提交按钮 #verify...'); const verifyButton = page.locator('#verify'); const verifyButtonCount = await verifyButton.count(); console.log('[LOG] 找到提交按钮数量:', verifyButtonCount); if (verifyButtonCount > 0) { const buttonText = await verifyButton.textContent(); console.log('[LOG] 按钮文本:', buttonText); console.log('[LOG] 点击提交按钮...'); await verifyButton.click(); console.log('[LOG] ✅ 已点击提交按钮'); } else { console.log('[LOG] ⚠️ 未找到提交按钮,尝试按回车'); await pinInput.press('Enter'); } // 等待验证完成 await page.waitForTimeout(2000); console.log('[LOG] ✅ PIN 码验证流程完成'); } else { console.log('[LOG] ❌ 未找到 PIN 输入框!'); throw new Error('未找到 PIN 码输入框'); } // 等待弹窗关闭后获取邮箱地址 await page.waitForTimeout(2000); console.log('[LOG] 获取生成的邮箱地址...'); // 尝试多个可能的邮箱地址元素 let emailAddress = ''; // 方法 1: 查找特定的邮箱显示元素 const emailDisplay = page.locator('#email-address, #mail, .email-address, [id*="email"]').first(); if (await emailDisplay.count() > 0 && await emailDisplay.isVisible()) { const text = await emailDisplay.textContent(); const match = text.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/); if (match) { emailAddress = match[0]; } } // 方法 2: 如果没找到,尝试从页面中提取所有包含 @ 的文本 if (!emailAddress) { const pageContent = await page.content(); const emailMatches = pageContent.match(/qichen111@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/); if (emailMatches && emailMatches.length > 0) { emailAddress = emailMatches[0]; } } // 方法 3: 查找复制按钮附近的文本 if (!emailAddress) { const copyButton = page.locator('button:has-text("复制")').first(); if (await copyButton.count() > 0) { const parent = copyButton.locator('..'); const text = await parent.textContent(); const match = text.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/); if (match) { emailAddress = match[0]; } } } console.log('[LOG] 提取到的邮箱地址:', emailAddress); if (!emailAddress || !emailAddress.includes('@')) { console.log('[LOG] ⚠️ 未能正确获取邮箱地址,尝试使用默认格式'); emailAddress = `${config.tempmail.username}@mailto.plus`; } console.log('✅ 邮箱地址:', emailAddress); return { inboxReady: true, page }; } // 全局基线时间,在 setupTempMail 后初始化,每次注册成功后更新 let globalBaselineTime = null; /** * 获取收件箱最新一条邮件的时间作为基线 */ export async function updateBaseline(page) { try { // 等待收件箱加载 await page.waitForSelector('.inbox', { timeout: 10000 }).catch(() => {}); await page.waitForTimeout(2000); // 查找所有含有时间的邮件行 const timeSpans = page.locator('.inbox span[data-date]'); const count = await timeSpans.count(); if (count === 0) { globalBaselineTime = null; console.log('[基线] 收件箱无邮件,基线为 null'); return; } // 第一个就是最新的 const dateStr = await timeSpans.first().getAttribute('data-date'); globalBaselineTime = dateStr; console.log('[基线] 更新为最新邮件时间:', globalBaselineTime); } catch (e) { console.log('[基线] 获取失败:', e.message); globalBaselineTime = null; } } /** * 等待并获取验证码 * @param {Page} page - Playwright 页面对象 * @returns {Promise} 验证码 */ export async function waitForVerificationCode(page, sentAfterMs, targetEmail) { console.log('⏳ 等待接收验证码邮件...'); console.log('[LOG] 基线时间:', globalBaselineTime || '未设置'); const maxWaitTime = 120000; const checkInterval = 1000; let elapsed = 0; async function extractCodeFromOpenedMail(expectEmail) { // 1) 优先从特定内容区域读取 const contentLoc = page.locator('.mail-content, .email-body, #mail-body, [class*="mail-content"], .pm-text, .view, .message-body').first(); if (await contentLoc.count() > 0) { const text = await contentLoc.textContent(); if (expectEmail && text && !text.includes(expectEmail)) { console.log('[LOG] 内容不匹配目标邮箱,继续查找'); } else { const code = /\b(\d{6})\b/.exec(text || ''); if (code) return code[1]; } } // 2) 遍历所有 iframe 提取文本 const frames = page.frames(); console.log(`[LOG] 当前 frame 数量: ${frames.length}`); for (const f of frames) { try { const body = await f.$('body'); if (!body) continue; const text = await f.evaluate(() => document.body.innerText || ''); if (expectEmail && text && !text.includes(expectEmail)) continue; const m = /\b(\d{6})\b/.exec(text); if (m) return m[1]; } catch {} } // 3) 退化为整页文本 const all = await page.textContent('body'); if (expectEmail && all && !all.includes(expectEmail)) return null; const m = /\b(\d{6})\b/.exec(all || ''); return m ? m[1] : null; } while (elapsed < maxWaitTime) { await page.waitForTimeout(checkInterval); elapsed += checkInterval; console.log(`[LOG] 检查邮件... (已等待 ${elapsed / 1000} 秒)`); try { // 每个邮件包含 ,找到所有带时间戳的行 const timeSpans = page.locator('.inbox span[data-date]'); const count = await timeSpans.count(); console.log(`[LOG] 当前邮件数量: ${count}`); if (count === 0) continue; // 遍历所有邮件,找第一个满足条件的:发件人 cfbounces && 时间 > 基线 for (let i = 0; i < count; i++) { const timeSpan = timeSpans.nth(i); const dateStr = await timeSpan.getAttribute('data-date'); // 向上找到包含 from 的父元素获取发件人 const row = timeSpan.locator('xpath=ancestor::div[contains(@class, "row")]').first(); const fromSpan = row.locator('.from span').last(); const sender = await fromSpan.textContent().catch(() => ''); const isCfbounces = /cfbounces/i.test(sender || ''); const isNewer = globalBaselineTime ? (dateStr > globalBaselineTime) : true; console.log(`[LOG] Row ${i+1}: sender=${sender} date=${dateStr} baseline=${globalBaselineTime} newer=${isNewer}`); if (isCfbounces && isNewer) { console.log('✅ 发现新验证邮件,点击读取...'); await row.scrollIntoViewIfNeeded().catch(() => {}); await row.click({ timeout: 5000 }); await page.waitForTimeout(2000); const code = await extractCodeFromOpenedMail(targetEmail); if (code) { console.log('✅ 获取到验证码:', code); return code; } console.log('[LOG] 该邮件未提取到验证码,继续...'); } } } catch (e) { console.log('[LOG] 检查邮件出错:', e.message); } } throw new Error('超时:未能在规定时间内收到验证码'); }