| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545 |
- // 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 邮箱中找到新的匹配验证码。`);
- }
- }
|