|
|
@@ -210,6 +210,162 @@ function extractEmails(text) {
|
|
|
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(/<tr\b[\s\S]*?<\/tr>/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) {
|
|
|
@@ -232,6 +388,56 @@ function getTargetEmailMatchState(text, targetEmail) {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+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(`<table><tbody>${source}</tbody></table>`, '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()));
|
|
|
@@ -338,6 +544,201 @@ function scoreRowCandidate(details, payload = {}) {
|
|
|
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) {
|
|
|
@@ -545,6 +946,13 @@ async function handlePollEmail(step, payload) {
|
|
|
|
|
|
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);
|