重构修改2925获取邮箱

This commit is contained in:
QLHazyCoder
2026-04-18 17:28:43 +08:00
parent 7a4b1e1e12
commit 8e3e085f3d
8 changed files with 717 additions and 205 deletions
+31 -3
View File
@@ -51,9 +51,14 @@
});
}
function normalizeStep8VerificationTargetEmail(value) {
return String(value || '').trim().toLowerCase();
}
async function runStep8Attempt(state) {
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const stepStartedAt = Date.now();
const authTabId = await getTabId('signup-page');
@@ -67,10 +72,25 @@
}
throwIfStopped();
await ensureStep8VerificationPageReady({
const pageState = await ensureStep8VerificationPageReady({
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪'),
});
const shouldCompareVerificationEmail = mail.provider !== '2925';
const displayedVerificationEmail = shouldCompareVerificationEmail
? normalizeStep8VerificationTargetEmail(pageState?.displayedEmail)
: '';
const fixedTargetEmail = shouldCompareVerificationEmail
? (displayedVerificationEmail || normalizeStep8VerificationTargetEmail(state?.email))
: '';
await setState({
step8VerificationTargetEmail: displayedVerificationEmail || '',
});
await addLog('步骤 8:登录验证码页面已就绪,开始获取验证码。', 'info');
if (shouldCompareVerificationEmail && displayedVerificationEmail) {
await addLog(`步骤 8:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info');
}
if (shouldUseCustomRegistrationEmail(state)) {
await confirmCustomVerificationStepBypass(8);
@@ -78,7 +98,11 @@
}
throwIfStopped();
if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
if (
mail.provider === HOTMAIL_PROVIDER
|| mail.provider === LUCKMAIL_PROVIDER
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
) {
await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`);
} else {
await addLog(`步骤 8:正在打开${mail.label}...`);
@@ -102,10 +126,14 @@
}
}
await resolveVerificationStep(8, state, mail, {
await resolveVerificationStep(8, {
...state,
step8VerificationTargetEmail: displayedVerificationEmail || '',
}, mail, {
filterAfterTimestamp: stepStartedAt,
getRemainingTimeMs: getStep8RemainingTimeResolver(),
requestFreshCodeFirst: false,
targetEmail: fixedTargetEmail,
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
? 0
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
+31 -3
View File
@@ -113,7 +113,7 @@
const is2925Provider = state?.mailProvider === '2925';
if (step === 4) {
return {
filterAfterTimestamp: getHotmailVerificationRequestTimestamp(4, state),
filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(4, state),
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
targetEmail: state.email,
@@ -124,10 +124,10 @@
}
return {
filterAfterTimestamp: getHotmailVerificationRequestTimestamp(8, state),
filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(8, state),
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
targetEmail: state.email,
targetEmail: String(state?.step8VerificationTargetEmail || '').trim() || state.email,
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
...overrides,
@@ -236,6 +236,33 @@
return requestedAt;
}
function triggerPostSuccessMailboxCleanup(step, mail) {
if (mail?.provider !== '2925') {
return;
}
Promise.resolve().then(async () => {
try {
await sendToMailContentScriptResilient(
mail,
{
type: 'DELETE_ALL_EMAILS',
step,
source: 'background',
payload: {},
},
{
timeoutMs: 10000,
responseTimeoutMs: 5000,
maxRecoveryAttempts: 1,
}
);
} catch (_) {
// Best-effort cleanup only.
}
});
}
async function pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides = {}) {
const stateKey = getVerificationCodeStateKey(step);
const rejectedCodes = new Set();
@@ -656,6 +683,7 @@
emailTimestamp: result.emailTimestamp,
code: result.code,
});
triggerPostSuccessMailboxCleanup(step, mail);
return;
}
}
+259 -148
View File
@@ -1,4 +1,4 @@
// content/mail-2925.js Content script for 2925 Mail (steps 4, 7)
// content/mail-2925.js - Content script for 2925 Mail (steps 4, 8)
// Injected dynamically on: 2925.com
const MAIL2925_PREFIX = '[MultiPage:mail-2925]';
@@ -37,19 +37,28 @@ async function persistSeenCodes() {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'POLL_EMAIL') {
resetStopState();
handlePollEmail(message.step, message.payload).then(result => {
handlePollEmail(message.step, message.payload).then((result) => {
sendResponse(result);
}).catch(err => {
}).catch((err) => {
if (isStopError(err)) {
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
sendResponse({ error: err.message });
});
return true;
}
if (message.type === 'DELETE_ALL_EMAILS') {
Promise.resolve(deleteAllMailboxEmails(message.step)).catch(() => {});
sendResponse({ ok: true });
return true;
}
return false;
});
const MAIL_ITEM_SELECTORS = [
@@ -64,13 +73,67 @@ const MAIL_ITEM_SELECTORS = [
'[class*="list-item"]',
'li[class*="mail"]',
];
const MAIL_REFRESH_SELECTOR = '[class*="refresh"], [title*="鍒锋柊"], [aria-label*="鍒锋柊"], [class*="Refresh"]';
const MAIL_ITEM_SELECTOR_GROUP = MAIL_ITEM_SELECTORS.join(', ');
const MAIL_REFRESH_SELECTORS = [
'[class*="refresh"]',
'[title*="刷新"]',
'[aria-label*="刷新"]',
'[class*="Refresh"]',
];
const MAIL_INBOX_SELECTORS = [
'a[href*="mailList"]',
'[class*="inbox"]',
'[class*="Inbox"]',
'[title*="鏀朵欢绠?]',
'[title*="收件箱"]',
];
const MAIL_DELETE_SELECTORS = [
'[class*="delete"]',
'[title*="删除"]',
'[aria-label*="删除"]',
'[class*="Delete"]',
];
const MAIL_SELECT_ALL_SELECTORS = [
'input[type="checkbox"]',
'[role="checkbox"]',
'.el-checkbox__input',
'.el-checkbox',
'label[class*="checkbox"]',
'[class*="checkbox"]',
];
const MAIL_ACTION_CANDIDATE_SELECTORS = 'button, [role="button"], a, label, span, div';
function normalizeNodeText(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function isVisibleNode(node) {
if (!node) return false;
if (node.hidden) return false;
const style = typeof window.getComputedStyle === 'function'
? window.getComputedStyle(node)
: null;
if (style && (style.display === 'none' || style.visibility === 'hidden')) {
return false;
}
const rect = typeof node.getBoundingClientRect === 'function'
? node.getBoundingClientRect()
: null;
if (rect && rect.width <= 0 && rect.height <= 0) {
return false;
}
return true;
}
function isMailItemNode(node) {
return Boolean(node?.closest?.(MAIL_ITEM_SELECTOR_GROUP));
}
function resolveActionTarget(node) {
return node?.closest?.('button, [role="button"], a, label, .el-checkbox, .el-checkbox__input') || node || null;
}
function findMailItems() {
for (const selector of MAIL_ITEM_SELECTORS) {
@@ -82,20 +145,81 @@ function findMailItems() {
return [];
}
function findRefreshButton() {
return document.querySelector(MAIL_REFRESH_SELECTOR);
}
function findInboxLink() {
for (const selector of MAIL_INBOX_SELECTORS) {
const node = document.querySelector(selector);
if (node) {
return node;
function findActionBySelectors(selectors = []) {
for (const selector of selectors) {
const candidates = document.querySelectorAll(selector);
for (const candidate of candidates) {
const target = resolveActionTarget(candidate);
if (!isVisibleNode(target) || isMailItemNode(target)) {
continue;
}
return target;
}
}
return null;
}
function findToolbarActionButton(patterns = [], selectors = []) {
const directMatch = findActionBySelectors(selectors);
if (directMatch) {
return directMatch;
}
const candidates = document.querySelectorAll(MAIL_ACTION_CANDIDATE_SELECTORS);
for (const candidate of candidates) {
const target = resolveActionTarget(candidate);
if (!isVisibleNode(target) || isMailItemNode(target)) {
continue;
}
const text = normalizeNodeText(target.innerText || target.textContent || '');
const label = normalizeNodeText(target.getAttribute?.('aria-label') || target.getAttribute?.('title') || '');
if (patterns.some((pattern) => pattern.test(text) || pattern.test(label))) {
return target;
}
}
return null;
}
function findRefreshButton() {
return findToolbarActionButton([
/刷新/i,
/refresh/i,
], MAIL_REFRESH_SELECTORS);
}
function findInboxLink() {
return findActionBySelectors(MAIL_INBOX_SELECTORS);
}
function findDeleteButton() {
return findToolbarActionButton([
/删除/i,
/delete/i,
], MAIL_DELETE_SELECTORS);
}
function findSelectAllControl() {
return findActionBySelectors(MAIL_SELECT_ALL_SELECTORS);
}
function isCheckboxChecked(node) {
const checkbox = node?.matches?.('input[type="checkbox"], [role="checkbox"]')
? node
: node?.querySelector?.('input[type="checkbox"], [role="checkbox"]');
if (checkbox?.checked === true) {
return true;
}
if (String(checkbox?.getAttribute?.('aria-checked') || '').toLowerCase() === 'true') {
return true;
}
return Boolean(
node?.classList?.contains('is-checked')
|| node?.classList?.contains('checked')
);
}
function getMailItemText(item) {
if (!item) return '';
const contentCell = item.querySelector('td.content, .content, .mail-content');
@@ -112,11 +236,11 @@ function getMailItemText(item) {
function getMailItemTimeText(item) {
const timeEl = item?.querySelector('.date-time-text, [class*="date-time"], [class*="time"], td.time');
return (timeEl?.textContent || '').replace(/\s+/g, ' ').trim();
return normalizeNodeText(timeEl?.textContent || '');
}
function normalizeMailIdentityPart(value) {
return (value || '').replace(/\s+/g, ' ').trim().toLowerCase();
return normalizeNodeText(value).toLowerCase();
}
function getMailItemId(item, index = 0) {
@@ -148,82 +272,36 @@ function getCurrentMailIds(items = []) {
return ids;
}
function normalizeMinuteTimestamp(timestamp) {
if (!Number.isFinite(timestamp) || timestamp <= 0) return 0;
const date = new Date(timestamp);
date.setSeconds(0, 0);
return date.getTime();
}
function matchesMailFilters(text, senderFilters, subjectFilters) {
const lower = (text || '').toLowerCase();
const senderMatch = senderFilters.some(filter => lower.includes(filter.toLowerCase()));
const subjectMatch = subjectFilters.some(filter => lower.includes(filter.toLowerCase()));
const lower = String(text || '').toLowerCase();
const senderMatch = senderFilters.some((filter) => lower.includes(String(filter || '').toLowerCase()));
const subjectMatch = subjectFilters.some((filter) => lower.includes(String(filter || '').toLowerCase()));
return senderMatch || subjectMatch;
}
function extractVerificationCode(text, strictChatGPTCodeOnly = false) {
if (strictChatGPTCodeOnly) {
const strictMatch = text.match(/your\s+chatgpt\s+code\s+is\s+(\d{6})/i);
const strictMatch = String(text || '').match(/your\s+chatgpt\s+code\s+is\s+(\d{6})/i);
return strictMatch ? strictMatch[1] : null;
}
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s:]*(\d{6})/);
const normalized = String(text || '');
const matchCn = normalized.match(/(?:代码为|验证码[^0-9]*?)[\s:]*(\d{6})/);
if (matchCn) return matchCn[1];
const matchChatGPT = text.match(/your\s+chatgpt\s+code\s+is\s+(\d{6})/i);
const matchChatGPT = normalized.match(/your\s+chatgpt\s+code\s+is\s+(\d{6})/i);
if (matchChatGPT) return matchChatGPT[1];
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
const matchEn = normalized.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
if (matchEn) return matchEn[1] || matchEn[2];
const match6 = text.match(/\b(\d{6})\b/);
const match6 = normalized.match(/\b(\d{6})\b/);
if (match6) return match6[1];
return null;
}
function extractEmails(text) {
const matches = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig) || [];
return [...new Set(matches.map(item => item.toLowerCase()))];
}
function emailMatchesTarget(candidate, targetEmail) {
const normalizedCandidate = String(candidate || '').trim().toLowerCase();
const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
return Boolean(normalizedCandidate && normalizedTarget && normalizedCandidate === normalizedTarget);
}
function getTargetEmailMatchState(text, targetEmail) {
const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
if (!normalizedTarget) {
return { matches: true, hasExplicitEmail: false };
}
const normalizedText = String(text || '').toLowerCase();
if (normalizedText.includes(normalizedTarget)) {
return { matches: true, hasExplicitEmail: true };
}
const atIndex = normalizedTarget.indexOf('@');
if (atIndex > 0) {
const encodedTarget = `${normalizedTarget.slice(0, atIndex)}=${normalizedTarget.slice(atIndex + 1)}`;
if (normalizedText.includes(encodedTarget)) {
return { matches: true, hasExplicitEmail: true };
}
}
const emails = extractEmails(text);
if (!emails.length) {
return { matches: false, hasExplicitEmail: false };
}
return {
matches: emails.some(email => emailMatchesTarget(email, normalizedTarget)),
hasExplicitEmail: true,
};
}
function parseMailItemTimestamp(item) {
const timeText = getMailItemTimeText(item);
if (!timeText) return null;
@@ -236,7 +314,7 @@ function parseMailItemTimestamp(item) {
return now.getTime();
}
match = timeText.match(/(\d+)\s*分(?:钟)?前/);
match = timeText.match(/(\d+)\s*分前/);
if (match) {
return now.getTime() - Number(match[1]) * 60 * 1000;
}
@@ -324,19 +402,70 @@ async function openMailAndGetMessageText(item) {
}
}
async function deleteCurrentMailboxEmail(step) {
try {
const deleteButton = findDeleteButton();
if (!deleteButton) {
return false;
}
simulateClick(deleteButton);
await sleepRandom(200, 500);
return true;
} catch (err) {
console.warn(MAIL2925_PREFIX, `Step ${step}: delete-current cleanup failed:`, err?.message || err);
return false;
}
}
async function openMailAndDeleteAfterRead(item, step) {
simulateClick(item);
try {
await sleepRandom(1200, 2200);
return document.body?.textContent || '';
} finally {
await deleteCurrentMailboxEmail(step);
await returnToInbox();
}
}
async function deleteAllMailboxEmails(step) {
try {
await returnToInbox();
const selectAllControl = findSelectAllControl();
if (!selectAllControl) {
return false;
}
if (!isCheckboxChecked(selectAllControl)) {
simulateClick(selectAllControl);
await sleepRandom(200, 500);
}
const deleteButton = findDeleteButton();
if (!deleteButton) {
return false;
}
simulateClick(deleteButton);
await sleepRandom(200, 500);
return true;
} catch (err) {
console.warn(MAIL2925_PREFIX, `Step ${step}: delete-all cleanup failed:`, err?.message || err);
return false;
}
}
async function refreshInbox() {
const refreshBtn = document.querySelector(
'[class*="refresh"], [title*="刷新"], [aria-label*="刷新"], [class*="Refresh"]'
);
const refreshBtn = findRefreshButton();
if (refreshBtn) {
simulateClick(refreshBtn);
await sleepRandom(700, 1200);
return;
}
const inboxLink = document.querySelector(
'a[href*="mailList"], [class*="inbox"], [class*="Inbox"], [title*="收件箱"]'
);
const inboxLink = findInboxLink();
if (inboxLink) {
simulateClick(inboxLink);
await sleepRandom(700, 1200);
@@ -349,27 +478,26 @@ async function handlePollEmail(step, payload) {
subjectFilters,
maxAttempts,
intervalMs,
filterAfterTimestamp = 0,
excludeCodes = [],
strictChatGPTCodeOnly = false,
targetEmail = '',
} = payload;
} = payload || {};
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
log(`步骤 ${step}:开始轮询 2925 邮箱(最多 ${maxAttempts} 次)`);
if (filterAfterMinute) {
log(`步骤 ${step}:仅尝试 ${new Date(filterAfterMinute).toLocaleString('zh-CN', { hour12: false })} 及之后时间的邮件。`);
}
let initialItems = [];
for (let i = 0; i < 20; i++) {
let initialLoadUsedRefresh = false;
for (let i = 0; i < 20; i += 1) {
initialItems = findMailItems();
if (initialItems.length > 0) break;
if (initialItems.length > 0) {
break;
}
await sleep(500);
}
if (initialItems.length === 0) {
initialLoadUsedRefresh = true;
await returnToInbox();
await refreshInbox();
await sleep(2000);
@@ -380,16 +508,16 @@ async function handlePollEmail(step, payload) {
throw new Error('2925 邮箱列表未加载完成,请确认当前已打开收件箱。');
}
const existingMailIds = getCurrentMailIds(initialItems);
const knownMailIds = getCurrentMailIds(initialItems);
log(`步骤 ${step}:邮件列表已加载,共 ${initialItems.length} 封邮件`);
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`);
log(`步骤 ${step}:已记录当前 ${knownMailIds.size} 封旧邮件快照`);
const FALLBACK_AFTER = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
log(`步骤 ${step}:正在轮询 2925 邮箱,第 ${attempt}/${maxAttempts}`);
if (attempt > 1) {
if (attempt > 1 || !initialLoadUsedRefresh) {
await returnToInbox();
await refreshInbox();
await sleepRandom(900, 1500);
@@ -398,78 +526,61 @@ async function handlePollEmail(step, payload) {
const items = findMailItems();
if (items.length > 0) {
const useFallback = attempt > FALLBACK_AFTER;
const newMailIds = new Set();
for (let index = 0; index < items.length; index++) {
items.forEach((item, index) => {
const itemId = getMailItemId(item, index);
if (!knownMailIds.has(itemId)) {
newMailIds.add(itemId);
}
});
for (let index = 0; index < items.length; index += 1) {
const item = items[index];
const itemId = getMailItemId(item, index);
const isNewMail = newMailIds.has(itemId);
const itemTimestamp = parseMailItemTimestamp(item);
const itemMinute = normalizeMinuteTimestamp(itemTimestamp || 0);
const passesTimeFilter = !filterAfterMinute || (itemMinute && itemMinute >= filterAfterMinute);
const shouldBypassOldSnapshot = Boolean(filterAfterMinute && passesTimeFilter && itemMinute > 0);
if (!passesTimeFilter) {
if (!useFallback && !isNewMail) {
continue;
}
if (!useFallback && !shouldBypassOldSnapshot && existingMailIds.has(itemId)) {
const previewText = getMailItemText(item);
if (!matchesMailFilters(previewText, senderFilters, subjectFilters)) {
continue;
}
const text = getMailItemText(item);
if (!matchesMailFilters(text, senderFilters, subjectFilters)) {
continue;
}
const previewEmails = extractEmails(text);
const previewTargetState = getTargetEmailMatchState(text, targetEmail);
const previewMatchesTarget = previewTargetState.matches;
if (targetEmail && previewEmails.length > 0 && !previewMatchesTarget) {
continue;
}
const code = extractVerificationCode(text, strictChatGPTCodeOnly);
if (code && previewMatchesTarget) {
if (excludedCodeSet.has(code)) {
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
continue;
}
if (seenCodes.has(code)) {
log(`步骤 ${step}:跳过已处理过的验证码:${code}`, 'info');
continue;
}
seenCodes.add(code);
persistSeenCodes();
const source = useFallback && existingMailIds.has(itemId) ? '回退匹配邮件' : '新邮件';
const timeLabel = itemTimestamp ? `,时间:${new Date(itemTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
log(`步骤 ${step}:已找到验证码:${code}(来源:${source}${timeLabel}`, 'ok');
await sleep(1000);
return { ok: true, code, emailTimestamp: Date.now() };
}
const openedText = await openMailAndGetMessageText(item);
const previewCode = extractVerificationCode(previewText, strictChatGPTCodeOnly);
const openedText = await openMailAndDeleteAfterRead(item, step);
const bodyCode = extractVerificationCode(openedText, strictChatGPTCodeOnly);
const openedTargetState = getTargetEmailMatchState(openedText, targetEmail);
if (targetEmail && openedTargetState.hasExplicitEmail && !openedTargetState.matches) {
const candidateCode = bodyCode || previewCode;
if (!candidateCode) {
continue;
}
if (bodyCode) {
if (excludedCodeSet.has(bodyCode)) {
log(`步骤 ${step}:跳过排除的验证码:${bodyCode}`, 'info');
continue;
}
if (seenCodes.has(bodyCode)) {
log(`步骤 ${step}:跳过已处理过的验证码:${bodyCode}`, 'info');
continue;
}
seenCodes.add(bodyCode);
persistSeenCodes();
const source = useFallback && existingMailIds.has(itemId) ? '回退匹配邮件正文' : '新邮件正文';
const timeLabel = itemTimestamp ? `,时间:${new Date(itemTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
log(`步骤 ${step}:已在邮件正文中找到验证码:${bodyCode}(来源:${source}${timeLabel}`, 'ok');
await sleep(1000);
return { ok: true, code: bodyCode, emailTimestamp: Date.now() };
if (excludedCodeSet.has(candidateCode)) {
log(`步骤 ${step}:跳过排除的验证码:${candidateCode}`, 'info');
continue;
}
if (seenCodes.has(candidateCode)) {
log(`步骤 ${step}:跳过已处理过的验证码:${candidateCode}`, 'info');
continue;
}
seenCodes.add(candidateCode);
persistSeenCodes();
const source = useFallback && !isNewMail
? (bodyCode ? '回退匹配邮件正文' : '回退匹配邮件')
: (bodyCode ? '新邮件正文' : '新邮件');
const timeLabel = itemTimestamp ? `,时间:${new Date(itemTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
log(`步骤 ${step}:已找到验证码:${candidateCode}(来源:${source}${timeLabel}`, 'ok');
return { ok: true, code: candidateCode, emailTimestamp: Date.now() };
}
items.forEach((item, index) => {
knownMailIds.add(getMailItemId(item, index));
});
}
if (attempt === FALLBACK_AFTER + 1) {
+8
View File
@@ -695,6 +695,12 @@ function getPageTextSnapshot() {
.trim();
}
function getLoginVerificationDisplayedEmail() {
const pageText = getPageTextSnapshot();
const matches = pageText.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig) || [];
return matches[0] ? String(matches[0]).trim().toLowerCase() : '';
}
function getOAuthConsentForm() {
return document.querySelector(OAUTH_CONSENT_FORM_SELECTOR);
}
@@ -1076,6 +1082,7 @@ function inspectLoginAuthState() {
state: 'unknown',
url: location.href,
path: location.pathname || '',
displayedEmail: getLoginVerificationDisplayedEmail(),
retryButton: retryState?.retryButton || null,
retryEnabled: Boolean(retryState?.retryEnabled),
titleMatched: Boolean(retryState?.titleMatched),
@@ -1141,6 +1148,7 @@ function serializeLoginAuthState(snapshot) {
state: snapshot?.state || 'unknown',
url: snapshot?.url || location.href,
path: snapshot?.path || location.pathname || '',
displayedEmail: snapshot?.displayedEmail || '',
retryEnabled: Boolean(snapshot?.retryEnabled),
titleMatched: Boolean(snapshot?.titleMatched),
detailMatched: Boolean(snapshot?.detailMatched),
+61 -2
View File
@@ -13,6 +13,7 @@ test('step 8 submits login verification directly without replaying step 7', asyn
executeStep7: 0,
sleep: [],
resolveOptions: null,
setStates: [],
};
const realDateNow = Date.now;
Date.now = () => 123456;
@@ -29,7 +30,7 @@ test('step 8 submits login verification directly without replaying step 7', asyn
ensureStep8VerificationPageReady: async (options) => {
calls.ensureReady += 1;
calls.ensureReadyOptions.push(options || null);
return { state: 'verification_page' };
return { state: 'verification_page', displayedEmail: 'display.user@example.com' };
},
executeStep7: async () => {
calls.executeStep7 += 1;
@@ -53,7 +54,9 @@ test('step 8 submits login verification directly without replaying step 7', asyn
calls.resolveOptions = options;
},
reuseOrCreateTab: async () => {},
setState: async () => {},
setState: async (payload) => {
calls.setStates.push(payload);
},
setStepStatus: async () => {},
shouldUseCustomRegistrationEmail: () => false,
sleepWithStop: async (ms) => {
@@ -82,6 +85,10 @@ test('step 8 submits login verification directly without replaying step 7', asyn
assert.equal(typeof calls.resolveOptions.getRemainingTimeMs, 'function');
assert.equal(await calls.resolveOptions.getRemainingTimeMs({ actionLabel: '登录验证码流程' }), 5000);
assert.equal(calls.resolveOptions.resendIntervalMs, 25000);
assert.equal(calls.resolveOptions.targetEmail, 'display.user@example.com');
assert.deepStrictEqual(calls.setStates, [
{ step8VerificationTargetEmail: 'display.user@example.com' },
]);
assert.deepStrictEqual(calls.ensureReadyOptions, [
{ timeoutMs: 5000 },
]);
@@ -136,10 +143,62 @@ test('step 8 disables resend interval for 2925 mailbox polling', async () => {
});
assert.equal(capturedOptions.resendIntervalMs, 0);
assert.equal(capturedOptions.targetEmail, '');
assert.equal(capturedOptions.beforeSubmit, undefined);
assert.equal(typeof capturedOptions.getRemainingTimeMs, 'function');
});
test('step 8 falls back to the run email when the verification page does not expose a displayed email', async () => {
let capturedOptions = null;
const executor = api.createStep8Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page', displayedEmail: '' }),
executeStep7: async () => {},
getOAuthFlowRemainingMs: async () => 8000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000),
getMailConfig: () => ({
provider: 'qq',
label: 'QQ 邮箱',
source: 'mail-qq',
url: 'https://mail.qq.com',
navigateOnReuse: false,
}),
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
isVerificationMailPollingError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
resolveVerificationStep: async (_step, _state, _mail, options) => {
capturedOptions = options;
},
reuseOrCreateTab: async () => {},
setState: async () => {},
setStepStatus: async () => {},
shouldUseCustomRegistrationEmail: () => false,
sleepWithStop: async () => {},
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
});
await executor.executeStep8({
email: 'user@example.com',
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
});
assert.equal(capturedOptions.targetEmail, 'user@example.com');
});
test('step 8 does not rerun step 7 when verification submit lands on add-phone', async () => {
const calls = {
executeStep7: 0,
+245 -49
View File
@@ -51,37 +51,38 @@ function extractFunction(name) {
return source.slice(start, end);
}
test('handlePollEmail returns to inbox before initial refresh when 2925 opens on a detail page', async () => {
test('handlePollEmail establishes a baseline after opening from detail view and only picks mail from a later refresh', async () => {
const bundle = extractFunction('handlePollEmail');
const api = new Function(`
let detailMode = true;
let state = 'detail';
let refreshCalls = 0;
const clickOrder = [];
const readAndDeleteCalls = [];
const seenCodes = new Set();
const mailItem = { text: 'OpenAI verification code 654321' };
const baselineMail = { id: 'baseline', text: 'OpenAI newsletter without code' };
const newMail = { id: 'new', text: 'OpenAI verification code 654321' };
function findMailItems() {
return detailMode ? [] : [mailItem];
if (state === 'detail') return [];
if (state === 'baseline') return [baselineMail];
return [baselineMail, newMail];
}
function getMailItemId() {
return 'mail-1';
function getMailItemId(item) {
return item.id;
}
function getCurrentMailIds(items = []) {
return new Set(items.map(() => 'mail-1'));
}
function normalizeMinuteTimestamp(value) {
return Number(value) || 0;
return new Set(items.map((item) => item.id));
}
function parseMailItemTimestamp() {
return Date.now();
}
function matchesMailFilters() {
return true;
function matchesMailFilters(text) {
return /openai|verification/i.test(String(text || ''));
}
function getMailItemText(item) {
@@ -93,29 +94,26 @@ function extractVerificationCode(text) {
return match ? match[1] : null;
}
function extractEmails() {
return [];
}
function emailMatchesTarget() {
return true;
}
function getTargetEmailMatchState() {
return { matches: true, hasExplicitEmail: false };
}
async function sleep() {}
async function sleepRandom() {}
async function returnToInbox() {
clickOrder.push('inbox');
detailMode = false;
state = 'baseline';
return true;
}
async function refreshInbox() {
clickOrder.push('refresh');
refreshCalls += 1;
if (refreshCalls >= 2) {
state = 'with-new';
}
}
async function openMailAndDeleteAfterRead(item) {
readAndDeleteCalls.push(item.id);
return item.id === 'new' ? 'Your ChatGPT code is 654321' : 'No code here';
}
function persistSeenCodes() {}
@@ -128,38 +126,113 @@ return {
getClickOrder() {
return clickOrder.slice();
},
getReadAndDeleteCalls() {
return readAndDeleteCalls.slice();
},
};
`)();
const result = await api.handlePollEmail(4, {
senderFilters: ['openai'],
subjectFilters: ['verification'],
maxAttempts: 1,
maxAttempts: 2,
intervalMs: 1,
filterAfterTimestamp: Date.now(),
});
assert.equal(result.code, '654321');
assert.deepEqual(api.getClickOrder(), ['inbox', 'refresh']);
assert.deepEqual(api.getClickOrder(), ['inbox', 'refresh', 'inbox', 'refresh']);
assert.deepEqual(api.getReadAndDeleteCalls(), ['new']);
});
test('handlePollEmail ignores targetEmail and still tests any matching ChatGPT mail', async () => {
const bundle = extractFunction('handlePollEmail');
const api = new Function(`
let state = 'empty';
const seenCodes = new Set();
const readAndDeleteCalls = [];
const matchingMail = {
id: 'mail-1',
text: 'ChatGPT verification code 112233 for another.user@example.com',
};
function findMailItems() {
return state === 'ready' ? [matchingMail] : [];
}
function getMailItemId(item) {
return item.id;
}
function getCurrentMailIds(items = []) {
return new Set(items.map((item) => item.id));
}
function parseMailItemTimestamp() {
return Date.now();
}
function matchesMailFilters(text) {
return /chatgpt|openai|verification/i.test(String(text || ''));
}
function getMailItemText(item) {
return item.text;
}
function extractVerificationCode(text) {
const match = String(text || '').match(/(\\d{6})/);
return match ? match[1] : null;
}
async function sleep() {}
async function sleepRandom() {}
async function returnToInbox() {
return true;
}
async function refreshInbox() {
state = 'ready';
}
async function openMailAndDeleteAfterRead(item) {
readAndDeleteCalls.push(item.id);
return item.text;
}
function persistSeenCodes() {}
function log() {}
${bundle}
return {
handlePollEmail,
getReadAndDeleteCalls() {
return readAndDeleteCalls.slice();
},
};
`)();
const result = await api.handlePollEmail(8, {
senderFilters: ['chatgpt'],
subjectFilters: ['verification'],
maxAttempts: 4,
intervalMs: 1,
targetEmail: 'expected@example.com',
});
assert.equal(result.code, '112233');
assert.deepEqual(api.getReadAndDeleteCalls(), ['mail-1']);
});
test('openMailAndGetMessageText always returns to inbox after opening a 2925 message', async () => {
const bundle = [
extractFunction('findInboxLink'),
extractFunction('returnToInbox'),
extractFunction('openMailAndGetMessageText'),
].join('\n');
const api = new Function(`
const MAIL_INBOX_SELECTORS = [
'a[href*="mailList"]',
'[class*="inbox"]',
'[class*="Inbox"]',
'[title*="鏀朵欢绠?]',
];
const clickOrder = [];
const mailItem = { kind: 'mail' };
const inboxLink = { kind: 'inbox' };
let listVisible = true;
let bodyText = '';
@@ -169,18 +242,16 @@ const document = {
return bodyText;
},
},
querySelector(selector) {
if (selector.includes('mailList') || selector.includes('inbox') || selector.includes('Inbox')) {
return inboxLink;
}
return null;
},
};
function findMailItems() {
return listVisible ? [mailItem] : [];
}
function findInboxLink() {
return { kind: 'inbox' };
}
function simulateClick(node) {
if (node === mailItem) {
clickOrder.push('mail');
@@ -188,12 +259,8 @@ function simulateClick(node) {
bodyText = 'Your ChatGPT code is 731091';
return;
}
if (node === inboxLink) {
clickOrder.push('inbox');
listVisible = true;
return;
}
throw new Error('unexpected node');
clickOrder.push('inbox');
listVisible = true;
}
async function sleep() {}
@@ -219,3 +286,132 @@ return {
assert.deepEqual(api.getClickOrder(), ['mail', 'inbox']);
assert.equal(api.isListVisible(), true);
});
test('openMailAndDeleteAfterRead deletes the opened message before returning to inbox', async () => {
const bundle = [
extractFunction('deleteCurrentMailboxEmail'),
extractFunction('returnToInbox'),
extractFunction('openMailAndDeleteAfterRead'),
].join('\n');
const api = new Function(`
const calls = [];
const mailItem = { kind: 'mail' };
const deleteButton = { kind: 'delete' };
let listVisible = true;
let bodyText = '';
const document = {
body: {
get textContent() {
return bodyText;
},
},
};
function findMailItems() {
return listVisible ? [mailItem] : [];
}
function findDeleteButton() {
return deleteButton;
}
function findInboxLink() {
return { kind: 'inbox' };
}
function simulateClick(node) {
if (node === mailItem) {
calls.push('mail');
listVisible = false;
bodyText = 'Your ChatGPT code is 778899';
return;
}
if (node === deleteButton) {
calls.push('delete');
return;
}
calls.push('inbox');
listVisible = true;
}
async function sleep() {}
async function sleepRandom() {}
const console = { warn() {} };
const MAIL2925_PREFIX = '[MultiPage:mail-2925]';
${bundle}
return {
mailItem,
openMailAndDeleteAfterRead,
getCalls() {
return calls.slice();
},
};
`)();
const text = await api.openMailAndDeleteAfterRead(api.mailItem, 8);
assert.match(text, /778899/);
assert.deepEqual(api.getCalls(), ['mail', 'delete', 'inbox']);
});
test('deleteAllMailboxEmails selects all messages and clicks delete', async () => {
const bundle = extractFunction('deleteAllMailboxEmails');
const api = new Function(`
const calls = [];
const selectAllControl = { kind: 'select-all' };
const deleteButton = { kind: 'delete' };
async function returnToInbox() {
calls.push('inbox');
return true;
}
function findSelectAllControl() {
return selectAllControl;
}
function isCheckboxChecked() {
return false;
}
function findDeleteButton() {
return deleteButton;
}
function simulateClick(node) {
if (node === selectAllControl) {
calls.push('select-all');
return;
}
if (node === deleteButton) {
calls.push('delete');
return;
}
throw new Error('unexpected node');
}
async function sleepRandom() {}
const console = { warn() {} };
const MAIL2925_PREFIX = '[MultiPage:mail-2925]';
${bundle}
return {
deleteAllMailboxEmails,
getCalls() {
return calls.slice();
},
};
`)();
const result = await api.deleteAllMailboxEmails(4);
assert.equal(result, true);
assert.deepEqual(api.getCalls(), ['inbox', 'select-all', 'delete']);
});
+19
View File
@@ -51,6 +51,8 @@ function extractFunction(name) {
}
const bundle = [
extractFunction('getPageTextSnapshot'),
extractFunction('getLoginVerificationDisplayedEmail'),
extractFunction('inspectLoginAuthState'),
extractFunction('normalizeStep6Snapshot'),
].join('\n');
@@ -62,6 +64,13 @@ const location = {
pathname: ${JSON.stringify(overrides.pathname || '/log-in')},
};
const document = {
body: {
innerText: ${JSON.stringify(overrides.pageText || '')},
textContent: ${JSON.stringify(overrides.pageText || '')},
},
};
function getLoginTimeoutErrorPageState() {
return ${JSON.stringify(overrides.retryState || null)};
}
@@ -127,6 +136,16 @@ return {
);
}
{
const api = createApi({
verificationTarget: { id: 'otp' },
pageText: 'We emailed a code to display.user@example.com. Enter it below.',
});
const snapshot = api.inspectLoginAuthState();
assert.strictEqual(snapshot.displayedEmail, 'display.user@example.com');
}
{
const api = createApi({
oauthConsentPage: true,
+63
View File
@@ -37,8 +37,10 @@ test('verification flow extends 2925 polling window', () => {
const step4Payload = helpers.getVerificationPollPayload(4, { email: 'user@example.com', mailProvider: '2925' });
const step8Payload = helpers.getVerificationPollPayload(8, { email: 'user@example.com', mailProvider: '2925' });
assert.equal(step4Payload.filterAfterTimestamp, 0);
assert.equal(step4Payload.maxAttempts, 15);
assert.equal(step4Payload.intervalMs, 15000);
assert.equal(step8Payload.filterAfterTimestamp, 0);
assert.equal(step8Payload.maxAttempts, 15);
assert.equal(step8Payload.intervalMs, 15000);
});
@@ -109,6 +111,67 @@ test('verification flow runs beforeSubmit hook before filling the code', async (
]);
});
test('verification flow triggers 2925 mailbox cleanup only after code submission succeeds', async () => {
const mailMessages = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async (_source, message) => {
if (message.type === 'FILL_CODE') {
return {};
}
return {};
},
sendToMailContentScriptResilient: async (_mail, message) => {
mailMessages.push(message.type);
if (message.type === 'POLL_EMAIL') {
return { code: '654321', emailTimestamp: 123 };
}
return { ok: true };
},
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
await helpers.resolveVerificationStep(
8,
{
email: 'user@example.com',
mailProvider: '2925',
lastLoginCode: null,
},
{ provider: '2925', label: '2925 邮箱' },
{}
);
await new Promise((resolve) => setTimeout(resolve, 0));
assert.deepStrictEqual(mailMessages, ['POLL_EMAIL', 'DELETE_ALL_EMAILS']);
});
test('verification flow treats add-phone after login code submit as fatal instead of completing step 8', async () => {
const events = [];