feat: add Inbucket mailbox support and enhance password management in side panel
This commit is contained in:
Vendored
+73
-11
@@ -8,6 +8,8 @@ const STOP_ERROR_MESSAGE = 'Flow stopped by user.';
|
|||||||
const HUMAN_STEP_DELAY_MIN = 700;
|
const HUMAN_STEP_DELAY_MIN = 700;
|
||||||
const HUMAN_STEP_DELAY_MAX = 2200;
|
const HUMAN_STEP_DELAY_MAX = 2200;
|
||||||
|
|
||||||
|
initializeSessionStorageAccess();
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// State Management (chrome.storage.session)
|
// State Management (chrome.storage.session)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -28,7 +30,9 @@ const DEFAULT_STATE = {
|
|||||||
tabRegistry: {},
|
tabRegistry: {},
|
||||||
logs: [],
|
logs: [],
|
||||||
vpsUrl: '',
|
vpsUrl: '',
|
||||||
|
customPassword: '',
|
||||||
mailProvider: '163', // 'qq' or '163'
|
mailProvider: '163', // 'qq' or '163'
|
||||||
|
inbucketMailbox: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getState() {
|
async function getState() {
|
||||||
@@ -36,6 +40,19 @@ async function getState() {
|
|||||||
return { ...DEFAULT_STATE, ...state };
|
return { ...DEFAULT_STATE, ...state };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function initializeSessionStorageAccess() {
|
||||||
|
try {
|
||||||
|
if (chrome.storage?.session?.setAccessLevel) {
|
||||||
|
await chrome.storage.session.setAccessLevel({
|
||||||
|
accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS',
|
||||||
|
});
|
||||||
|
console.log(LOG_PREFIX, 'Enabled storage.session for content scripts');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(LOG_PREFIX, 'Failed to enable storage.session for content scripts:', err?.message || err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function setState(updates) {
|
async function setState(updates) {
|
||||||
console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
|
console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
|
||||||
await chrome.storage.session.set(updates);
|
await chrome.storage.session.set(updates);
|
||||||
@@ -53,18 +70,35 @@ async function setEmailState(email) {
|
|||||||
broadcastDataUpdate({ email });
|
broadcastDataUpdate({ email });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setPasswordState(password) {
|
||||||
|
await setState({ password });
|
||||||
|
broadcastDataUpdate({ password });
|
||||||
|
}
|
||||||
|
|
||||||
async function resetState() {
|
async function resetState() {
|
||||||
console.log(LOG_PREFIX, 'Resetting all state');
|
console.log(LOG_PREFIX, 'Resetting all state');
|
||||||
// Preserve settings and persistent data across resets
|
// Preserve settings and persistent data across resets
|
||||||
const prev = await chrome.storage.session.get(['seenCodes', 'accounts', 'tabRegistry', 'vpsUrl', 'mailProvider']);
|
const prev = await chrome.storage.session.get([
|
||||||
|
'seenCodes',
|
||||||
|
'seenInbucketMailIds',
|
||||||
|
'accounts',
|
||||||
|
'tabRegistry',
|
||||||
|
'vpsUrl',
|
||||||
|
'customPassword',
|
||||||
|
'mailProvider',
|
||||||
|
'inbucketMailbox',
|
||||||
|
]);
|
||||||
await chrome.storage.session.clear();
|
await chrome.storage.session.clear();
|
||||||
await chrome.storage.session.set({
|
await chrome.storage.session.set({
|
||||||
...DEFAULT_STATE,
|
...DEFAULT_STATE,
|
||||||
seenCodes: prev.seenCodes || [],
|
seenCodes: prev.seenCodes || [],
|
||||||
|
seenInbucketMailIds: prev.seenInbucketMailIds || [],
|
||||||
accounts: prev.accounts || [],
|
accounts: prev.accounts || [],
|
||||||
tabRegistry: prev.tabRegistry || {},
|
tabRegistry: prev.tabRegistry || {},
|
||||||
vpsUrl: prev.vpsUrl || '',
|
vpsUrl: prev.vpsUrl || '',
|
||||||
|
customPassword: prev.customPassword || '',
|
||||||
mailProvider: prev.mailProvider || '163',
|
mailProvider: prev.mailProvider || '163',
|
||||||
|
inbucketMailbox: prev.inbucketMailbox || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +515,9 @@ async function handleMessage(message, sender) {
|
|||||||
case 'SAVE_SETTING': {
|
case 'SAVE_SETTING': {
|
||||||
const updates = {};
|
const updates = {};
|
||||||
if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
|
if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
|
||||||
|
if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
|
||||||
if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
|
if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
|
||||||
|
if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
|
||||||
await setState(updates);
|
await setState(updates);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
@@ -725,6 +761,7 @@ async function autoRunLoop(totalRuns) {
|
|||||||
const keepSettings = {
|
const keepSettings = {
|
||||||
vpsUrl: prevState.vpsUrl,
|
vpsUrl: prevState.vpsUrl,
|
||||||
mailProvider: prevState.mailProvider,
|
mailProvider: prevState.mailProvider,
|
||||||
|
inbucketMailbox: prevState.inbucketMailbox,
|
||||||
autoRunning: true,
|
autoRunning: true,
|
||||||
};
|
};
|
||||||
await resetState();
|
await resetState();
|
||||||
@@ -881,16 +918,17 @@ async function executeStep3(state) {
|
|||||||
throw new Error('No email address. Paste email in Side Panel first.');
|
throw new Error('No email address. Paste email in Side Panel first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a unique password for this account
|
const password = state.customPassword || generatePassword();
|
||||||
const password = generatePassword();
|
await setPasswordState(password);
|
||||||
await setState({ password });
|
|
||||||
|
|
||||||
// Save account record
|
// Save account record
|
||||||
const accounts = state.accounts || [];
|
const accounts = state.accounts || [];
|
||||||
accounts.push({ email: state.email, password, createdAt: new Date().toISOString() });
|
accounts.push({ email: state.email, password, createdAt: new Date().toISOString() });
|
||||||
await setState({ accounts });
|
await setState({ accounts });
|
||||||
|
|
||||||
await addLog(`Step 3: Filling email ${state.email}, password generated (${password.length} chars)`);
|
await addLog(
|
||||||
|
`Step 3: Filling email ${state.email}, password ${state.customPassword ? 'customized' : 'generated'} (${password.length} chars)`
|
||||||
|
);
|
||||||
await sendToContentScript('signup-page', {
|
await sendToContentScript('signup-page', {
|
||||||
type: 'EXECUTE_STEP',
|
type: 'EXECUTE_STEP',
|
||||||
step: 3,
|
step: 3,
|
||||||
@@ -908,18 +946,35 @@ function getMailConfig(state) {
|
|||||||
if (provider === '163') {
|
if (provider === '163') {
|
||||||
return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 Mail' };
|
return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 Mail' };
|
||||||
}
|
}
|
||||||
|
if (provider === 'inbucket') {
|
||||||
|
const mailbox = (state.inbucketMailbox || '').trim();
|
||||||
|
if (!mailbox) {
|
||||||
|
return { error: 'Inbucket mailbox name is empty.' };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
source: 'inbucket-mail',
|
||||||
|
url: `https://inbucket.j2to.de/m/${encodeURIComponent(mailbox)}/`,
|
||||||
|
label: `Inbucket Mailbox (${mailbox})`,
|
||||||
|
navigateOnReuse: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' };
|
return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeStep4(state) {
|
async function executeStep4(state) {
|
||||||
const mail = getMailConfig(state);
|
const mail = getMailConfig(state);
|
||||||
|
if (mail.error) throw new Error(mail.error);
|
||||||
await addLog(`Step 4: Opening ${mail.label}...`);
|
await addLog(`Step 4: Opening ${mail.label}...`);
|
||||||
|
|
||||||
// For mail tabs, only create if not alive — don't navigate (preserves login session)
|
// For mail tabs, only create if not alive — don't navigate (preserves login session)
|
||||||
const alive = await isTabAlive(mail.source);
|
const alive = await isTabAlive(mail.source);
|
||||||
if (alive) {
|
if (alive) {
|
||||||
const tabId = await getTabId(mail.source);
|
if (mail.navigateOnReuse) {
|
||||||
await chrome.tabs.update(tabId, { active: true });
|
await reuseOrCreateTab(mail.source, mail.url);
|
||||||
|
} else {
|
||||||
|
const tabId = await getTabId(mail.source);
|
||||||
|
await chrome.tabs.update(tabId, { active: true });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await reuseOrCreateTab(mail.source, mail.url);
|
await reuseOrCreateTab(mail.source, mail.url);
|
||||||
}
|
}
|
||||||
@@ -930,8 +985,9 @@ async function executeStep4(state) {
|
|||||||
source: 'background',
|
source: 'background',
|
||||||
payload: {
|
payload: {
|
||||||
filterAfterTimestamp: state.flowStartTime || 0,
|
filterAfterTimestamp: state.flowStartTime || 0,
|
||||||
senderFilters: ['openai', 'noreply', 'verify', 'auth'],
|
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
|
||||||
subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'],
|
subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'],
|
||||||
|
targetEmail: state.email,
|
||||||
maxAttempts: 20,
|
maxAttempts: 20,
|
||||||
intervalMs: 3000,
|
intervalMs: 3000,
|
||||||
},
|
},
|
||||||
@@ -1010,12 +1066,17 @@ async function executeStep6(state) {
|
|||||||
|
|
||||||
async function executeStep7(state) {
|
async function executeStep7(state) {
|
||||||
const mail = getMailConfig(state);
|
const mail = getMailConfig(state);
|
||||||
|
if (mail.error) throw new Error(mail.error);
|
||||||
await addLog(`Step 7: Opening ${mail.label}...`);
|
await addLog(`Step 7: Opening ${mail.label}...`);
|
||||||
|
|
||||||
const alive = await isTabAlive(mail.source);
|
const alive = await isTabAlive(mail.source);
|
||||||
if (alive) {
|
if (alive) {
|
||||||
const tabId = await getTabId(mail.source);
|
if (mail.navigateOnReuse) {
|
||||||
await chrome.tabs.update(tabId, { active: true });
|
await reuseOrCreateTab(mail.source, mail.url);
|
||||||
|
} else {
|
||||||
|
const tabId = await getTabId(mail.source);
|
||||||
|
await chrome.tabs.update(tabId, { active: true });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await reuseOrCreateTab(mail.source, mail.url);
|
await reuseOrCreateTab(mail.source, mail.url);
|
||||||
}
|
}
|
||||||
@@ -1026,8 +1087,9 @@ async function executeStep7(state) {
|
|||||||
source: 'background',
|
source: 'background',
|
||||||
payload: {
|
payload: {
|
||||||
filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0,
|
filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0,
|
||||||
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt'],
|
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
||||||
subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
|
subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
|
||||||
|
targetEmail: state.email,
|
||||||
maxAttempts: 20,
|
maxAttempts: 20,
|
||||||
intervalMs: 3000,
|
intervalMs: 3000,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
// Track codes we've already seen — persisted in chrome.storage.session to survive script re-injection
|
||||||
let seenCodes = new Set();
|
let seenCodes = new Set();
|
||||||
|
|
||||||
// Load previously seen codes on startup
|
async function loadSeenCodes() {
|
||||||
(async () => {
|
|
||||||
try {
|
try {
|
||||||
const data = await chrome.storage.session.get('seenCodes');
|
const data = await chrome.storage.session.get('seenCodes');
|
||||||
if (data.seenCodes && Array.isArray(data.seenCodes)) {
|
if (data.seenCodes && Array.isArray(data.seenCodes)) {
|
||||||
seenCodes = new Set(data.seenCodes);
|
seenCodes = new Set(data.seenCodes);
|
||||||
console.log(MAIL163_PREFIX, `Loaded ${seenCodes.size} previously seen codes`);
|
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() {
|
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('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.qq.com')) return 'qq-mail';
|
||||||
if (url.includes('mail.163.com')) return 'mail-163';
|
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('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
|
||||||
if (url.includes('chatgpt.com')) return 'chatgpt';
|
if (url.includes('chatgpt.com')) return 'chatgpt';
|
||||||
// VPS panel — detected dynamically since URL is configurable
|
// VPS panel — detected dynamically since URL is configurable
|
||||||
@@ -328,7 +329,7 @@ async function humanPause(min = 250, max = 850) {
|
|||||||
|
|
||||||
// Auto-report ready on load
|
// Auto-report ready on load
|
||||||
// Skip ready signal from child iframes of mail pages to avoid overwriting the top frame's registration
|
// 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) {
|
if (!_isMailChildFrame) {
|
||||||
reportReady();
|
reportReady();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,14 @@
|
|||||||
],
|
],
|
||||||
"js": ["content/utils.js", "content/duck-mail.js"],
|
"js": ["content/utils.js", "content/duck-mail.js"],
|
||||||
"run_at": "document_idle"
|
"run_at": "document_idle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
"https://inbucket.j2to.de/*"
|
||||||
|
],
|
||||||
|
"js": ["content/utils.js", "content/inbucket-mail.js"],
|
||||||
|
"all_frames": true,
|
||||||
|
"run_at": "document_idle"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"action": {
|
"action": {
|
||||||
|
|||||||
@@ -301,6 +301,11 @@ header {
|
|||||||
padding-inline: 10px;
|
padding-inline: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#btn-toggle-password {
|
||||||
|
min-width: 58px;
|
||||||
|
padding-inline: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.data-select {
|
.data-select {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
|
|||||||
@@ -53,8 +53,13 @@
|
|||||||
<select id="select-mail-provider" class="data-select">
|
<select id="select-mail-provider" class="data-select">
|
||||||
<option value="163">163 Mail (mail.163.com)</option>
|
<option value="163">163 Mail (mail.163.com)</option>
|
||||||
<option value="qq">QQ Mail (wx.mail.qq.com)</option>
|
<option value="qq">QQ Mail (wx.mail.qq.com)</option>
|
||||||
|
<option value="inbucket">Inbucket (inbucket.j2to.de/m/<mailbox>/)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="data-row" id="row-inbucket-mailbox" style="display:none;">
|
||||||
|
<span class="data-label">Mailbox</span>
|
||||||
|
<input type="text" id="input-inbucket-mailbox" class="data-input" placeholder="e.g. zju2001" />
|
||||||
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">Email</span>
|
<span class="data-label">Email</span>
|
||||||
<div class="data-inline">
|
<div class="data-inline">
|
||||||
@@ -62,6 +67,13 @@
|
|||||||
<button id="btn-fetch-email" class="btn btn-outline btn-sm" type="button">Auto</button>
|
<button id="btn-fetch-email" class="btn btn-outline btn-sm" type="button">Auto</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="data-row">
|
||||||
|
<span class="data-label">Password</span>
|
||||||
|
<div class="data-inline">
|
||||||
|
<input type="password" id="input-password" class="data-input" placeholder="Leave blank to auto-generate" />
|
||||||
|
<button id="btn-toggle-password" class="btn btn-outline btn-sm" type="button">Show</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">OAuth</span>
|
<span class="data-label">OAuth</span>
|
||||||
<span id="display-oauth-url" class="data-value mono truncate">Waiting...</span>
|
<span id="display-oauth-url" class="data-value mono truncate">Waiting...</span>
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ const displayLocalhostUrl = document.getElementById('display-localhost-url');
|
|||||||
const displayStatus = document.getElementById('display-status');
|
const displayStatus = document.getElementById('display-status');
|
||||||
const statusBar = document.getElementById('status-bar');
|
const statusBar = document.getElementById('status-bar');
|
||||||
const inputEmail = document.getElementById('input-email');
|
const inputEmail = document.getElementById('input-email');
|
||||||
|
const inputPassword = document.getElementById('input-password');
|
||||||
const btnFetchEmail = document.getElementById('btn-fetch-email');
|
const btnFetchEmail = document.getElementById('btn-fetch-email');
|
||||||
|
const btnTogglePassword = document.getElementById('btn-toggle-password');
|
||||||
const btnStop = document.getElementById('btn-stop');
|
const btnStop = document.getElementById('btn-stop');
|
||||||
const btnReset = document.getElementById('btn-reset');
|
const btnReset = document.getElementById('btn-reset');
|
||||||
const stepsProgress = document.getElementById('steps-progress');
|
const stepsProgress = document.getElementById('steps-progress');
|
||||||
@@ -24,6 +26,8 @@ const autoContinueBar = document.getElementById('auto-continue-bar');
|
|||||||
const btnClearLog = document.getElementById('btn-clear-log');
|
const btnClearLog = document.getElementById('btn-clear-log');
|
||||||
const inputVpsUrl = document.getElementById('input-vps-url');
|
const inputVpsUrl = document.getElementById('input-vps-url');
|
||||||
const selectMailProvider = document.getElementById('select-mail-provider');
|
const selectMailProvider = document.getElementById('select-mail-provider');
|
||||||
|
const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
|
||||||
|
const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
|
||||||
const inputRunCount = document.getElementById('input-run-count');
|
const inputRunCount = document.getElementById('input-run-count');
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -77,12 +81,16 @@ async function restoreState() {
|
|||||||
if (state.email) {
|
if (state.email) {
|
||||||
inputEmail.value = state.email;
|
inputEmail.value = state.email;
|
||||||
}
|
}
|
||||||
|
syncPasswordField(state);
|
||||||
if (state.vpsUrl) {
|
if (state.vpsUrl) {
|
||||||
inputVpsUrl.value = state.vpsUrl;
|
inputVpsUrl.value = state.vpsUrl;
|
||||||
}
|
}
|
||||||
if (state.mailProvider) {
|
if (state.mailProvider) {
|
||||||
selectMailProvider.value = state.mailProvider;
|
selectMailProvider.value = state.mailProvider;
|
||||||
}
|
}
|
||||||
|
if (state.inbucketMailbox) {
|
||||||
|
inputInbucketMailbox.value = state.inbucketMailbox;
|
||||||
|
}
|
||||||
|
|
||||||
if (state.stepStatuses) {
|
if (state.stepStatuses) {
|
||||||
for (const [step, status] of Object.entries(state.stepStatuses)) {
|
for (const [step, status] of Object.entries(state.stepStatuses)) {
|
||||||
@@ -98,11 +106,21 @@ async function restoreState() {
|
|||||||
|
|
||||||
updateStatusDisplay(state);
|
updateStatusDisplay(state);
|
||||||
updateProgressCounter();
|
updateProgressCounter();
|
||||||
|
updateMailProviderUI();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to restore state:', err);
|
console.error('Failed to restore state:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncPasswordField(state) {
|
||||||
|
inputPassword.value = state.customPassword || state.password || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMailProviderUI() {
|
||||||
|
const useInbucket = selectMailProvider.value === 'inbucket';
|
||||||
|
rowInbucketMailbox.style.display = useInbucket ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// UI Updates
|
// UI Updates
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -262,6 +280,10 @@ async function fetchDuckEmail() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncPasswordToggleLabel() {
|
||||||
|
btnTogglePassword.textContent = inputPassword.type === 'password' ? 'Show' : 'Hide';
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Button Handlers
|
// Button Handlers
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -286,6 +308,11 @@ btnFetchEmail.addEventListener('click', async () => {
|
|||||||
await fetchDuckEmail().catch(() => {});
|
await fetchDuckEmail().catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
btnTogglePassword.addEventListener('click', () => {
|
||||||
|
inputPassword.type = inputPassword.type === 'password' ? 'text' : 'password';
|
||||||
|
syncPasswordToggleLabel();
|
||||||
|
});
|
||||||
|
|
||||||
btnStop.addEventListener('click', async () => {
|
btnStop.addEventListener('click', async () => {
|
||||||
btnStop.disabled = true;
|
btnStop.disabled = true;
|
||||||
await chrome.runtime.sendMessage({ type: 'STOP_FLOW', source: 'sidepanel', payload: {} });
|
await chrome.runtime.sendMessage({ type: 'STOP_FLOW', source: 'sidepanel', payload: {} });
|
||||||
@@ -355,13 +382,30 @@ inputVpsUrl.addEventListener('change', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
inputPassword.addEventListener('change', async () => {
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
type: 'SAVE_SETTING',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { customPassword: inputPassword.value },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
selectMailProvider.addEventListener('change', async () => {
|
selectMailProvider.addEventListener('change', async () => {
|
||||||
|
updateMailProviderUI();
|
||||||
await chrome.runtime.sendMessage({
|
await chrome.runtime.sendMessage({
|
||||||
type: 'SAVE_SETTING', source: 'sidepanel',
|
type: 'SAVE_SETTING', source: 'sidepanel',
|
||||||
payload: { mailProvider: selectMailProvider.value },
|
payload: { mailProvider: selectMailProvider.value },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
inputInbucketMailbox.addEventListener('change', async () => {
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
type: 'SAVE_SETTING',
|
||||||
|
source: 'sidepanel',
|
||||||
|
payload: { inbucketMailbox: inputInbucketMailbox.value.trim() },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Listen for Background broadcasts
|
// Listen for Background broadcasts
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -381,6 +425,7 @@ chrome.runtime.onMessage.addListener((message) => {
|
|||||||
chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(updateStatusDisplay);
|
chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(updateStatusDisplay);
|
||||||
if (status === 'completed') {
|
if (status === 'completed') {
|
||||||
chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(state => {
|
chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(state => {
|
||||||
|
syncPasswordField(state);
|
||||||
if (state.oauthUrl) {
|
if (state.oauthUrl) {
|
||||||
displayOauthUrl.textContent = state.oauthUrl;
|
displayOauthUrl.textContent = state.oauthUrl;
|
||||||
displayOauthUrl.classList.add('has-value');
|
displayOauthUrl.classList.add('has-value');
|
||||||
@@ -415,6 +460,9 @@ chrome.runtime.onMessage.addListener((message) => {
|
|||||||
if (message.payload.email) {
|
if (message.payload.email) {
|
||||||
inputEmail.value = message.payload.email;
|
inputEmail.value = message.payload.email;
|
||||||
}
|
}
|
||||||
|
if (message.payload.password !== undefined) {
|
||||||
|
inputPassword.value = message.payload.password || '';
|
||||||
|
}
|
||||||
if (message.payload.oauthUrl) {
|
if (message.payload.oauthUrl) {
|
||||||
displayOauthUrl.textContent = message.payload.oauthUrl;
|
displayOauthUrl.textContent = message.payload.oauthUrl;
|
||||||
displayOauthUrl.classList.add('has-value');
|
displayOauthUrl.classList.add('has-value');
|
||||||
@@ -490,5 +538,6 @@ btnTheme.addEventListener('click', () => {
|
|||||||
|
|
||||||
initTheme();
|
initTheme();
|
||||||
restoreState().then(() => {
|
restoreState().then(() => {
|
||||||
|
syncPasswordToggleLabel();
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user