// content/phplife-mail.js — Content script for A4Sky mailbox on mail.phplife.net // Injected dynamically on: mail.phplife.net const PHPLIFE_MAIL_PREFIX = '[MultiPage:mail-phplife]'; const isTopFrame = window === window.top; console.log(PHPLIFE_MAIL_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child'); if (!isTopFrame) { console.log(PHPLIFE_MAIL_PREFIX, 'Skipping child frame'); } else { let seenCodes = new Set(); let waitingLoginLogged = false; async function loadSeenCodes() { try { const data = await chrome.storage.session.get('seenPhplifeCodes'); if (Array.isArray(data.seenPhplifeCodes)) { seenCodes = new Set(data.seenPhplifeCodes.filter(Boolean)); } } catch (err) { console.warn(PHPLIFE_MAIL_PREFIX, 'Could not load seen codes:', err?.message || err); } } loadSeenCodes(); async function persistSeenCodes() { try { await chrome.storage.session.set({ seenPhplifeCodes: [...seenCodes] }); } catch (err) { console.warn(PHPLIFE_MAIL_PREFIX, 'Could not persist seen codes:', err?.message || err); } } chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'POLL_EMAIL') { resetStopState(); handlePollEmail(message.step, message.payload).then((result) => { sendResponse(result); }).catch((err) => { if (isStopError(err)) { log(`步骤 ${message.step}:已被用户停止。`, 'warn'); sendResponse({ stopped: true, error: err.message }); return; } log(`步骤 ${message.step}:A4Sky 邮箱轮询失败:${err.message}`, 'warn'); sendResponse({ error: err.message }); }); return true; } }); function normalizeText(value) { return String(value || '').replace(/\s+/g, ' ').trim(); } function sleep(ms) { return new Promise((resolve, reject) => { if (flowStopped) { reject(new Error(STOP_ERROR_MESSAGE)); return; } setTimeout(() => { if (flowStopped) { reject(new Error(STOP_ERROR_MESSAGE)); return; } resolve(); }, ms); }); } function isVisibleElement(element) { if (!element) return false; const style = window.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden') return false; const rect = element.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } function getRcmailEnv() { try { return window.rcmail?.env || null; } catch { return null; } } function isLoginPageLikely() { const url = location.href.toLowerCase(); if (/[_-]task=login|[?&]_action=login\b|\/login\b/.test(url)) { return true; } const passwordInput = document.querySelector('input[type="password"]'); const loginForm = document.querySelector('form[action*="login"], form[name*="login"], #login-form, .login-form'); const loginButton = Array.from(document.querySelectorAll('button, input[type="submit"], a')).find((element) => { const text = normalizeText( element?.textContent || element?.value || element?.getAttribute?.('title') || element?.getAttribute?.('aria-label') || '' ); return /登录|登入|sign in|log in/i.test(text); }); const mailboxList = document.querySelector('#mailboxlist'); const messageList = document.querySelector('#messagelist'); const hasMailUi = mailboxList || messageList || getRcmailEnv()?.task === 'mail'; if ((passwordInput || loginForm || loginButton) && !hasMailUi) { return true; } const pageText = normalizeText(document.body?.innerText || document.body?.textContent || ''); return /(企业邮箱登录|请输入密码|登录邮箱|sign in)/i.test(pageText) && !hasMailUi; } async function waitUntilLoggedIn(step) { let loggedWaitMessage = false; while (isLoginPageLikely()) { throwIfStopped(); if (!loggedWaitMessage && !waitingLoginLogged) { waitingLoginLogged = true; loggedWaitMessage = true; log(`步骤 ${step}:检测到 mail.phplife.net 未登录,正在等待你手动登录...`, 'warn'); } await sleep(1000); } if (loggedWaitMessage || waitingLoginLogged) { log(`步骤 ${step}:已检测到 mail.phplife.net 登录完成,继续读取验证码...`, 'ok'); } waitingLoginLogged = false; } function normalizeMinuteTimestamp(timestamp) { if (!Number.isFinite(timestamp) || timestamp <= 0) return 0; const date = new Date(timestamp); date.setSeconds(0, 0); return date.getTime(); } function parseRoundcubeTimestamp(rawText) { const text = normalizeText(rawText); if (!text) return null; let match = text.match(/今天\s*(\d{1,2}):(\d{2})/); if (match) { const now = new Date(); return new Date(now.getFullYear(), now.getMonth(), now.getDate(), Number(match[1]), Number(match[2]), 0, 0).getTime(); } match = text.match(/昨天\s*(\d{1,2}):(\d{2})/); if (match) { const now = new Date(); now.setDate(now.getDate() - 1); return new Date(now.getFullYear(), now.getMonth(), now.getDate(), Number(match[1]), Number(match[2]), 0, 0).getTime(); } match = text.match(/(\d{4})[-/年](\d{1,2})[-/月](\d{1,2})日?\s*(\d{1,2}):(\d{2})/); if (match) { return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]), 0, 0).getTime(); } match = text.match(/(\d{1,2})[-/](\d{1,2})\s*(\d{1,2}):(\d{2})/); if (match) { const now = new Date(); return new Date(now.getFullYear(), Number(match[1]) - 1, Number(match[2]), Number(match[3]), Number(match[4]), 0, 0).getTime(); } match = text.match(/^(\d{1,2}):(\d{2})$/); if (match) { const now = new Date(); return new Date(now.getFullYear(), now.getMonth(), now.getDate(), Number(match[1]), Number(match[2]), 0, 0).getTime(); } const parsed = Date.parse(text); return Number.isFinite(parsed) ? parsed : null; } function extractVerificationCodes(text) { const source = String(text || ''); const codes = []; const patterns = [ /(?:验证码|代码)[^0-9]{0,24}(\d{6})/ig, /(?:chatgpt\s+log-?in\s+code|your\s+chatgpt\s+code\s+is|verification\s+code|temporary\s+verification\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/ig, /\b(\d{6})\b/g, ]; for (const pattern of patterns) { let match = null; while ((match = pattern.exec(source))) { if (match[1] && !codes.includes(match[1])) { codes.push(match[1]); } } if (codes.length) { return codes; } } return codes; } function extractEmails(text) { const matches = String(text || '').match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig) || []; return [...new Set(matches.map((item) => item.toLowerCase()))]; } function collectPayloadStringFragments(value, bucket = []) { if (typeof value === 'string') { const normalized = value.trim(); if (normalized) { bucket.push(normalized); } return bucket; } if (Array.isArray(value)) { value.forEach((item) => collectPayloadStringFragments(item, bucket)); return bucket; } if (value && typeof value === 'object') { Object.values(value).forEach((item) => collectPayloadStringFragments(item, bucket)); } return bucket; } function extractRemoteExecText(rawText = '') { try { const parsed = JSON.parse(String(rawText || '')); const execText = String(parsed?.exec || '').trim(); return execText || String(rawText || ''); } catch { return String(rawText || ''); } } function readBalancedJsonObject(source = '', startIndex = 0) { if (String(source || '')[startIndex] !== '{') { return null; } let depth = 0; let inString = false; let escaped = false; for (let index = startIndex; index < source.length; index += 1) { const ch = source[index]; if (escaped) { escaped = false; continue; } if (ch === '\\') { escaped = true; continue; } if (ch === '"') { inString = !inString; continue; } if (inString) { continue; } if (ch === '{') depth += 1; if (ch === '}') { depth -= 1; if (depth === 0) { return { text: source.slice(startIndex, index + 1), endIndex: index + 1, }; } } } return null; } function parseRemoteMessageRowEntries(rawText = '') { const source = extractRemoteExecText(rawText); const entries = []; const marker = 'this.add_message_row('; let cursor = 0; while (cursor < source.length) { const start = source.indexOf(marker, cursor); if (start < 0) { break; } const argsStart = start + marker.length; const firstComma = source.indexOf(',', argsStart); if (firstComma < 0) { break; } const uid = String(source.slice(argsStart, firstComma).trim() || ''); const rowObjectStart = source.indexOf('{', firstComma); if (rowObjectStart < 0) { break; } const rowObject = readBalancedJsonObject(source, rowObjectStart); if (!rowObject) { break; } const metaObjectStart = source.indexOf('{', rowObject.endIndex); const metaObject = metaObjectStart >= 0 ? readBalancedJsonObject(source, metaObjectStart) : null; try { const rowData = JSON.parse(rowObject.text); const metaData = metaObject ? JSON.parse(metaObject.text) : {}; entries.push({ uid, rowData, metaData }); } catch { // Ignore malformed row and continue scanning. } cursor = metaObject?.endIndex || rowObject.endIndex; } return entries; } function extractRemotePayloadFragments(rawText = '') { const fragments = []; const addFragment = (value) => { const normalized = String(value || '').trim(); if (normalized) { fragments.push(normalized); } }; let parsedJson = null; try { parsedJson = JSON.parse(rawText); } catch { parsedJson = null; } if (parsedJson) { collectPayloadStringFragments(parsedJson.exec || parsedJson, fragments); } else { addFragment(rawText); } const expanded = []; fragments.forEach((fragment) => { const rowMatches = String(fragment).match(//gi); if (rowMatches?.length) { expanded.push(...rowMatches); } else { expanded.push(fragment); } }); return [...new Set(expanded.map((item) => String(item || '').trim()).filter(Boolean))]; } 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 emails = extractEmails(text); if (!emails.length) { return { matches: false, hasExplicitEmail: false }; } return { matches: emails.includes(normalizedTarget), hasExplicitEmail: true, }; } function normalizeFragmentText(fragment = '') { const source = String(fragment || ''); if (!source) return ''; if (!/[<>]/.test(source)) { return normalizeText(source); } try { const parser = new DOMParser(); const doc = parser.parseFromString(`${source}
`, 'text/html'); return normalizeText(doc.body?.textContent || doc.documentElement?.textContent || source); } catch { return normalizeText(source); } } function buildRemoteRowDetails(entry = {}) { const rowData = entry?.rowData || {}; const metaData = entry?.metaData || {}; const subject = normalizeText(rowData.subject || ''); const from = normalizeFragmentText(rowData.fromto || ''); const to = normalizeFragmentText(rowData.to || ''); const dateText = normalizeText(rowData.date || ''); const combinedText = normalizeText([subject, from, to, dateText].join(' ')); return { uid: String(entry?.uid || ''), subject, from, to, dateText, ctype: String(metaData?.ctype || ''), emailTimestamp: parseRoundcubeTimestamp(dateText), codes: extractVerificationCodes(combinedText), combinedText, }; } function findExactTargetRemoteRows(entries = [], targetEmail = '') { const normalizedTarget = String(targetEmail || '').trim().toLowerCase(); if (!normalizedTarget) { return []; } return entries.filter((details) => { const targetState = getTargetEmailMatchState(details?.combinedText || '', normalizedTarget); return targetState.hasExplicitEmail && targetState.matches; }); } function matchesMailFilters(text, senderFilters = [], subjectFilters = []) { const normalizedText = String(text || '').toLowerCase(); const senderMatched = senderFilters.some((filter) => normalizedText.includes(String(filter || '').toLowerCase())); const subjectMatched = subjectFilters.some((filter) => normalizedText.includes(String(filter || '').toLowerCase())); return senderMatched || subjectMatched; } function getPreviewDocument() { const frame = document.getElementById('messagecontframe'); if (!frame) return null; try { return frame.contentDocument || frame.contentWindow?.document || null; } catch { return null; } } function getMessageDocument() { if (document.querySelector('#messagebody, #messageheader, .headers-table')) { return document; } const previewDocument = getPreviewDocument(); if (previewDocument?.querySelector?.('#messagebody, #messageheader, .headers-table')) { return previewDocument; } return null; } function getMessageDetailsFromDocument(doc = null) { const sourceDocument = doc || getMessageDocument(); if (!sourceDocument) return null; const subject = normalizeText(sourceDocument.querySelector('h2.subject')?.textContent || ''); const from = normalizeText(sourceDocument.querySelector('.headers-table .header.from')?.textContent || ''); const to = normalizeText(sourceDocument.querySelector('.headers-table .header.to')?.textContent || ''); const dateText = normalizeText(sourceDocument.querySelector('.headers-table .header.date')?.textContent || ''); const bodyText = normalizeText( sourceDocument.querySelector('#messagebody')?.innerText || sourceDocument.querySelector('#messagebody')?.textContent || sourceDocument.body?.innerText || sourceDocument.body?.textContent || '' ); const combinedText = normalizeText([subject, from, to, dateText, bodyText].join(' ')); const codes = extractVerificationCodes(combinedText); return { subject, from, to, dateText, emailTimestamp: parseRoundcubeTimestamp(dateText), bodyText, combinedText, codes, }; } function getMessageListRows() { return Array.from(document.querySelectorAll('#messagelist tbody tr')).filter(isVisibleElement); } function getRowText(row, selector) { const node = row?.querySelector(selector); return normalizeText( node?.getAttribute?.('title') || node?.getAttribute?.('aria-label') || node?.textContent || '' ); } function getRowDetails(row) { const subject = getRowText(row, 'td.subject'); const from = getRowText(row, 'td.fromto'); const to = getRowText(row, 'td.to'); const dateText = getRowText(row, 'td.date'); const combinedText = normalizeText([subject, from, to, dateText, row?.textContent || ''].join(' ')); return { row, subject, from, to, dateText, emailTimestamp: parseRoundcubeTimestamp(dateText), codes: extractVerificationCodes(combinedText), combinedText, }; } function scoreRowCandidate(details, payload = {}) { const { senderFilters = [], subjectFilters = [], targetEmail = '' } = payload; let score = 0; const combinedText = details?.combinedText || ''; if (matchesMailFilters(combinedText, senderFilters, subjectFilters)) score += 4; if (/openai|chatgpt|verification|verify|验证码/i.test(combinedText)) score += 3; const targetMatch = getTargetEmailMatchState(combinedText, targetEmail); if (targetMatch.matches) score += targetMatch.hasExplicitEmail ? 4 : 1; if (details?.emailTimestamp) score += 1; return score; } function scoreRemotePayloadCandidate(text = '', payload = {}) { const targetState = getTargetEmailMatchState(text, payload.targetEmail); let score = 0; if (targetState.matches) score += targetState.hasExplicitEmail ? 10 : 2; if (matchesMailFilters(text, payload.senderFilters, payload.subjectFilters)) score += 5; if (/openai|chatgpt|verification|verify|验证码|登录|login/i.test(text)) score += 4; if (shouldOpenRowForCodeDetection({ subject: text })) score += 2; return { score, targetState }; } function buildRemoteMailApiUrl(action = 'list', options = {}) { const { targetEmail = '', unlockPrefix = 'loading', } = options; const requestAt = Date.now(); const url = new URL(`/?_task=mail&_action=${encodeURIComponent(action)}`, location.origin); url.searchParams.set('_mbox', 'INBOX'); url.searchParams.set('_remote', '1'); url.searchParams.set('_unlock', `${unlockPrefix}${requestAt}`); url.searchParams.set('_', String(requestAt)); if (action === 'list') { url.searchParams.set('_refresh', '1'); } if (action === 'search' && targetEmail) { url.searchParams.set('_filter', 'ALL'); url.searchParams.set('_interval', ''); url.searchParams.set('_q', targetEmail); url.searchParams.set('_headers', 'to'); url.searchParams.set('_scope', 'base'); } return url; } async function requestRemoteMailApi(action, options = {}) { const url = buildRemoteMailApiUrl(action, options); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), 15000); try { const response = await fetch(url.toString(), { credentials: 'same-origin', headers: { Accept: 'application/json, text/javascript, */*; q=0.01', 'X-Requested-With': 'XMLHttpRequest', }, cache: 'no-store', signal: controller.signal, }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.text(); } catch (error) { if (error?.name === 'AbortError') { throw new Error(`A4Sky ${action} 接口请求超时`); } throw new Error(`A4Sky ${action} 接口请求失败:${error?.message || error}`); } finally { clearTimeout(timeoutId); } } async function requestRemoteMessageList(step) { return requestRemoteMailApi('list', { unlockPrefix: 'loading' }); } async function requestRemoteMessageSearch(step, targetEmail = '') { if (!String(targetEmail || '').trim()) { return ''; } return requestRemoteMailApi('search', { targetEmail: String(targetEmail || '').trim(), unlockPrefix: 'loading', }); } function findVerificationCodeFromRemotePayload(rawText, payload, excludedCodeSet = new Set(), filterAfterMinute = 0) { const parsedRows = parseRemoteMessageRowEntries(rawText) .map((entry) => buildRemoteRowDetails(entry)); const exactTargetRows = findExactTargetRemoteRows(parsedRows, payload?.targetEmail); const candidateRows = exactTargetRows.length ? exactTargetRows : parsedRows; const rowCandidates = candidateRows .map((details) => { const match = matchesCurrentMessage(details, payload, excludedCodeSet, filterAfterMinute); return match.matched ? { code: match.code, emailTimestamp: match.emailTimestamp, score: scoreRowCandidate(details, payload), uid: Number(details.uid || 0) || 0, } : null; }) .filter(Boolean) .sort((left, right) => { if (left.score !== right.score) return right.score - left.score; if (left.emailTimestamp !== right.emailTimestamp) return right.emailTimestamp - left.emailTimestamp; return right.uid - left.uid; }); if (rowCandidates.length) { return rowCandidates[0]; } if (exactTargetRows.length) { return null; } const fragments = extractRemotePayloadFragments(rawText); const candidates = []; fragments.forEach((fragment) => { const text = normalizeFragmentText(fragment); if (!text) return; const { score, targetState } = scoreRemotePayloadCandidate(text, payload); if (payload?.targetEmail && targetState.hasExplicitEmail && !targetState.matches) { return; } if (score <= 0) return; const codes = extractVerificationCodes(text); const code = selectCandidateCode(codes, excludedCodeSet); if (!code) return; candidates.push({ code, score, text, targetMatched: targetState.matches, explicitTarget: targetState.hasExplicitEmail, }); }); candidates.sort((left, right) => { if (left.score !== right.score) return right.score - left.score; if (left.explicitTarget !== right.explicitTarget) return Number(right.explicitTarget) - Number(left.explicitTarget); if (left.targetMatched !== right.targetMatched) return Number(right.targetMatched) - Number(left.targetMatched); return left.text.length - right.text.length; }); return candidates[0] || null; } async function tryReadRemoteMessageList(step, payload, excludedCodeSet) { try { const filterAfterMinute = normalizeMinuteTimestamp(Number(payload?.filterAfterTimestamp) || 0); const targetEmail = String(payload?.targetEmail || '').trim().toLowerCase(); if (targetEmail) { const searchText = await requestRemoteMessageSearch(step, targetEmail); const searchResult = findVerificationCodeFromRemotePayload( searchText, payload, excludedCodeSet, filterAfterMinute ); if (searchResult?.code) { log(`步骤 ${step}:已直接从 mail.phplife.net 搜索接口命中验证码邮件。`, 'ok'); return { code: searchResult.code, emailTimestamp: Date.now(), }; } } const rawText = await requestRemoteMessageList(step); const result = findVerificationCodeFromRemotePayload( rawText, payload, excludedCodeSet, filterAfterMinute ); if (result?.code) { log(`步骤 ${step}:已直接从 mail.phplife.net 列表接口命中验证码邮件。`, 'ok'); return { code: result.code, emailTimestamp: Date.now(), }; } return null; } catch (error) { console.warn(PHPLIFE_MAIL_PREFIX, 'Remote list request failed:', error?.message || error); return null; } } function shouldOpenRowForCodeDetection(details = {}) { const subject = normalizeText(details?.subject || ''); if (!subject) { return false; } return /your\s+temporary\s+chatgpt\s+login\s+code|(?:你(?:的)?|您的)?\s*临时\s*chatgpt\s*登录代?码/i.test(subject); } function getCurrentMessageUid() { const envUid = getRcmailEnv()?.uid; if (envUid) return String(envUid); const doc = getMessageDocument(); const permaLink = doc?.querySelector?.('a[href*="_uid="]')?.getAttribute?.('href') || location.href; const match = String(permaLink || '').match(/[?&]_uid=(\d+)/); return match ? match[1] : ''; } async function waitForPreviewLoaded(previousUid = '', timeoutMs = 15000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { throwIfStopped(); const doc = getMessageDocument(); const details = getMessageDetailsFromDocument(doc); const currentUid = getCurrentMessageUid(); if (details?.combinedText && (!previousUid || !currentUid || currentUid !== previousUid)) { return details; } await sleep(250); } return getMessageDetailsFromDocument(); } function openMessageRow(row) { if (!row) { return; } if (typeof row.click === 'function') { row.click(); return; } row.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } function findRefreshButton() { const selectors = [ '#rcmbtn105', 'a.button.checkmail', 'a[title*="检查新邮件"]', 'a[title*="刷新"]', ]; for (const selector of selectors) { const node = document.querySelector(selector); if (node) return node; } return Array.from(document.querySelectorAll('a, button')).find((element) => /刷新|检查新邮件|check mail/i.test(normalizeText(element.textContent || element.getAttribute('title') || ''))) || null; } function findInboxLink() { return document.querySelector('#mailboxlist .mailbox.inbox a[rel="INBOX"], #mailboxlist a[rel="INBOX"]'); } async function ensureInboxActive(options = {}) { const { forceRefresh = false } = options; const inboxLink = findInboxLink(); if (!inboxLink) return; const inboxItem = inboxLink.closest('.mailbox'); if (!forceRefresh && inboxItem?.classList?.contains('selected')) { return; } inboxLink.click(); await sleep(800); } async function refreshMessageList() { await ensureInboxActive({ forceRefresh: true }); const refreshButton = findRefreshButton(); if (!refreshButton) return true; refreshButton.click(); await sleep(1200); return true; } function selectCandidateCode(codes = [], excludedCodeSet = new Set()) { for (const code of codes) { if (!excludedCodeSet.has(code) && !seenCodes.has(code)) { return code; } } return null; } function matchesCurrentMessage(details, payload = {}, excludedCodeSet = new Set(), filterAfterMinute = 0) { if (!details?.combinedText) { return { matched: false }; } const targetMatch = getTargetEmailMatchState(details.combinedText, payload.targetEmail); if (targetMatch.hasExplicitEmail && !targetMatch.matches) { return { matched: false }; } if (!matchesMailFilters(details.combinedText, payload.senderFilters, payload.subjectFilters)) { return { matched: false }; } const normalizedTimestamp = normalizeMinuteTimestamp(details.emailTimestamp || 0); if (filterAfterMinute && normalizedTimestamp && normalizedTimestamp < filterAfterMinute) { return { matched: false }; } const code = selectCandidateCode(details.codes, excludedCodeSet); if (!code) { return { matched: false }; } return { matched: true, code, emailTimestamp: details.emailTimestamp || Date.now(), }; } async function tryReadCurrentMessage(payload, excludedCodeSet, filterAfterMinute) { const details = getMessageDetailsFromDocument(); const result = matchesCurrentMessage(details, payload, excludedCodeSet, filterAfterMinute); return result.matched ? result : null; } async function tryOpenRowsAndRead(step, payload, excludedCodeSet, filterAfterMinute) { const rows = getMessageListRows() .map((row) => getRowDetails(row)) .map((details) => ({ ...details, score: scoreRowCandidate(details, payload) })) .filter((details) => details.score > 0) .sort((left, right) => right.score - left.score); for (const details of rows.slice(0, 8)) { throwIfStopped(); const rowMinuteTimestamp = normalizeMinuteTimestamp(details.emailTimestamp || 0); if (filterAfterMinute && rowMinuteTimestamp && rowMinuteTimestamp < filterAfterMinute) { continue; } const directResult = matchesCurrentMessage(details, payload, excludedCodeSet, filterAfterMinute); const shouldOpenDetail = shouldOpenRowForCodeDetection(details); if (directResult.matched && !shouldOpenDetail) { log(`步骤 ${step}:已直接从 mail.phplife.net 列表命中验证码邮件。`, 'ok'); return directResult; } if (!shouldOpenDetail) { continue; } log(`步骤 ${step}:检测到临时登录验证码邮件标题,正在打开详情读取验证码...`, 'info'); const previousUid = getCurrentMessageUid(); openMessageRow(details.row); await sleep(500); const openedDetails = await waitForPreviewLoaded(previousUid); const detailResult = matchesCurrentMessage(openedDetails, payload, excludedCodeSet, filterAfterMinute); if (detailResult.matched) { log(`步骤 ${step}:已在 mail.phplife.net 邮件详情中命中验证码。`, 'ok'); return detailResult; } if (directResult.matched) { log(`步骤 ${step}:邮件详情未提取到验证码,已回退使用列表中的匹配结果。`, 'warn'); return directResult; } } return null; } async function handlePollEmail(step, payload) { const { maxAttempts = 5, intervalMs = 3000, excludeCodes = [], filterAfterTimestamp = 0, } = payload || {}; const excludedCodeSet = new Set((excludeCodes || []).filter(Boolean)); const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0); await waitUntilLoggedIn(step); await ensureInboxActive(); log(`步骤 ${step}:开始轮询 A4Sky 邮箱(最多 ${maxAttempts} 次)`); if (filterAfterMinute) { log(`步骤 ${step}:仅尝试 ${new Date(filterAfterMinute).toLocaleString('zh-CN', { hour12: false })} 及之后时间的邮件。`); } let lastError = null; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { throwIfStopped(); await waitUntilLoggedIn(step); log(`步骤 ${step}:正在检查 mail.phplife.net 邮件(${attempt}/${maxAttempts})...`); await refreshMessageList(); const remoteListResult = await tryReadRemoteMessageList(step, payload, excludedCodeSet); if (remoteListResult?.code) { seenCodes.add(remoteListResult.code); await persistSeenCodes(); return remoteListResult; } const currentMessageResult = await tryReadCurrentMessage(payload, excludedCodeSet, filterAfterMinute); if (currentMessageResult?.matched) { seenCodes.add(currentMessageResult.code); await persistSeenCodes(); return { code: currentMessageResult.code, emailTimestamp: currentMessageResult.emailTimestamp, }; } const openedRowResult = await tryOpenRowsAndRead(step, payload, excludedCodeSet, filterAfterMinute); if (openedRowResult?.matched) { seenCodes.add(openedRowResult.code); await persistSeenCodes(); return { code: openedRowResult.code, emailTimestamp: openedRowResult.emailTimestamp, }; } lastError = new Error(`步骤 ${step}:暂未在 A4Sky 邮箱中找到新的匹配验证码(${attempt}/${maxAttempts})。`); if (attempt < maxAttempts) { await sleep(intervalMs); } } throw lastError || new Error(`步骤 ${step}:未在 A4Sky 邮箱中找到新的匹配验证码。`); } }