// 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 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 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 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() { const inboxLink = findInboxLink(); if (!inboxLink) return; const inboxItem = inboxLink.closest('.mailbox'); if (inboxItem?.classList?.contains('selected')) { return; } inboxLink.click(); await sleep(800); } async function refreshMessageList() { const refreshButton = findRefreshButton(); if (!refreshButton) return false; 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); if (directResult.matched) { log(`步骤 ${step}:已直接从 mail.phplife.net 列表命中验证码邮件。`, 'ok'); 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 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 邮箱中找到新的匹配验证码。`); } }