fix(mail163): read body when preview lacks login code
This commit is contained in:
+350
-31
@@ -69,15 +69,141 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
// Find mail items
|
// Find mail items
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function findMailItems() {
|
function normalizeText(value) {
|
||||||
return document.querySelectorAll('div[sign="letter"]');
|
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentMailIds() {
|
function isVisibleNode(node) {
|
||||||
|
if (!node) return false;
|
||||||
|
if (node.hidden) return false;
|
||||||
|
|
||||||
|
const style = typeof window.getComputedStyle === 'function'
|
||||||
|
? window.getComputedStyle(node)
|
||||||
|
: null;
|
||||||
|
if (style && (style.display === 'none' || style.visibility === 'hidden')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = typeof node.getBoundingClientRect === 'function'
|
||||||
|
? node.getBoundingClientRect()
|
||||||
|
: null;
|
||||||
|
if (rect && rect.width <= 0 && rect.height <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLikelyMailItemNode(node) {
|
||||||
|
if (!isVisibleNode(node)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (node.matches?.('div[sign="letter"]')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (node.querySelector?.('.nui-user, span.da0, [sign="trash"], [title="删除邮件"], [class*="subject"], [class*="sender"]')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const summaryText = normalizeText(
|
||||||
|
node.getAttribute?.('aria-label')
|
||||||
|
|| node.getAttribute?.('title')
|
||||||
|
|| node.textContent
|
||||||
|
);
|
||||||
|
if (!summaryText) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return /发件人|验证码|verification|chatgpt|openai|code/i.test(summaryText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMailItems() {
|
||||||
|
const selectorGroups = [
|
||||||
|
'div[sign="letter"]',
|
||||||
|
'[role="option"][aria-label]',
|
||||||
|
'[role="listitem"][aria-label]',
|
||||||
|
'tr[aria-label]',
|
||||||
|
'li[aria-label]',
|
||||||
|
'div[aria-label]',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const selector of selectorGroups) {
|
||||||
|
const matches = Array.from(document.querySelectorAll(selector)).filter(isLikelyMailItemNode);
|
||||||
|
if (matches.length > 0) {
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailTextBySelectors(item, selectors = []) {
|
||||||
|
for (const selector of selectors) {
|
||||||
|
const candidates = item.querySelectorAll(selector);
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const texts = [
|
||||||
|
candidate.getAttribute?.('title'),
|
||||||
|
candidate.getAttribute?.('aria-label'),
|
||||||
|
candidate.textContent,
|
||||||
|
];
|
||||||
|
for (const text of texts) {
|
||||||
|
const normalized = normalizeText(text);
|
||||||
|
if (normalized) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailSenderText(item) {
|
||||||
|
return getMailTextBySelectors(item, [
|
||||||
|
'.nui-user',
|
||||||
|
'[class*="sender"]',
|
||||||
|
'[class*="from"]',
|
||||||
|
'[data-sender]',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailSubjectText(item) {
|
||||||
|
return getMailTextBySelectors(item, [
|
||||||
|
'span.da0',
|
||||||
|
'[class*="subject"]',
|
||||||
|
'[data-subject]',
|
||||||
|
'strong',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailRowText(item) {
|
||||||
|
const ariaLabel = normalizeText(item.getAttribute('aria-label'));
|
||||||
|
const sender = getMailSenderText(item);
|
||||||
|
const subject = getMailSubjectText(item);
|
||||||
|
const fullText = normalizeText(item.textContent || '');
|
||||||
|
return normalizeText([ariaLabel, sender, subject, fullText].filter(Boolean).join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailItemId(item, index = 0) {
|
||||||
|
const candidates = [
|
||||||
|
item.getAttribute('id'),
|
||||||
|
item.getAttribute('data-id'),
|
||||||
|
item.dataset?.id,
|
||||||
|
item.getAttribute('data-key'),
|
||||||
|
item.getAttribute('key'),
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
if (candidates.length > 0) {
|
||||||
|
return String(candidates[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${index}|${getMailRowText(item).slice(0, 240)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentMailIds(items = []) {
|
||||||
const ids = new Set();
|
const ids = new Set();
|
||||||
findMailItems().forEach(item => {
|
const sourceItems = items.length > 0 ? items : findMailItems();
|
||||||
const id = item.getAttribute('id') || '';
|
sourceItems.forEach((item, index) => {
|
||||||
if (id) ids.add(id);
|
ids.add(getMailItemId(item, index));
|
||||||
});
|
});
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
@@ -90,7 +216,7 @@ function normalizeMinuteTimestamp(timestamp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseMail163Timestamp(rawText) {
|
function parseMail163Timestamp(rawText) {
|
||||||
const text = (rawText || '').replace(/\s+/g, ' ').trim();
|
const text = normalizeText(rawText);
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
|
|
||||||
let match = text.match(/(\d{4})年(\d{1,2})月(\d{1,2})日\s+(\d{1,2}):(\d{2})/);
|
let match = text.match(/(\d{4})年(\d{1,2})月(\d{1,2})日\s+(\d{1,2}):(\d{2})/);
|
||||||
@@ -107,6 +233,37 @@ function parseMail163Timestamp(rawText) {
|
|||||||
).getTime();
|
).getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match = text.match(/今天\s*(\d{1,2}):(\d{2})/);
|
||||||
|
if (match) {
|
||||||
|
const [, hour, minute] = match;
|
||||||
|
const now = new Date();
|
||||||
|
return new Date(
|
||||||
|
now.getFullYear(),
|
||||||
|
now.getMonth(),
|
||||||
|
now.getDate(),
|
||||||
|
Number(hour),
|
||||||
|
Number(minute),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
match = text.match(/昨天\s*(\d{1,2}):(\d{2})/);
|
||||||
|
if (match) {
|
||||||
|
const [, hour, minute] = match;
|
||||||
|
const now = new Date();
|
||||||
|
now.setDate(now.getDate() - 1);
|
||||||
|
return new Date(
|
||||||
|
now.getFullYear(),
|
||||||
|
now.getMonth(),
|
||||||
|
now.getDate(),
|
||||||
|
Number(hour),
|
||||||
|
Number(minute),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
match = text.match(/\b(\d{1,2}):(\d{2})\b/);
|
match = text.match(/\b(\d{1,2}):(\d{2})\b/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const [, hour, minute] = match;
|
const [, hour, minute] = match;
|
||||||
@@ -125,18 +282,48 @@ function parseMail163Timestamp(rawText) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMailTimestamp(item) {
|
function isLikelyMailTimestampText(value) {
|
||||||
const candidates = [];
|
const text = normalizeText(value);
|
||||||
const timeCell = item.querySelector('.e00[title], [title*="年"][title*=":"]');
|
if (!text) {
|
||||||
if (timeCell?.getAttribute('title')) candidates.push(timeCell.getAttribute('title'));
|
return false;
|
||||||
if (timeCell?.textContent) candidates.push(timeCell.textContent);
|
}
|
||||||
|
return /(\d{4}年\d{1,2}月\d{1,2}日\s+\d{1,2}:\d{2})|今天\s*\d{1,2}:\d{2}|昨天\s*\d{1,2}:\d{2}|\b\d{1,2}:\d{2}\b/.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
const titledNodes = item.querySelectorAll('[title]');
|
function collectMailTimestampCandidates(item) {
|
||||||
titledNodes.forEach((node) => {
|
const candidates = [];
|
||||||
const title = node.getAttribute('title');
|
const seen = new Set();
|
||||||
if (title) candidates.push(title);
|
const pushCandidate = (value) => {
|
||||||
|
const normalized = normalizeText(value);
|
||||||
|
if (!normalized || !isLikelyMailTimestampText(normalized) || seen.has(normalized)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seen.add(normalized);
|
||||||
|
candidates.push(normalized);
|
||||||
|
};
|
||||||
|
|
||||||
|
const priorityNodes = item.querySelectorAll('.e00, [title], [aria-label], time, [class*="time"], [class*="date"]');
|
||||||
|
priorityNodes.forEach((node) => {
|
||||||
|
pushCandidate(node.getAttribute?.('title'));
|
||||||
|
pushCandidate(node.getAttribute?.('aria-label'));
|
||||||
|
pushCandidate(node.textContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const textNodes = item.querySelectorAll('span, div, td, strong, b');
|
||||||
|
textNodes.forEach((node) => {
|
||||||
|
const text = normalizeText(node.textContent);
|
||||||
|
if (text && text.length <= 24) {
|
||||||
|
pushCandidate(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pushCandidate(item.getAttribute('aria-label'));
|
||||||
|
pushCandidate(item.getAttribute('title'));
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailTimestamp(item) {
|
||||||
|
const candidates = collectMailTimestampCandidates(item);
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
const parsed = parseMail163Timestamp(candidate);
|
const parsed = parseMail163Timestamp(candidate);
|
||||||
if (parsed) return parsed;
|
if (parsed) return parsed;
|
||||||
@@ -145,6 +332,118 @@ function getMailTimestamp(item) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectOpenedMailTextCandidates() {
|
||||||
|
const texts = [];
|
||||||
|
const seen = new Set();
|
||||||
|
const pushText = (value) => {
|
||||||
|
const normalized = normalizeText(value);
|
||||||
|
if (!normalized || seen.has(normalized)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seen.add(normalized);
|
||||||
|
texts.push(normalized);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectors = [
|
||||||
|
'.readHtml',
|
||||||
|
'[class*="readmail"]',
|
||||||
|
'[class*="mailread"]',
|
||||||
|
'[class*="mailBody"]',
|
||||||
|
'[class*="mailbody"]',
|
||||||
|
'[class*="mail-content"]',
|
||||||
|
'[class*="mailContent"]',
|
||||||
|
'[class*="mail-detail"]',
|
||||||
|
'[class*="mailDetail"]',
|
||||||
|
'[class*="detail"] [class*="content"]',
|
||||||
|
'[class*="read"] [class*="content"]',
|
||||||
|
'[role="main"]',
|
||||||
|
];
|
||||||
|
|
||||||
|
selectors.forEach((selector) => {
|
||||||
|
document.querySelectorAll(selector).forEach((node) => {
|
||||||
|
pushText(node.innerText || node.textContent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('iframe').forEach((frame) => {
|
||||||
|
try {
|
||||||
|
pushText(frame.contentDocument?.body?.innerText || frame.contentDocument?.body?.textContent);
|
||||||
|
} catch {
|
||||||
|
// Ignore cross-frame access errors and keep trying other candidates.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pushText(document.body?.innerText || document.body?.textContent);
|
||||||
|
return texts.sort((a, b) => b.length - a.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOpenedMailText(item) {
|
||||||
|
const subject = normalizeText(getMailSubjectText(item)).toLowerCase();
|
||||||
|
const sender = normalizeText(getMailSenderText(item)).toLowerCase();
|
||||||
|
const candidates = collectOpenedMailTextCandidates();
|
||||||
|
|
||||||
|
const preferred = candidates.find((candidate) => {
|
||||||
|
const lower = candidate.toLowerCase();
|
||||||
|
if (subject && lower.includes(subject)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (sender && lower.includes(sender)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Boolean(extractVerificationCode(candidate) && /chatgpt|openai|verification|验证码|login code/i.test(lower));
|
||||||
|
});
|
||||||
|
|
||||||
|
return preferred || candidates[0] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function returnToInbox() {
|
||||||
|
const inboxLink = document.querySelector('.nui-tree-item-text[title="收件箱"], [title="收件箱"]');
|
||||||
|
if (inboxLink) {
|
||||||
|
if (typeof simulateClick === 'function') {
|
||||||
|
simulateClick(inboxLink);
|
||||||
|
} else {
|
||||||
|
inboxLink.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 20; i += 1) {
|
||||||
|
if (findMailItems().length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await sleep(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openMailAndGetMessageText(item) {
|
||||||
|
const beforeText = readOpenedMailText(item);
|
||||||
|
if (typeof simulateClick === 'function') {
|
||||||
|
simulateClick(item);
|
||||||
|
} else {
|
||||||
|
item.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
let openedText = '';
|
||||||
|
for (let i = 0; i < 24; i += 1) {
|
||||||
|
await sleep(250);
|
||||||
|
const candidate = readOpenedMailText(item);
|
||||||
|
if (!candidate) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
openedText = candidate;
|
||||||
|
if (extractVerificationCode(candidate)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (candidate !== beforeText && candidate.length > beforeText.length + 24) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await returnToInbox();
|
||||||
|
return openedText;
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleEmailCleanup(item, step) {
|
function scheduleEmailCleanup(item, step) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Promise.resolve(deleteEmail(item, step)).catch(() => {
|
Promise.resolve(deleteEmail(item, step)).catch(() => {
|
||||||
@@ -199,7 +498,7 @@ async function handlePollEmail(step, payload) {
|
|||||||
log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`);
|
log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`);
|
||||||
|
|
||||||
// Snapshot existing mail IDs
|
// Snapshot existing mail IDs
|
||||||
const existingMailIds = getCurrentMailIds();
|
const existingMailIds = getCurrentMailIds(items);
|
||||||
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`);
|
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`);
|
||||||
|
|
||||||
const FALLBACK_AFTER = 3;
|
const FALLBACK_AFTER = 3;
|
||||||
@@ -215,38 +514,58 @@ async function handlePollEmail(step, payload) {
|
|||||||
const allItems = findMailItems();
|
const allItems = findMailItems();
|
||||||
const useFallback = attempt > FALLBACK_AFTER;
|
const useFallback = attempt > FALLBACK_AFTER;
|
||||||
|
|
||||||
for (const item of allItems) {
|
for (let index = 0; index < allItems.length; index++) {
|
||||||
const id = item.getAttribute('id') || '';
|
const item = allItems[index];
|
||||||
|
const id = getMailItemId(item, index);
|
||||||
const mailTimestamp = getMailTimestamp(item);
|
const mailTimestamp = getMailTimestamp(item);
|
||||||
const mailMinute = normalizeMinuteTimestamp(mailTimestamp || 0);
|
const mailMinute = normalizeMinuteTimestamp(mailTimestamp || 0);
|
||||||
const passesTimeFilter = !filterAfterMinute || (mailMinute && mailMinute >= filterAfterMinute);
|
const passesTimeFilter = !filterAfterMinute || (mailMinute && mailMinute >= filterAfterMinute);
|
||||||
const shouldBypassOldSnapshot = Boolean(filterAfterMinute && passesTimeFilter && mailMinute > 0);
|
|
||||||
|
|
||||||
if (!passesTimeFilter) {
|
if (!passesTimeFilter) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!useFallback && !shouldBypassOldSnapshot && existingMailIds.has(id)) continue;
|
if (!useFallback && existingMailIds.has(id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const senderEl = item.querySelector('.nui-user');
|
const sender = getMailSenderText(item).toLowerCase();
|
||||||
const sender = senderEl ? senderEl.textContent.toLowerCase() : '';
|
const subject = getMailSubjectText(item);
|
||||||
|
const rowText = getMailRowText(item);
|
||||||
|
const ariaLabel = normalizeText(item.getAttribute('aria-label')).toLowerCase();
|
||||||
|
const combinedText = normalizeText([subject, ariaLabel, rowText].filter(Boolean).join(' '));
|
||||||
|
|
||||||
const subjectEl = item.querySelector('span.da0');
|
if (!mailTimestamp) {
|
||||||
const subject = subjectEl ? subjectEl.textContent : '';
|
log(`步骤 ${step}:邮件 ${id.slice(0, 60)} 未读取到时间,已跳过时间窗口校验后的文本匹配阶段。`, 'info');
|
||||||
|
}
|
||||||
|
|
||||||
const ariaLabel = (item.getAttribute('aria-label') || '').toLowerCase();
|
const senderMatch = senderFilters.some((filter) => {
|
||||||
|
const normalizedFilter = String(filter || '').toLowerCase();
|
||||||
const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase()));
|
return normalizedFilter && (sender.includes(normalizedFilter) || ariaLabel.includes(normalizedFilter) || rowText.toLowerCase().includes(normalizedFilter));
|
||||||
const subjectMatch = subjectFilters.some(f => subject.toLowerCase().includes(f.toLowerCase()) || ariaLabel.includes(f.toLowerCase()));
|
});
|
||||||
|
const subjectMatch = subjectFilters.some((filter) => {
|
||||||
|
const normalizedFilter = String(filter || '').toLowerCase();
|
||||||
|
return normalizedFilter && (subject.toLowerCase().includes(normalizedFilter) || ariaLabel.includes(normalizedFilter) || rowText.toLowerCase().includes(normalizedFilter));
|
||||||
|
});
|
||||||
|
|
||||||
if (senderMatch || subjectMatch) {
|
if (senderMatch || subjectMatch) {
|
||||||
const code = extractVerificationCode(subject + ' ' + ariaLabel);
|
let code = extractVerificationCode(combinedText);
|
||||||
|
let codeSource = '邮件列表';
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
const openedText = await openMailAndGetMessageText(item);
|
||||||
|
code = extractVerificationCode(openedText);
|
||||||
|
if (code) {
|
||||||
|
codeSource = '邮件正文';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (code && excludedCodeSet.has(code)) {
|
if (code && excludedCodeSet.has(code)) {
|
||||||
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
|
||||||
} else if (code && !seenCodes.has(code)) {
|
} else if (code && !seenCodes.has(code)) {
|
||||||
seenCodes.add(code);
|
seenCodes.add(code);
|
||||||
persistSeenCodes();
|
persistSeenCodes();
|
||||||
const source = useFallback && existingMailIds.has(id) ? '回退匹配邮件' : '新邮件';
|
const source = useFallback && existingMailIds.has(id) ? `回退匹配${codeSource}` : `新邮件${codeSource}`;
|
||||||
const timeLabel = mailTimestamp ? `,时间:${new Date(mailTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
|
const timeLabel = mailTimestamp ? `,时间:${new Date(mailTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
|
||||||
log(`步骤 ${step}:已找到验证码:${code}(来源:${source}${timeLabel},主题:${subject.slice(0, 40)})`, 'ok');
|
log(`步骤 ${step}:已找到验证码:${code}(来源:${source}${timeLabel},主题:${subject.slice(0, 40)})`, 'ok');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,585 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const source = fs.readFileSync('content/mail-163.js', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||||
|
const start = markers
|
||||||
|
.map((marker) => source.indexOf(marker))
|
||||||
|
.find((index) => index >= 0);
|
||||||
|
if (start < 0) {
|
||||||
|
throw new Error(`missing function ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parenDepth = 0;
|
||||||
|
let signatureEnded = false;
|
||||||
|
let braceStart = -1;
|
||||||
|
for (let i = start; i < source.length; i += 1) {
|
||||||
|
const ch = source[i];
|
||||||
|
if (ch === '(') {
|
||||||
|
parenDepth += 1;
|
||||||
|
} else if (ch === ')') {
|
||||||
|
parenDepth -= 1;
|
||||||
|
if (parenDepth === 0) {
|
||||||
|
signatureEnded = true;
|
||||||
|
}
|
||||||
|
} else if (ch === '{' && signatureEnded) {
|
||||||
|
braceStart = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (braceStart < 0) {
|
||||||
|
throw new Error(`missing body for function ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
let end = braceStart;
|
||||||
|
for (; end < source.length; end += 1) {
|
||||||
|
const ch = source[end];
|
||||||
|
if (ch === '{') depth += 1;
|
||||||
|
if (ch === '}') {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
end += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return source.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('findMailItems falls back to visible aria-label mail rows when legacy selector is missing', () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('normalizeText'),
|
||||||
|
extractFunction('isVisibleNode'),
|
||||||
|
extractFunction('isLikelyMailItemNode'),
|
||||||
|
extractFunction('findMailItems'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
const mailRow = {
|
||||||
|
hidden: false,
|
||||||
|
textContent: 'Your temporary ChatGPT verification code 911113',
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'aria-label') return 'Your temporary ChatGPT verification code 911113 发件人 OpenAI';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
getBoundingClientRect() {
|
||||||
|
return { width: 600, height: 48 };
|
||||||
|
},
|
||||||
|
matches() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
querySelector() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const document = {
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
if (selector === 'div[sign="letter"]') return [];
|
||||||
|
if (selector === '[role="option"][aria-label]') return [mailRow];
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const window = {
|
||||||
|
getComputedStyle() {
|
||||||
|
return { display: 'block', visibility: 'visible' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
${bundle}
|
||||||
|
|
||||||
|
return { findMailItems };
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const rows = api.findMailItems();
|
||||||
|
assert.equal(rows.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getMailTimestamp parses visible hh:mm text even when no title attribute exists', () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('normalizeText'),
|
||||||
|
extractFunction('parseMail163Timestamp'),
|
||||||
|
extractFunction('isLikelyMailTimestampText'),
|
||||||
|
extractFunction('collectMailTimestampCandidates'),
|
||||||
|
extractFunction('getMailTimestamp'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const timestamp = new Function(`
|
||||||
|
const item = {
|
||||||
|
getAttribute() {
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
if (selector === '.e00, [title], [aria-label], time, [class*="time"], [class*="date"]') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (selector === 'span, div, td, strong, b') {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
textContent: '22:22',
|
||||||
|
getAttribute() {
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
${bundle}
|
||||||
|
|
||||||
|
return getMailTimestamp(item);
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const expected = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 22, 22, 0, 0).getTime();
|
||||||
|
assert.equal(timestamp, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('readOpenedMailText prefers opened body content that contains the verification code', () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('normalizeText'),
|
||||||
|
extractFunction('collectOpenedMailTextCandidates'),
|
||||||
|
extractFunction('readOpenedMailText'),
|
||||||
|
extractFunction('extractVerificationCode'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const text = new Function(`
|
||||||
|
const item = {
|
||||||
|
querySelectorAll() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getMailSubjectText() {
|
||||||
|
return 'Your temporary ChatGPT login code';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailSenderText() {
|
||||||
|
return 'OpenAI';
|
||||||
|
}
|
||||||
|
|
||||||
|
const document = {
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
if (selector === 'iframe') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ innerText: 'Your temporary ChatGPT login code', textContent: 'Your temporary ChatGPT login code' },
|
||||||
|
{ innerText: 'Enter this temporary verification code to continue: 214203', textContent: 'Enter this temporary verification code to continue: 214203' },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
innerText: 'fallback body',
|
||||||
|
textContent: 'fallback body',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
${bundle}
|
||||||
|
|
||||||
|
return readOpenedMailText(item);
|
||||||
|
`)();
|
||||||
|
|
||||||
|
assert.match(text, /214203/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('openMailAndGetMessageText reads opened body text and returns to inbox', async () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('normalizeText'),
|
||||||
|
extractFunction('collectOpenedMailTextCandidates'),
|
||||||
|
extractFunction('readOpenedMailText'),
|
||||||
|
extractFunction('returnToInbox'),
|
||||||
|
extractFunction('openMailAndGetMessageText'),
|
||||||
|
extractFunction('extractVerificationCode'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
let inInbox = true;
|
||||||
|
let clickCount = 0;
|
||||||
|
const mailItem = {
|
||||||
|
click() {
|
||||||
|
clickCount += 1;
|
||||||
|
inInbox = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const inboxLink = {
|
||||||
|
click() {
|
||||||
|
inInbox = true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getMailSubjectText() {
|
||||||
|
return 'Your temporary ChatGPT login code';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailSenderText() {
|
||||||
|
return 'OpenAI';
|
||||||
|
}
|
||||||
|
|
||||||
|
const document = {
|
||||||
|
querySelector(selector) {
|
||||||
|
if (selector === '.nui-tree-item-text[title="收件箱"], [title="收件箱"]') return inboxLink;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
if (selector === 'iframe') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return inInbox ? [] : [{ innerText: 'Enter this temporary verification code to continue: 214203', textContent: 'Enter this temporary verification code to continue: 214203' }];
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
innerText: '',
|
||||||
|
textContent: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function findMailItems() {
|
||||||
|
return inInbox ? [mailItem] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sleep() {}
|
||||||
|
|
||||||
|
${bundle}
|
||||||
|
|
||||||
|
return {
|
||||||
|
openMailAndGetMessageText,
|
||||||
|
getClickCount: () => clickCount,
|
||||||
|
isInInbox: () => inInbox,
|
||||||
|
mailItem,
|
||||||
|
};
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const text = await api.openMailAndGetMessageText(api.mailItem);
|
||||||
|
assert.match(text, /214203/);
|
||||||
|
assert.equal(api.getClickCount(), 1);
|
||||||
|
assert.equal(api.isInInbox(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handlePollEmail ignores same-minute old snapshot mail before fallback', async () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('normalizeText'),
|
||||||
|
extractFunction('normalizeMinuteTimestamp'),
|
||||||
|
extractFunction('handlePollEmail'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
let currentItems = [{ id: 'old-mail' }];
|
||||||
|
const seenCodes = new Set();
|
||||||
|
|
||||||
|
function findMailItems() {
|
||||||
|
return currentItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentMailIds(items = []) {
|
||||||
|
return new Set((items.length ? items : currentItems).map((item) => item.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailItemId(item) {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailTimestamp() {
|
||||||
|
return new Date(2026, 3, 22, 22, 22, 0, 0).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailSenderText() {
|
||||||
|
return 'OpenAI';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailSubjectText() {
|
||||||
|
return 'Your temporary ChatGPT verification code';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailRowText() {
|
||||||
|
return 'Your temporary ChatGPT verification code 911113 发件人 OpenAI';
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractVerificationCode() {
|
||||||
|
return '911113';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForElement() {
|
||||||
|
return { click() {} };
|
||||||
|
}
|
||||||
|
async function refreshInbox() {}
|
||||||
|
async function sleep() {}
|
||||||
|
function log() {}
|
||||||
|
function persistSeenCodes() {}
|
||||||
|
function scheduleEmailCleanup() {}
|
||||||
|
|
||||||
|
${bundle}
|
||||||
|
|
||||||
|
return { handlePollEmail };
|
||||||
|
`)();
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => api.handlePollEmail(4, {
|
||||||
|
senderFilters: ['openai'],
|
||||||
|
subjectFilters: ['verification'],
|
||||||
|
maxAttempts: 1,
|
||||||
|
intervalMs: 1,
|
||||||
|
filterAfterTimestamp: new Date(2026, 3, 22, 22, 22, 40, 0).getTime(),
|
||||||
|
}),
|
||||||
|
/未在 163 邮箱中找到新的匹配邮件/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handlePollEmail accepts a new same-minute mail that appears after the snapshot', async () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('normalizeText'),
|
||||||
|
extractFunction('normalizeMinuteTimestamp'),
|
||||||
|
extractFunction('handlePollEmail'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
const oldMail = {
|
||||||
|
id: 'old-mail',
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'aria-label') return 'Old verification mail 111111 发件人 OpenAI';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const newMail = {
|
||||||
|
id: 'new-mail',
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'aria-label') return 'Your temporary ChatGPT verification code 654321 发件人 OpenAI';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let refreshCount = 0;
|
||||||
|
let currentItems = [oldMail];
|
||||||
|
const seenCodes = new Set();
|
||||||
|
|
||||||
|
function findMailItems() {
|
||||||
|
return currentItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentMailIds(items = []) {
|
||||||
|
return new Set((items.length ? items : currentItems).map((item) => item.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailItemId(item) {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailTimestamp() {
|
||||||
|
return new Date(2026, 3, 22, 22, 22, 0, 0).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailSenderText() {
|
||||||
|
return 'OpenAI';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailSubjectText(item) {
|
||||||
|
return item.id === 'new-mail' ? 'Your temporary ChatGPT verification code' : 'Old verification mail';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailRowText(item) {
|
||||||
|
return item.id === 'new-mail'
|
||||||
|
? 'Your temporary ChatGPT verification code 654321 发件人 OpenAI'
|
||||||
|
: 'Old verification mail 111111 发件人 OpenAI';
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractVerificationCode(text) {
|
||||||
|
const match = String(text || '').match(/(\\d{6})/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForElement() {
|
||||||
|
return { click() {} };
|
||||||
|
}
|
||||||
|
async function refreshInbox() {
|
||||||
|
refreshCount += 1;
|
||||||
|
if (refreshCount >= 1) {
|
||||||
|
currentItems = [oldMail, newMail];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function sleep() {}
|
||||||
|
function log() {}
|
||||||
|
function persistSeenCodes() {}
|
||||||
|
function scheduleEmailCleanup() {}
|
||||||
|
|
||||||
|
${bundle}
|
||||||
|
|
||||||
|
return { handlePollEmail };
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const result = await api.handlePollEmail(4, {
|
||||||
|
senderFilters: ['openai'],
|
||||||
|
subjectFilters: ['verification'],
|
||||||
|
maxAttempts: 2,
|
||||||
|
intervalMs: 1,
|
||||||
|
filterAfterTimestamp: new Date(2026, 3, 22, 22, 22, 40, 0).getTime(),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.code, '654321');
|
||||||
|
assert.equal(result.mailId, 'new-mail');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handlePollEmail falls back to row text when the subject node is missing', async () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('normalizeText'),
|
||||||
|
extractFunction('normalizeMinuteTimestamp'),
|
||||||
|
extractFunction('handlePollEmail'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
const matchingMail = {
|
||||||
|
id: 'mail-1',
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'aria-label') return 'OpenAI Your temporary ChatGPT verification code 123456';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const seenCodes = new Set();
|
||||||
|
|
||||||
|
function findMailItems() {
|
||||||
|
return [matchingMail];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentMailIds() {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailItemId(item) {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailTimestamp() {
|
||||||
|
return new Date(2026, 3, 22, 22, 22, 0, 0).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailSenderText() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailSubjectText() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailRowText() {
|
||||||
|
return 'OpenAI Your temporary ChatGPT verification code 123456';
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractVerificationCode(text) {
|
||||||
|
const match = String(text || '').match(/(\\d{6})/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForElement() {
|
||||||
|
return { click() {} };
|
||||||
|
}
|
||||||
|
async function refreshInbox() {}
|
||||||
|
async function sleep() {}
|
||||||
|
function log() {}
|
||||||
|
function persistSeenCodes() {}
|
||||||
|
function scheduleEmailCleanup() {}
|
||||||
|
async function openMailAndGetMessageText() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
${bundle}
|
||||||
|
|
||||||
|
return { handlePollEmail };
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const result = await api.handlePollEmail(8, {
|
||||||
|
senderFilters: ['openai'],
|
||||||
|
subjectFilters: ['verification'],
|
||||||
|
maxAttempts: 1,
|
||||||
|
intervalMs: 1,
|
||||||
|
filterAfterTimestamp: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.code, '123456');
|
||||||
|
assert.equal(result.mailId, 'mail-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handlePollEmail opens matching mail body when preview has no code', async () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('normalizeText'),
|
||||||
|
extractFunction('normalizeMinuteTimestamp'),
|
||||||
|
extractFunction('handlePollEmail'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
const matchingMail = {
|
||||||
|
id: 'mail-body-1',
|
||||||
|
getAttribute(name) {
|
||||||
|
if (name === 'aria-label') return 'OpenAI Your temporary ChatGPT login code';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const seenCodes = new Set();
|
||||||
|
let openedCount = 0;
|
||||||
|
|
||||||
|
function findMailItems() {
|
||||||
|
return [matchingMail];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentMailIds() {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailItemId(item) {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailTimestamp() {
|
||||||
|
return new Date(2026, 3, 22, 22, 49, 0, 0).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailSenderText() {
|
||||||
|
return 'OpenAI';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailSubjectText() {
|
||||||
|
return 'Your temporary ChatGPT login code';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailRowText() {
|
||||||
|
return 'OpenAI Your temporary ChatGPT login code';
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractVerificationCode(text) {
|
||||||
|
const match = String(text || '').match(/(\\d{6})/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForElement() {
|
||||||
|
return { click() {} };
|
||||||
|
}
|
||||||
|
async function refreshInbox() {}
|
||||||
|
async function sleep() {}
|
||||||
|
function log() {}
|
||||||
|
function persistSeenCodes() {}
|
||||||
|
function scheduleEmailCleanup() {}
|
||||||
|
async function openMailAndGetMessageText() {
|
||||||
|
openedCount += 1;
|
||||||
|
return 'Enter this temporary verification code to continue: 214203';
|
||||||
|
}
|
||||||
|
|
||||||
|
${bundle}
|
||||||
|
|
||||||
|
return { handlePollEmail, getOpenedCount: () => openedCount };
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const result = await api.handlePollEmail(8, {
|
||||||
|
senderFilters: ['openai'],
|
||||||
|
subjectFilters: ['verification', 'login'],
|
||||||
|
maxAttempts: 1,
|
||||||
|
intervalMs: 1,
|
||||||
|
filterAfterTimestamp: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.code, '214203');
|
||||||
|
assert.equal(result.mailId, 'mail-body-1');
|
||||||
|
assert.equal(api.getOpenedCount(), 1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user