269 lines
10 KiB
JavaScript
269 lines
10 KiB
JavaScript
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<string>} 验证码
|
||
*/
|
||
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 {
|
||
// 每个邮件包含 <span data-date>,找到所有带时间戳的行
|
||
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('超时:未能在规定时间内收到验证码');
|
||
}
|