|
@@ -0,0 +1,545 @@
|
|
|
|
|
+// 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 邮箱中找到新的匹配验证码。`);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+}
|