import GuJumpgate snapshot from GitHub

This commit is contained in:
2026-05-20 20:33:51 +08:00
commit f8903dcd14
335 changed files with 179790 additions and 0 deletions
+321
View File
@@ -0,0 +1,321 @@
// content/inbucket-mail.js — Content script for Inbucket polling (steps 4, 7)
// Injected dynamically on the configured Inbucket host
//
// Supported page:
// - /m/<mailbox>/
const INBUCKET_PREFIX = '[MultiPage:inbucket-mail]';
const isTopFrame = window === window.top;
const SEEN_MAIL_IDS_KEY = 'seenInbucketMailIds';
console.log(INBUCKET_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
if (!isTopFrame) {
console.log(INBUCKET_PREFIX, 'Skipping child frame');
} else {
let seenMailIds = new Set();
async function loadSeenMailIds() {
try {
const data = await chrome.storage.session.get(SEEN_MAIL_IDS_KEY);
if (Array.isArray(data[SEEN_MAIL_IDS_KEY])) {
seenMailIds = new Set(data[SEEN_MAIL_IDS_KEY]);
console.log(INBUCKET_PREFIX, `Loaded ${seenMailIds.size} previously seen mail ids`);
}
} catch (err) {
console.warn(INBUCKET_PREFIX, 'Session storage unavailable, using in-memory seen mail ids:', err?.message || err);
}
}
async function persistSeenMailIds() {
try {
await chrome.storage.session.set({ [SEEN_MAIL_IDS_KEY]: [...seenMailIds] });
} catch (err) {
console.warn(INBUCKET_PREFIX, 'Could not persist seen mail ids, continuing in-memory only:', err?.message || err);
}
}
loadSeenMailIds();
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}:邮箱轮询失败:${err.message}`, 'warn');
sendResponse({ error: err.message });
});
return true;
}
});
function normalizeText(value) {
return (value || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function normalizeRulePatternList(patterns = []) {
return Array.isArray(patterns) ? patterns : [];
}
function extractCodeByRulePatterns(text, patterns = []) {
const normalizedText = String(text || '');
for (const pattern of normalizeRulePatternList(patterns)) {
try {
const source = String(pattern?.source || '').trim();
if (!source) {
continue;
}
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
const match = normalizedText.match(new RegExp(source, flags));
if (!match) {
continue;
}
for (let index = 1; index < match.length; index += 1) {
const candidate = String(match[index] || '').trim();
if (candidate) {
return candidate;
}
}
if (String(match[0] || '').trim()) {
return String(match[0] || '').trim();
}
} catch (_) {
// Ignore invalid runtime rule patterns and continue with other candidates.
}
}
return null;
}
function matchesKeywordHints(text, keywords = []) {
const normalizedText = normalizeText(text);
const normalizedKeywords = Array.isArray(keywords) ? keywords : [];
return normalizedKeywords.some((keyword) => normalizedText.includes(normalizeText(keyword)));
}
function extractVerificationCode(text, options = {}) {
const matchedByRule = extractCodeByRulePatterns(text, options?.codePatterns);
if (matchedByRule) {
return matchedByRule;
}
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s:]*(\d{6})/);
if (matchCn) return matchCn[1];
const matchLoginCode = text.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
if (matchLoginCode) return matchLoginCode[1];
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
if (matchEn) return matchEn[1] || matchEn[2];
const match6 = text.match(/\b(\d{6})\b/);
if (match6) return match6[1];
return null;
}
function rowMatchesFilters(mail, senderFilters, subjectFilters, targetEmail, options = {}) {
const sender = normalizeText(mail.sender);
const subject = normalizeText(mail.subject);
const mailbox = normalizeText(mail.mailbox);
const combined = normalizeText(mail.combinedText);
const targetLocal = normalizeText((targetEmail || '').split('@')[0]);
const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()) || combined.includes(f.toLowerCase()));
const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()) || combined.includes(f.toLowerCase()));
const mailboxMatch = Boolean(targetLocal) && mailbox.includes(targetLocal);
const forwardedDuck = /duckduckgo|forward(?:ed)?\s*by/i.test(mail.combinedText);
const code = extractVerificationCode(mail.combinedText, {
codePatterns: options?.codePatterns,
});
const keywordMatch = options?.requiredKeywords?.length
? matchesKeywordHints(combined, options.requiredKeywords)
: /verify|verification|confirm|log-?in|验证码|代码/.test(combined);
if (mailboxMatch) return { matched: true, mailboxMatch, code };
if (senderMatch || subjectMatch) return { matched: true, mailboxMatch: false, code };
if (code && (forwardedDuck || keywordMatch)) return { matched: true, mailboxMatch: false, code };
return { matched: false, mailboxMatch: false, code };
}
function findMailboxEntries() {
return document.querySelectorAll('.message-list-entry');
}
function getMailboxEntryId(entry, index = 0) {
const explicitId = entry.getAttribute('data-id') || entry.dataset?.id || '';
if (explicitId) return explicitId;
const subject = entry.querySelector('.subject')?.textContent?.trim() || '';
const sender = entry.querySelector('.from')?.textContent?.trim() || '';
const dateText = entry.querySelector('.date')?.textContent?.trim() || '';
return `mailbox:${index}:${normalizeText(subject)}|${normalizeText(sender)}|${normalizeText(dateText)}`;
}
function parseMailboxEntry(entry, index = 0) {
const subject = entry.querySelector('.subject')?.textContent?.trim() || '';
const sender = entry.querySelector('.from')?.textContent?.trim() || '';
const dateText = entry.querySelector('.date')?.textContent?.trim() || '';
const combinedText = [subject, sender, dateText].filter(Boolean).join(' ');
return {
entry,
dateText,
sender,
mailbox: '',
subject,
unread: entry.classList.contains('unseen'),
combinedText,
mailId: getMailboxEntryId(entry, index),
};
}
function getCurrentMailboxIds() {
const ids = new Set();
Array.from(findMailboxEntries()).forEach((entry, index) => {
ids.add(getMailboxEntryId(entry, index));
});
return ids;
}
async function refreshMailbox() {
const refreshButton = document.querySelector('button[alt="Refresh Mailbox"]');
if (!refreshButton) return;
simulateClick(refreshButton);
await sleep(800);
}
async function openMailboxEntry(entry) {
simulateClick(entry);
for (let i = 0; i < 20; i++) {
if (entry.classList.contains('selected') || document.querySelector('.message-header, .message-body, .button-bar')) {
return;
}
await sleep(150);
}
}
async function deleteCurrentMailboxMessage(step) {
try {
const deleteButton = await waitForElement('.button-bar button.danger', 5000);
simulateClick(deleteButton);
log(`步骤 ${step}:已删除邮箱消息`, 'ok');
await sleep(1200);
} catch (err) {
log(`步骤 ${step}:删除邮箱消息失败:${err.message}`, 'warn');
}
}
async function handleMailboxPollEmail(step, payload) {
const {
codePatterns = [],
requiredKeywords = [],
senderFilters = [],
subjectFilters = [],
maxAttempts = 20,
intervalMs = 3000,
excludeCodes = [],
} = payload || {};
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
log(`步骤 ${step}:开始轮询 Inbucket 邮箱页面(最多 ${maxAttempts} 次)`);
try {
await waitForElement('.message-list, .message-list-entry', 15000);
log(`步骤 ${step}:邮箱页面已加载`);
} catch {
throw new Error('Inbucket 邮箱页面未加载完成,请确认已打开 /m/<mailbox>/ 页面。');
}
const existingMailIds = getCurrentMailboxIds();
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧消息快照`);
const FALLBACK_AFTER = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
log(`步骤 ${step}:正在轮询 Inbucket 邮箱,第 ${attempt}/${maxAttempts}`);
if (attempt > 1) {
await refreshMailbox();
}
const entries = Array.from(findMailboxEntries()).map(parseMailboxEntry);
const useFallback = attempt > FALLBACK_AFTER;
const candidates = [];
for (const mail of entries) {
if (!mail.unread) continue;
if (seenMailIds.has(mail.mailId)) continue;
if (!useFallback && existingMailIds.has(mail.mailId)) continue;
const match = rowMatchesFilters(mail, senderFilters, subjectFilters, '', {
codePatterns,
requiredKeywords,
});
if (!match.matched) continue;
candidates.push({ ...mail, code: match.code });
}
for (const mail of candidates) {
const code = mail.code || extractVerificationCode(mail.combinedText, {
codePatterns,
});
if (!code) continue;
if (excludedCodeSet.has(code)) {
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
continue;
}
await openMailboxEntry(mail.entry);
await deleteCurrentMailboxMessage(step);
seenMailIds.add(mail.mailId);
await persistSeenMailIds();
const source = existingMailIds.has(mail.mailId) ? '回退匹配邮件' : '新邮件';
log(
`步骤 ${step}:已找到验证码:${code}(来源:${source},发件人:${mail.sender || '未知'},主题:${(mail.subject || '').slice(0, 60)}`,
'ok'
);
return {
ok: true,
code,
emailTimestamp: Date.now(),
mailId: mail.mailId,
};
}
if (attempt === FALLBACK_AFTER + 1) {
log(`步骤 ${step}:暂未发现新消息,开始回退到较早的匹配邮件`, 'warn');
}
if (attempt < maxAttempts) {
await sleep(intervalMs);
}
}
throw new Error(
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 Inbucket 邮箱中找到匹配的验证码邮件。` +
'请手动检查邮箱页面。'
);
}
async function handlePollEmail(step, payload) {
if (!location.pathname.startsWith('/m/')) {
throw new Error('当前 Inbucket 仅支持 /m/<mailbox>/ 这种邮箱页面。');
}
return handleMailboxPollEmail(step, payload);
}
} // end of isTopFrame else block