feat: add Inbucket mailbox support and enhance password management in side panel
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
// content/inbucket-mail.js — Content script for Inbucket polling (steps 4, 7)
|
||||
// Injected on: inbucket.j2to.de
|
||||
//
|
||||
// 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(`Step ${message.step}: Stopped by user.`, 'warn');
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
reportError(message.step, err.message);
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
function normalizeText(value) {
|
||||
return (value || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function extractVerificationCode(text) {
|
||||
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
||||
if (matchCn) return matchCn[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) {
|
||||
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);
|
||||
const keywordMatch = /openai|chatgpt|verify|verification|confirm|login|验证码|代码/.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 ${step}: Deleted mailbox message`, 'ok');
|
||||
await sleep(1200);
|
||||
} catch (err) {
|
||||
log(`Step ${step}: Failed to delete mailbox message: ${err.message}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMailboxPollEmail(step, payload) {
|
||||
const {
|
||||
senderFilters = [],
|
||||
subjectFilters = [],
|
||||
maxAttempts = 20,
|
||||
intervalMs = 3000,
|
||||
} = payload || {};
|
||||
|
||||
log(`Step ${step}: Starting email poll on Inbucket mailbox page (max ${maxAttempts} attempts)`);
|
||||
|
||||
try {
|
||||
await waitForElement('.message-list, .message-list-entry', 15000);
|
||||
log(`Step ${step}: Mailbox page loaded`);
|
||||
} catch {
|
||||
throw new Error('Inbucket mailbox page did not load. Make sure /m/<mailbox>/ is open.');
|
||||
}
|
||||
|
||||
const existingMailIds = getCurrentMailboxIds();
|
||||
log(`Step ${step}: Snapshotted ${existingMailIds.size} existing mailbox messages`);
|
||||
|
||||
const FALLBACK_AFTER = 3;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
log(`Polling Inbucket mailbox... attempt ${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, '');
|
||||
if (!match.matched) continue;
|
||||
|
||||
candidates.push({ ...mail, code: match.code });
|
||||
}
|
||||
|
||||
for (const mail of candidates) {
|
||||
const code = mail.code || extractVerificationCode(mail.combinedText);
|
||||
if (!code) continue;
|
||||
|
||||
await openMailboxEntry(mail.entry);
|
||||
await deleteCurrentMailboxMessage(step);
|
||||
|
||||
seenMailIds.add(mail.mailId);
|
||||
await persistSeenMailIds();
|
||||
|
||||
const source = existingMailIds.has(mail.mailId) ? 'fallback' : 'new';
|
||||
log(
|
||||
`Step ${step}: Code found: ${code} (${source}, sender: ${mail.sender || 'unknown'}, subject: ${(mail.subject || '').slice(0, 60)})`,
|
||||
'ok'
|
||||
);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
code,
|
||||
emailTimestamp: Date.now(),
|
||||
mailId: mail.mailId,
|
||||
};
|
||||
}
|
||||
|
||||
if (attempt === FALLBACK_AFTER + 1) {
|
||||
log(`Step ${step}: No new mailbox messages yet, falling back to older matching messages`, 'warn');
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No matching verification email found in Inbucket mailbox after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
|
||||
'Check the mailbox page manually.'
|
||||
);
|
||||
}
|
||||
|
||||
async function handlePollEmail(step, payload) {
|
||||
if (!location.pathname.startsWith('/m/')) {
|
||||
throw new Error('Inbucket now only supports mailbox pages like /m/<mailbox>/.');
|
||||
}
|
||||
return handleMailboxPollEmail(step, payload);
|
||||
}
|
||||
|
||||
} // end of isTopFrame else block
|
||||
+13
-5
@@ -20,19 +20,27 @@ if (!isTopFrame) {
|
||||
// Track codes we've already seen — persisted in chrome.storage.session to survive script re-injection
|
||||
let seenCodes = new Set();
|
||||
|
||||
// Load previously seen codes on startup
|
||||
(async () => {
|
||||
async function loadSeenCodes() {
|
||||
try {
|
||||
const data = await chrome.storage.session.get('seenCodes');
|
||||
if (data.seenCodes && Array.isArray(data.seenCodes)) {
|
||||
seenCodes = new Set(data.seenCodes);
|
||||
console.log(MAIL163_PREFIX, `Loaded ${seenCodes.size} previously seen codes`);
|
||||
}
|
||||
} catch {}
|
||||
})();
|
||||
} catch (err) {
|
||||
console.warn(MAIL163_PREFIX, 'Session storage unavailable, using in-memory seen codes:', err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
// Load previously seen codes on startup
|
||||
loadSeenCodes();
|
||||
|
||||
async function persistSeenCodes() {
|
||||
await chrome.storage.session.set({ seenCodes: [...seenCodes] });
|
||||
try {
|
||||
await chrome.storage.session.set({ seenCodes: [...seenCodes] });
|
||||
} catch (err) {
|
||||
console.warn(MAIL163_PREFIX, 'Could not persist seen codes, continuing in-memory only:', err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ const SCRIPT_SOURCE = (() => {
|
||||
if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page';
|
||||
if (url.includes('mail.qq.com')) return 'qq-mail';
|
||||
if (url.includes('mail.163.com')) return 'mail-163';
|
||||
if (url.includes('inbucket.j2to.de')) return 'inbucket-mail';
|
||||
if (url.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
|
||||
if (url.includes('chatgpt.com')) return 'chatgpt';
|
||||
// VPS panel — detected dynamically since URL is configurable
|
||||
@@ -328,7 +329,7 @@ async function humanPause(min = 250, max = 850) {
|
||||
|
||||
// Auto-report ready on load
|
||||
// Skip ready signal from child iframes of mail pages to avoid overwriting the top frame's registration
|
||||
const _isMailChildFrame = (SCRIPT_SOURCE === 'qq-mail' || SCRIPT_SOURCE === 'mail-163') && window !== window.top;
|
||||
const _isMailChildFrame = (SCRIPT_SOURCE === 'qq-mail' || SCRIPT_SOURCE === 'mail-163' || SCRIPT_SOURCE === 'inbucket-mail') && window !== window.top;
|
||||
if (!_isMailChildFrame) {
|
||||
reportReady();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user