|
|
@@ -23,7 +23,7 @@ async function ensureAutomationWindowId() {
|
|
|
}
|
|
|
const registry = await getTabRegistry();
|
|
|
for (const entry of Object.values(registry)) {
|
|
|
- if (entry.tabId) {
|
|
|
+ if (entry && entry.tabId) {
|
|
|
try {
|
|
|
const tab = await chrome.tabs.get(entry.tabId);
|
|
|
automationWindowId = tab.windowId;
|
|
|
@@ -38,9 +38,18 @@ async function ensureAutomationWindowId() {
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
-// State Management (chrome.storage.session)
|
|
|
+// State Management (chrome.storage.session + local for persistence)
|
|
|
// ============================================================
|
|
|
|
|
|
+// Keys that should survive extension reload (saved to chrome.storage.local)
|
|
|
+const PERSISTENT_KEYS = [
|
|
|
+ 'vpsUrl', 'customPassword', 'mailProvider',
|
|
|
+ 'tempApiUrl',
|
|
|
+ 'inbucketHost', 'inbucketMailbox', 'accounts',
|
|
|
+ 'seenCodes', 'seenInbucketMailIds',
|
|
|
+ 'duckToken',
|
|
|
+];
|
|
|
+
|
|
|
const DEFAULT_STATE = {
|
|
|
currentStep: 0,
|
|
|
stepStatuses: {
|
|
|
@@ -58,7 +67,8 @@ const DEFAULT_STATE = {
|
|
|
logs: [],
|
|
|
vpsUrl: '',
|
|
|
customPassword: '',
|
|
|
- mailProvider: '163', // 'qq' or '163'
|
|
|
+ mailProvider: 'temp-api', // 'temp-api', 'qq', '163', or 'inbucket'
|
|
|
+ tempApiUrl: '',
|
|
|
inbucketHost: '',
|
|
|
inbucketMailbox: '',
|
|
|
};
|
|
|
@@ -79,11 +89,35 @@ async function initializeSessionStorageAccess() {
|
|
|
} catch (err) {
|
|
|
console.warn(LOG_PREFIX, 'Failed to enable storage.session for content scripts:', err?.message || err);
|
|
|
}
|
|
|
+
|
|
|
+ // Restore persistent settings from chrome.storage.local into session
|
|
|
+ try {
|
|
|
+ const saved = await chrome.storage.local.get(PERSISTENT_KEYS);
|
|
|
+ const toRestore = {};
|
|
|
+ for (const key of PERSISTENT_KEYS) {
|
|
|
+ if (saved[key] !== undefined) toRestore[key] = saved[key];
|
|
|
+ }
|
|
|
+ if (Object.keys(toRestore).length > 0) {
|
|
|
+ await chrome.storage.session.set(toRestore);
|
|
|
+ console.log(LOG_PREFIX, 'Restored persistent settings from local storage:', Object.keys(toRestore).join(', '));
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.warn(LOG_PREFIX, 'Failed to restore persistent settings:', err?.message || err);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
async function setState(updates) {
|
|
|
console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
|
|
|
await chrome.storage.session.set(updates);
|
|
|
+
|
|
|
+ // Persist relevant keys to chrome.storage.local so they survive reload
|
|
|
+ const toPersist = {};
|
|
|
+ for (const key of PERSISTENT_KEYS) {
|
|
|
+ if (key in updates) toPersist[key] = updates[key];
|
|
|
+ }
|
|
|
+ if (Object.keys(toPersist).length > 0) {
|
|
|
+ await chrome.storage.local.set(toPersist);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
function broadcastDataUpdate(payload) {
|
|
|
@@ -117,18 +151,22 @@ async function resetState() {
|
|
|
'inbucketHost',
|
|
|
'inbucketMailbox',
|
|
|
]);
|
|
|
+
|
|
|
+ // Also check chrome.storage.local in case session was empty (after reload)
|
|
|
+ const local = await chrome.storage.local.get(PERSISTENT_KEYS);
|
|
|
+
|
|
|
await chrome.storage.session.clear();
|
|
|
await chrome.storage.session.set({
|
|
|
...DEFAULT_STATE,
|
|
|
- seenCodes: prev.seenCodes || [],
|
|
|
- seenInbucketMailIds: prev.seenInbucketMailIds || [],
|
|
|
- accounts: prev.accounts || [],
|
|
|
+ seenCodes: prev.seenCodes || local.seenCodes || [],
|
|
|
+ seenInbucketMailIds: prev.seenInbucketMailIds || local.seenInbucketMailIds || [],
|
|
|
+ accounts: prev.accounts || local.accounts || [],
|
|
|
tabRegistry: prev.tabRegistry || {},
|
|
|
- vpsUrl: prev.vpsUrl || '',
|
|
|
- customPassword: prev.customPassword || '',
|
|
|
- mailProvider: prev.mailProvider || '163',
|
|
|
- inbucketHost: prev.inbucketHost || '',
|
|
|
- inbucketMailbox: prev.inbucketMailbox || '',
|
|
|
+ vpsUrl: prev.vpsUrl || local.vpsUrl || '',
|
|
|
+ customPassword: prev.customPassword || local.customPassword || '',
|
|
|
+ mailProvider: prev.mailProvider || local.mailProvider || '163',
|
|
|
+ inbucketHost: prev.inbucketHost || local.inbucketHost || '',
|
|
|
+ inbucketMailbox: prev.inbucketMailbox || local.inbucketMailbox || '',
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@@ -392,7 +430,38 @@ async function sendToContentScript(source, message) {
|
|
|
}
|
|
|
|
|
|
console.log(LOG_PREFIX, `Sending to ${source} (tab ${entry.tabId}):`, message.type);
|
|
|
- return chrome.tabs.sendMessage(entry.tabId, message);
|
|
|
+ try {
|
|
|
+ return await chrome.tabs.sendMessage(entry.tabId, message);
|
|
|
+ } catch (err) {
|
|
|
+ // Handle bfcache / port closed errors by reloading the tab and retrying
|
|
|
+ if (err?.message?.includes('back/forward cache') || err?.message?.includes('message channel is closed')) {
|
|
|
+ console.log(LOG_PREFIX, `${source} hit bfcache error, reloading tab and retrying...`);
|
|
|
+ try {
|
|
|
+ await chrome.tabs.reload(entry.tabId);
|
|
|
+ // Wait for page to reload and content script to re-inject
|
|
|
+ await new Promise((resolve) => {
|
|
|
+ const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 15000);
|
|
|
+ const listener = (tid, info) => {
|
|
|
+ if (tid === entry.tabId && info.status === 'complete') {
|
|
|
+ chrome.tabs.onUpdated.removeListener(listener);
|
|
|
+ clearTimeout(timer);
|
|
|
+ resolve();
|
|
|
+ }
|
|
|
+ };
|
|
|
+ chrome.tabs.onUpdated.addListener(listener);
|
|
|
+ });
|
|
|
+ // Mark as not ready and wait for READY signal
|
|
|
+ const reg = await getTabRegistry();
|
|
|
+ if (reg[source]) reg[source].ready = false;
|
|
|
+ await setState({ tabRegistry: reg });
|
|
|
+ // Queue the command — it'll be sent once the content script signals READY
|
|
|
+ return queueCommand(source, message);
|
|
|
+ } catch (retryErr) {
|
|
|
+ throw new Error(`${source} bfcache recovery failed: ${retryErr.message}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw err;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
|
@@ -455,57 +524,7 @@ async function humanStepDelay(min = HUMAN_STEP_DELAY_MIN, max = HUMAN_STEP_DELAY
|
|
|
await sleepWithStop(duration);
|
|
|
}
|
|
|
|
|
|
-async function clickWithDebugger(tabId, rect) {
|
|
|
- if (!tabId) {
|
|
|
- throw new Error('No auth tab found for debugger click.');
|
|
|
- }
|
|
|
- if (!rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) {
|
|
|
- throw new Error('Step 8 debugger fallback needs a valid button position.');
|
|
|
- }
|
|
|
-
|
|
|
- const target = { tabId };
|
|
|
- try {
|
|
|
- await chrome.debugger.attach(target, '1.3');
|
|
|
- } catch (err) {
|
|
|
- throw new Error(
|
|
|
- `Debugger attach failed during step 8 fallback: ${err.message}. ` +
|
|
|
- 'If DevTools is open on the auth tab, close it and retry.'
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- const x = Math.round(rect.centerX);
|
|
|
- const y = Math.round(rect.centerY);
|
|
|
-
|
|
|
- await chrome.debugger.sendCommand(target, 'Page.bringToFront');
|
|
|
- await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
|
|
|
- type: 'mouseMoved',
|
|
|
- x,
|
|
|
- y,
|
|
|
- button: 'none',
|
|
|
- buttons: 0,
|
|
|
- clickCount: 0,
|
|
|
- });
|
|
|
- await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
|
|
|
- type: 'mousePressed',
|
|
|
- x,
|
|
|
- y,
|
|
|
- button: 'left',
|
|
|
- buttons: 1,
|
|
|
- clickCount: 1,
|
|
|
- });
|
|
|
- await chrome.debugger.sendCommand(target, 'Input.dispatchMouseEvent', {
|
|
|
- type: 'mouseReleased',
|
|
|
- x,
|
|
|
- y,
|
|
|
- button: 'left',
|
|
|
- buttons: 0,
|
|
|
- clickCount: 1,
|
|
|
- });
|
|
|
- } finally {
|
|
|
- await chrome.debugger.detach(target).catch(() => {});
|
|
|
- }
|
|
|
-}
|
|
|
+// clickWithDebugger removed — Step 8 now uses content script simulateClick directly
|
|
|
|
|
|
async function broadcastStopToContentScripts() {
|
|
|
const registry = await getTabRegistry();
|
|
|
@@ -627,6 +646,7 @@ async function handleMessage(message, sender) {
|
|
|
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.tempApiUrl !== undefined) updates.tempApiUrl = message.payload.tempApiUrl;
|
|
|
if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost;
|
|
|
if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
|
|
|
await setState(updates);
|
|
|
@@ -821,29 +841,84 @@ async function executeStepAndWait(step, delayAfter = 2000) {
|
|
|
|
|
|
async function fetchDuckEmail(options = {}) {
|
|
|
throwIfStopped();
|
|
|
- const { generateNew = true } = options;
|
|
|
|
|
|
- await addLog(`Duck Mail: Opening autofill settings (${generateNew ? 'generate new' : 'reuse current'})...`);
|
|
|
+ // Try API-based generation first (no tab needed)
|
|
|
+ const state = await getState();
|
|
|
+ let token = state.duckToken;
|
|
|
+
|
|
|
+ if (token) {
|
|
|
+ try {
|
|
|
+ const email = await generateDuckEmailViaApi(token);
|
|
|
+ await setEmailState(email);
|
|
|
+ await addLog(`Duck Mail: Generated ${email} (via API, no tab)`, 'ok');
|
|
|
+ return email;
|
|
|
+ } catch (err) {
|
|
|
+ await addLog(`Duck Mail: API generation failed: ${err.message}, falling back to page...`, 'warn');
|
|
|
+ // Token may be expired, clear it so we re-extract
|
|
|
+ token = null;
|
|
|
+ await setState({ duckToken: null });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Fallback: open page, extract token for future use, then generate
|
|
|
+ await addLog('Duck Mail: Opening page to extract token...');
|
|
|
await reuseOrCreateTab('duck-mail', DUCK_AUTOFILL_URL);
|
|
|
|
|
|
+ // Try to extract and save the access token
|
|
|
+ try {
|
|
|
+ const tokenResult = await sendToContentScript('duck-mail', {
|
|
|
+ type: 'EXTRACT_DUCK_TOKEN',
|
|
|
+ source: 'background',
|
|
|
+ payload: {},
|
|
|
+ });
|
|
|
+ if (tokenResult?.token) {
|
|
|
+ token = tokenResult.token;
|
|
|
+ await setState({ duckToken: token });
|
|
|
+ await addLog(`Duck Mail: Token saved for ${tokenResult.username}, future runs won't need page`, 'ok');
|
|
|
+
|
|
|
+ // Now use API to generate
|
|
|
+ const email = await generateDuckEmailViaApi(token);
|
|
|
+ await setEmailState(email);
|
|
|
+ await addLog(`Duck Mail: Generated ${email} (via API)`, 'ok');
|
|
|
+ return email;
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ await addLog(`Duck Mail: Token extraction failed: ${err.message}, using page method`, 'warn');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Final fallback: use the old page-based method
|
|
|
const result = await sendToContentScript('duck-mail', {
|
|
|
type: 'FETCH_DUCK_EMAIL',
|
|
|
source: 'background',
|
|
|
- payload: { generateNew },
|
|
|
+ payload: { generateNew: true },
|
|
|
});
|
|
|
|
|
|
- if (result?.error) {
|
|
|
- throw new Error(result.error);
|
|
|
- }
|
|
|
- if (!result?.email) {
|
|
|
- throw new Error('Duck email not returned.');
|
|
|
- }
|
|
|
+ if (result?.error) throw new Error(result.error);
|
|
|
+ if (!result?.email) throw new Error('Duck email not returned.');
|
|
|
|
|
|
await setEmailState(result.email);
|
|
|
- await addLog(`Duck Mail: ${result.generated ? 'Generated' : 'Loaded'} ${result.email}`, 'ok');
|
|
|
+ await addLog(`Duck Mail: ${result.generated ? 'Generated' : 'Loaded'} ${result.email} (via page)`, 'ok');
|
|
|
return result.email;
|
|
|
}
|
|
|
|
|
|
+async function generateDuckEmailViaApi(token) {
|
|
|
+ const resp = await fetch('https://quack.duckduckgo.com/api/email/addresses', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${token}`,
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ });
|
|
|
+ if (!resp.ok) {
|
|
|
+ throw new Error(`DuckDuckGo API returned ${resp.status}`);
|
|
|
+ }
|
|
|
+ const data = await resp.json();
|
|
|
+ // Response: { address: "random-words" }
|
|
|
+ const address = data?.address;
|
|
|
+ if (!address) throw new Error('No address in API response');
|
|
|
+ return `${address}@duck.com`;
|
|
|
+}
|
|
|
+
|
|
|
// ============================================================
|
|
|
// Auto Run Flow
|
|
|
// ============================================================
|
|
|
@@ -862,6 +937,8 @@ async function autoRunLoop(totalRuns) {
|
|
|
clearStopRequest();
|
|
|
autoRunActive = true;
|
|
|
autoRunTotalRuns = totalRuns;
|
|
|
+ let successCount = 0;
|
|
|
+ let failCount = 0;
|
|
|
await setState({ autoRunning: true });
|
|
|
|
|
|
for (let run = 1; run <= totalRuns; run++) {
|
|
|
@@ -872,8 +949,10 @@ async function autoRunLoop(totalRuns) {
|
|
|
const keepSettings = {
|
|
|
vpsUrl: prevState.vpsUrl,
|
|
|
mailProvider: prevState.mailProvider,
|
|
|
+ tempApiUrl: prevState.tempApiUrl,
|
|
|
inbucketHost: prevState.inbucketHost,
|
|
|
inbucketMailbox: prevState.inbucketMailbox,
|
|
|
+ duckToken: prevState.duckToken,
|
|
|
autoRunning: true,
|
|
|
};
|
|
|
await resetState();
|
|
|
@@ -931,29 +1010,30 @@ async function autoRunLoop(totalRuns) {
|
|
|
await executeStepAndWait(8, 2000);
|
|
|
await executeStepAndWait(9, 1000);
|
|
|
|
|
|
- await addLog(`=== Run ${run}/${totalRuns} COMPLETE! ===`, 'ok');
|
|
|
+ successCount++;
|
|
|
+ await addLog(`=== Run ${run}/${totalRuns} COMPLETE! (${successCount} succeeded, ${failCount} failed) ===`, 'ok');
|
|
|
|
|
|
} catch (err) {
|
|
|
if (isStopError(err)) {
|
|
|
await addLog(`Run ${run}/${totalRuns} stopped by user`, 'warn');
|
|
|
+ chrome.runtime.sendMessage(status('stopped')).catch(() => {});
|
|
|
+ break; // Only stop on explicit user stop
|
|
|
} else {
|
|
|
- await addLog(`Run ${run}/${totalRuns} failed: ${err.message}`, 'error');
|
|
|
+ failCount++;
|
|
|
+ await addLog(`Run ${run}/${totalRuns} failed: ${err.message} (${successCount} succeeded, ${failCount} failed)`, 'error');
|
|
|
+ await addLog(`Skipping to next run...`, 'warn');
|
|
|
+ continue; // Continue to next run on error
|
|
|
}
|
|
|
- chrome.runtime.sendMessage(status('stopped')).catch(() => {});
|
|
|
- break; // Stop on error
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const completedRuns = autoRunCurrentRun;
|
|
|
+ const attempted = autoRunCurrentRun;
|
|
|
if (stopRequested) {
|
|
|
- await addLog(`=== Stopped after ${Math.max(0, completedRuns - 1)}/${autoRunTotalRuns} runs ===`, 'warn');
|
|
|
- chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
|
|
|
- } else if (completedRuns >= autoRunTotalRuns) {
|
|
|
- await addLog(`=== All ${autoRunTotalRuns} runs completed successfully ===`, 'ok');
|
|
|
- chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
|
|
|
+ await addLog(`=== Stopped after ${Math.max(0, attempted - 1)}/${autoRunTotalRuns} runs — ${successCount} succeeded, ${failCount} failed ===`, 'warn');
|
|
|
+ chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: attempted, totalRuns: autoRunTotalRuns, successCount, failCount } }).catch(() => {});
|
|
|
} else {
|
|
|
- await addLog(`=== Stopped after ${completedRuns}/${autoRunTotalRuns} runs ===`, 'warn');
|
|
|
- chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'stopped', currentRun: completedRuns, totalRuns: autoRunTotalRuns } }).catch(() => {});
|
|
|
+ await addLog(`=== All ${autoRunTotalRuns} runs finished — ${successCount} succeeded, ${failCount} failed ===`, successCount > 0 ? 'ok' : 'warn');
|
|
|
+ chrome.runtime.sendMessage({ type: 'AUTO_RUN_STATUS', payload: { phase: 'complete', currentRun: attempted, totalRuns: autoRunTotalRuns, successCount, failCount } }).catch(() => {});
|
|
|
}
|
|
|
autoRunActive = false;
|
|
|
await setState({ autoRunning: false });
|
|
|
@@ -1053,8 +1133,38 @@ async function executeStep3(state) {
|
|
|
// Step 4: Get Signup Verification Code (qq-mail.js polls, then fills in signup-page.js)
|
|
|
// ============================================================
|
|
|
|
|
|
-function getMailConfig(state) {
|
|
|
- const provider = state.mailProvider || 'qq';
|
|
|
+async function getMailConfig(state) {
|
|
|
+ const provider = state.mailProvider || 'temp-api';
|
|
|
+ if (provider === 'temp-api') {
|
|
|
+ let rawUrl = (state.tempApiUrl || '').trim();
|
|
|
+ // Fallback: read directly from chrome.storage.local in case session is stale
|
|
|
+ if (!rawUrl) {
|
|
|
+ const local = await chrome.storage.local.get('tempApiUrl');
|
|
|
+ rawUrl = (local.tempApiUrl || '').trim();
|
|
|
+ }
|
|
|
+ if (!rawUrl) {
|
|
|
+ return { error: 'Temp Email API URL is empty. Please paste the full URL with JWT in Side Panel.' };
|
|
|
+ }
|
|
|
+ // Extract base origin and JWT from the URL (e.g. https://host/?jwt=xxx)
|
|
|
+ let apiOrigin, jwt;
|
|
|
+ try {
|
|
|
+ const parsed = new URL(rawUrl);
|
|
|
+ apiOrigin = parsed.origin;
|
|
|
+ jwt = parsed.searchParams.get('jwt');
|
|
|
+ } catch {
|
|
|
+ return { error: 'Temp Email API URL is invalid.' };
|
|
|
+ }
|
|
|
+ if (!jwt) {
|
|
|
+ return { error: 'Temp Email API URL has no jwt parameter.' };
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ source: 'temp-api',
|
|
|
+ apiOrigin,
|
|
|
+ jwt,
|
|
|
+ label: 'Temp Email API',
|
|
|
+ useFetch: true, // signal that this provider uses fetch, not a tab
|
|
|
+ };
|
|
|
+ }
|
|
|
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' };
|
|
|
}
|
|
|
@@ -1093,6 +1203,130 @@ function normalizeInbucketOrigin(rawValue) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// ============================================================
|
|
|
+// Temp Email API: fetch-based polling (no tab needed)
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+function extractVerificationCodeFromText(text) {
|
|
|
+ // Pattern 1: Chinese format "代码为 370794" or "验证码...370794"
|
|
|
+ const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
|
|
|
+ if (matchCn) return matchCn[1];
|
|
|
+ // Pattern 2: English format "code is 370794" or "code: 370794"
|
|
|
+ const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
|
|
|
+ if (matchEn) return matchEn[1] || matchEn[2];
|
|
|
+ // Pattern 3: standalone 6-digit number
|
|
|
+ const match6 = text.match(/\b(\d{6})\b/);
|
|
|
+ if (match6) return match6[1];
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+function decodeMimeWord(encoded) {
|
|
|
+ // Decode =?charset?encoding?text?= (RFC 2047)
|
|
|
+ return encoded.replace(/=\?([^?]+)\?(B|Q)\?([^?]*)\?=/gi, (_, charset, enc, text) => {
|
|
|
+ if (enc.toUpperCase() === 'B') {
|
|
|
+ // Base64
|
|
|
+ try { return atob(text); } catch { return text; }
|
|
|
+ }
|
|
|
+ // Quoted-Printable
|
|
|
+ return text.replace(/_/g, ' ').replace(/=([0-9A-Fa-f]{2})/g, (__, hex) =>
|
|
|
+ String.fromCharCode(parseInt(hex, 16))
|
|
|
+ );
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function decodeUtf8Bytes(str) {
|
|
|
+ // If the decoded string contains raw UTF-8 bytes, decode them
|
|
|
+ try { return decodeURIComponent(escape(str)); } catch { return str; }
|
|
|
+}
|
|
|
+
|
|
|
+function extractSubjectFromRaw(raw) {
|
|
|
+ // Extract Subject header (may span multiple lines with folding)
|
|
|
+ const match = raw.match(/^Subject:\s*([\s\S]*?)(?=\r?\n[^\s]|\r?\n\r?\n)/mi);
|
|
|
+ if (!match) return '';
|
|
|
+ // Unfold continuation lines and decode MIME words
|
|
|
+ const unfolded = match[1].replace(/\r?\n\s+/g, ' ').trim();
|
|
|
+ return decodeUtf8Bytes(decodeMimeWord(unfolded));
|
|
|
+}
|
|
|
+
|
|
|
+function extractFromHeader(raw) {
|
|
|
+ const match = raw.match(/^From:\s*([\s\S]*?)(?=\r?\n[^\s]|\r?\n\r?\n)/mi);
|
|
|
+ if (!match) return '';
|
|
|
+ return match[1].replace(/\r?\n\s+/g, ' ').trim().toLowerCase();
|
|
|
+}
|
|
|
+
|
|
|
+function extractPlainTextFromRaw(raw) {
|
|
|
+ // Try to extract the text/plain part from a multipart email
|
|
|
+ const plainMatch = raw.match(/Content-Type:\s*text\/plain[\s\S]*?\r?\n\r?\n([\s\S]*?)(?:\r?\n--|\r?\n$)/i);
|
|
|
+ if (plainMatch) {
|
|
|
+ let text = plainMatch[1];
|
|
|
+ // Decode quoted-printable
|
|
|
+ text = text.replace(/=\r?\n/g, '');
|
|
|
+ text = text.replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
|
+ try { return decodeURIComponent(escape(text)); } catch { return text; }
|
|
|
+ }
|
|
|
+ return raw;
|
|
|
+}
|
|
|
+
|
|
|
+async function pollTempEmailApi(mail, step, payload) {
|
|
|
+ const { senderFilters, subjectFilters, maxAttempts, intervalMs, usedMailIds, usedCodes } = payload;
|
|
|
+ const usedIdSet = new Set((usedMailIds || []).map(String));
|
|
|
+ const usedCodeSet = new Set(usedCodes || []);
|
|
|
+
|
|
|
+ const headers = { 'Authorization': `Bearer ${mail.jwt}` };
|
|
|
+
|
|
|
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
|
+ throwIfStopped();
|
|
|
+ await addLog(`Step ${step}: Polling Temp Email API... attempt ${attempt}/${maxAttempts}`);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const resp = await fetch(`${mail.apiOrigin}/api/mails?limit=10&offset=0`, { headers });
|
|
|
+ if (!resp.ok) throw new Error(`API returned ${resp.status}`);
|
|
|
+ const data = await resp.json();
|
|
|
+ const mails = data.results || [];
|
|
|
+
|
|
|
+ for (const m of mails) {
|
|
|
+ if (usedIdSet.has(String(m.id))) continue;
|
|
|
+
|
|
|
+ const raw = m.raw || '';
|
|
|
+ const source = (m.source || '').toLowerCase();
|
|
|
+ const fromHeader = extractFromHeader(raw);
|
|
|
+ const subject = extractSubjectFromRaw(raw).toLowerCase();
|
|
|
+ const plainText = extractPlainTextFromRaw(raw);
|
|
|
+
|
|
|
+ // Match against both envelope sender and From header
|
|
|
+ const senderMatch = senderFilters.some(f => {
|
|
|
+ const fl = f.toLowerCase();
|
|
|
+ return source.includes(fl) || fromHeader.includes(fl);
|
|
|
+ });
|
|
|
+ const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()));
|
|
|
+
|
|
|
+ if (senderMatch || subjectMatch) {
|
|
|
+ const code = extractVerificationCodeFromText(subject + ' ' + plainText);
|
|
|
+ if (code) {
|
|
|
+ if (usedCodeSet.has(code)) {
|
|
|
+ console.log(LOG_PREFIX, `Step ${step}: Skipping already-used code ${code} (id: ${m.id})`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ await addLog(`Step ${step}: Code found from API: ${code} (subject: ${subject.slice(0, 40)})`, 'ok');
|
|
|
+ return { ok: true, code, emailTimestamp: Date.now(), mailId: String(m.id) };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ await addLog(`Step ${step}: API fetch error: ${err.message}`, 'warn');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (attempt < maxAttempts) {
|
|
|
+ await sleepWithStop(intervalMs);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new Error(
|
|
|
+ `No matching email found via Temp Email API after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
|
|
|
+ 'Email may be delayed.'
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
async function clickResendOnSignupPage(step) {
|
|
|
const signupTabId = await getTabId('signup-page');
|
|
|
if (!signupTabId) return;
|
|
|
@@ -1115,49 +1349,68 @@ async function executeStep4(state) {
|
|
|
// Click "重新发送电子邮件" on the signup page before polling
|
|
|
await clickResendOnSignupPage(4);
|
|
|
|
|
|
- const mail = getMailConfig(state);
|
|
|
+ // Re-read state to ensure tempApiUrl is fresh (may have been saved after initial getState)
|
|
|
+ const freshState = await getState();
|
|
|
+ const mail = await getMailConfig(freshState);
|
|
|
if (mail.error) throw new Error(mail.error);
|
|
|
- await addLog(`Step 4: Opening ${mail.label}...`);
|
|
|
|
|
|
- // For mail tabs, only create if not alive — don't navigate (preserves login session)
|
|
|
- const alive = await isTabAlive(mail.source);
|
|
|
- if (alive) {
|
|
|
- if (mail.navigateOnReuse) {
|
|
|
+ const pollPayload = {
|
|
|
+ filterAfterTimestamp: state.flowStartTime || 0,
|
|
|
+ senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
|
|
|
+ subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'],
|
|
|
+ targetEmail: state.email,
|
|
|
+ maxAttempts: 20,
|
|
|
+ intervalMs: 3000,
|
|
|
+ usedMailIds: state.usedMailIds || [],
|
|
|
+ usedCodes: state.usedCodes || [],
|
|
|
+ };
|
|
|
+
|
|
|
+ let result;
|
|
|
+
|
|
|
+ if (mail.useFetch) {
|
|
|
+ // Temp Email API: fetch directly, no tab needed
|
|
|
+ await addLog(`Step 4: Polling ${mail.label}...`);
|
|
|
+ result = await pollTempEmailApi(mail, 4, pollPayload);
|
|
|
+ } else {
|
|
|
+ await addLog(`Step 4: Opening ${mail.label}...`);
|
|
|
+ // For mail tabs, only create if not alive — don't navigate (preserves login session)
|
|
|
+ const alive = await isTabAlive(mail.source);
|
|
|
+ if (alive) {
|
|
|
+ if (mail.navigateOnReuse) {
|
|
|
+ await reuseOrCreateTab(mail.source, mail.url, {
|
|
|
+ inject: mail.inject,
|
|
|
+ injectSource: mail.injectSource,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ const tabId = await getTabId(mail.source);
|
|
|
+ await chrome.tabs.update(tabId, { active: true });
|
|
|
+ }
|
|
|
+ } else {
|
|
|
await reuseOrCreateTab(mail.source, mail.url, {
|
|
|
inject: mail.inject,
|
|
|
injectSource: mail.injectSource,
|
|
|
});
|
|
|
- } else {
|
|
|
- const tabId = await getTabId(mail.source);
|
|
|
- await chrome.tabs.update(tabId, { active: true });
|
|
|
}
|
|
|
- } else {
|
|
|
- await reuseOrCreateTab(mail.source, mail.url, {
|
|
|
- inject: mail.inject,
|
|
|
- injectSource: mail.injectSource,
|
|
|
+
|
|
|
+ result = await sendToContentScript(mail.source, {
|
|
|
+ type: 'POLL_EMAIL',
|
|
|
+ step: 4,
|
|
|
+ source: 'background',
|
|
|
+ payload: pollPayload,
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- const result = await sendToContentScript(mail.source, {
|
|
|
- type: 'POLL_EMAIL',
|
|
|
- step: 4,
|
|
|
- source: 'background',
|
|
|
- payload: {
|
|
|
- filterAfterTimestamp: state.flowStartTime || 0,
|
|
|
- senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
|
|
|
- subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'],
|
|
|
- targetEmail: state.email,
|
|
|
- maxAttempts: 20,
|
|
|
- intervalMs: 3000,
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
if (result && result.error) {
|
|
|
throw new Error(result.error);
|
|
|
}
|
|
|
|
|
|
if (result && result.code) {
|
|
|
- await setState({ lastEmailTimestamp: result.emailTimestamp });
|
|
|
+ // Track used mail IDs and codes to avoid reusing stale ones
|
|
|
+ const usedMailIds = [...(state.usedMailIds || [])];
|
|
|
+ const usedCodes = [...(state.usedCodes || [])];
|
|
|
+ if (result.mailId) usedMailIds.push(result.mailId);
|
|
|
+ usedCodes.push(result.code);
|
|
|
+ await setState({ lastEmailTimestamp: result.emailTimestamp, usedMailIds, usedCodes });
|
|
|
await addLog(`Step 4: Got verification code: ${result.code}`);
|
|
|
|
|
|
// Switch to signup tab and fill code
|
|
|
@@ -1184,6 +1437,9 @@ async function executeStep5(state) {
|
|
|
const { firstName, lastName } = generateRandomName();
|
|
|
const { year, month, day } = generateRandomBirthday();
|
|
|
|
|
|
+ // Save birthday data to state for potential reuse on about-you page
|
|
|
+ await setState({ birthYear: year, birthMonth: month, birthDay: day, fullName: `${firstName} ${lastName}` });
|
|
|
+
|
|
|
await addLog(`Step 5: Generated name: ${firstName} ${lastName}, Birthday: ${year}-${month}-${day}`);
|
|
|
|
|
|
await sendToContentScript('signup-page', {
|
|
|
@@ -1224,50 +1480,105 @@ async function executeStep6(state) {
|
|
|
// ============================================================
|
|
|
|
|
|
async function executeStep7(state) {
|
|
|
+ // Check if page landed on add-phone — means phone verification required, skip this run
|
|
|
+ const signupTabIdCheck = await getTabId('signup-page');
|
|
|
+ if (signupTabIdCheck) {
|
|
|
+ try {
|
|
|
+ const tab = await chrome.tabs.get(signupTabIdCheck);
|
|
|
+ if (tab.url && (tab.url.includes('add-phone') || tab.url.includes('/phone'))) {
|
|
|
+ throw new Error('Phone verification required — skipping this account.');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ if (err.message.includes('Phone verification')) throw err;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if the page is on /about-you (birthday not filled during signup)
|
|
|
+ // and handle it before proceeding with verification code polling
|
|
|
+ try {
|
|
|
+ const aboutYouResult = await sendToContentScript('signup-page', {
|
|
|
+ type: 'HANDLE_ABOUT_YOU',
|
|
|
+ step: 7,
|
|
|
+ source: 'background',
|
|
|
+ payload: {
|
|
|
+ year: state.birthYear,
|
|
|
+ month: state.birthMonth,
|
|
|
+ day: state.birthDay,
|
|
|
+ fullName: state.fullName,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ if (aboutYouResult && aboutYouResult.handled) {
|
|
|
+ await addLog('Step 7: Handled about-you page, waiting for next page...', 'info');
|
|
|
+ await sleep(3000);
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // Not on about-you page, continue normally
|
|
|
+ }
|
|
|
+
|
|
|
// Click "重新发送电子邮件" on the auth page before polling
|
|
|
await clickResendOnSignupPage(7);
|
|
|
|
|
|
- const mail = getMailConfig(state);
|
|
|
+ const mail = await getMailConfig(state);
|
|
|
if (mail.error) throw new Error(mail.error);
|
|
|
- await addLog(`Step 7: Opening ${mail.label}...`);
|
|
|
|
|
|
- const alive = await isTabAlive(mail.source);
|
|
|
- if (alive) {
|
|
|
- if (mail.navigateOnReuse) {
|
|
|
+ // Re-read state to get latest usedMailIds/usedCodes
|
|
|
+ const freshState = await getState();
|
|
|
+
|
|
|
+ const pollPayload = {
|
|
|
+ filterAfterTimestamp: freshState.lastEmailTimestamp || freshState.flowStartTime || 0,
|
|
|
+ senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
|
|
+ subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
|
|
|
+ targetEmail: freshState.email,
|
|
|
+ maxAttempts: 20,
|
|
|
+ intervalMs: 3000,
|
|
|
+ usedMailIds: freshState.usedMailIds || [],
|
|
|
+ usedCodes: freshState.usedCodes || [],
|
|
|
+ };
|
|
|
+
|
|
|
+ let result;
|
|
|
+
|
|
|
+ if (mail.useFetch) {
|
|
|
+ await addLog(`Step 7: Polling ${mail.label}...`);
|
|
|
+ result = await pollTempEmailApi(mail, 7, pollPayload);
|
|
|
+ } else {
|
|
|
+ await addLog(`Step 7: Opening ${mail.label}...`);
|
|
|
+ const alive = await isTabAlive(mail.source);
|
|
|
+ if (alive) {
|
|
|
+ if (mail.navigateOnReuse) {
|
|
|
+ await reuseOrCreateTab(mail.source, mail.url, {
|
|
|
+ inject: mail.inject,
|
|
|
+ injectSource: mail.injectSource,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ const tabId = await getTabId(mail.source);
|
|
|
+ await chrome.tabs.update(tabId, { active: true });
|
|
|
+ }
|
|
|
+ } else {
|
|
|
await reuseOrCreateTab(mail.source, mail.url, {
|
|
|
inject: mail.inject,
|
|
|
injectSource: mail.injectSource,
|
|
|
});
|
|
|
- } else {
|
|
|
- const tabId = await getTabId(mail.source);
|
|
|
- await chrome.tabs.update(tabId, { active: true });
|
|
|
}
|
|
|
- } else {
|
|
|
- await reuseOrCreateTab(mail.source, mail.url, {
|
|
|
- inject: mail.inject,
|
|
|
- injectSource: mail.injectSource,
|
|
|
+
|
|
|
+ result = await sendToContentScript(mail.source, {
|
|
|
+ type: 'POLL_EMAIL',
|
|
|
+ step: 7,
|
|
|
+ source: 'background',
|
|
|
+ payload: pollPayload,
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- const result = await sendToContentScript(mail.source, {
|
|
|
- type: 'POLL_EMAIL',
|
|
|
- step: 7,
|
|
|
- source: 'background',
|
|
|
- payload: {
|
|
|
- filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0,
|
|
|
- senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
|
|
|
- subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
|
|
|
- targetEmail: state.email,
|
|
|
- maxAttempts: 20,
|
|
|
- intervalMs: 3000,
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
if (result && result.error) {
|
|
|
throw new Error(result.error);
|
|
|
}
|
|
|
|
|
|
if (result && result.code) {
|
|
|
+ // Track used mail IDs and codes
|
|
|
+ const usedMailIds = [...(freshState.usedMailIds || [])];
|
|
|
+ const usedCodes = [...(freshState.usedCodes || [])];
|
|
|
+ if (result.mailId) usedMailIds.push(result.mailId);
|
|
|
+ usedCodes.push(result.code);
|
|
|
+ await setState({ usedMailIds, usedCodes });
|
|
|
await addLog(`Step 7: Got login verification code: ${result.code}`);
|
|
|
|
|
|
// Switch to signup/auth tab and fill code
|
|
|
@@ -1360,17 +1671,16 @@ async function executeStep8(state) {
|
|
|
chrome.webNavigation.onErrorOccurred.addListener(webNavListener);
|
|
|
|
|
|
// After step 7, the auth page shows a consent screen ("使用 ChatGPT 登录到 Codex")
|
|
|
- // with a "继续" button. We locate the button in-page, then click it through
|
|
|
- // the debugger Input API directly.
|
|
|
+ // with a "继续" button. Content script clicks it directly (no debugger needed).
|
|
|
(async () => {
|
|
|
try {
|
|
|
let signupTabId = await getTabId('signup-page');
|
|
|
if (signupTabId) {
|
|
|
await chrome.tabs.update(signupTabId, { active: true });
|
|
|
- await addLog('Step 8: Switched to auth page. Preparing debugger click...');
|
|
|
+ await addLog('Step 8: Switched to auth page...');
|
|
|
} else {
|
|
|
signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
|
|
|
- await addLog('Step 8: Auth tab reopened. Preparing debugger click...');
|
|
|
+ await addLog('Step 8: Auth tab reopened...');
|
|
|
}
|
|
|
|
|
|
const clickResult = await sendToContentScript('signup-page', {
|
|
|
@@ -1383,21 +1693,18 @@ async function executeStep8(state) {
|
|
|
throw new Error(clickResult.error);
|
|
|
}
|
|
|
|
|
|
- if (!resolved) {
|
|
|
- await clickWithDebugger(signupTabId, clickResult?.rect);
|
|
|
- await addLog('Step 8: Debugger click dispatched, waiting for redirect...');
|
|
|
-
|
|
|
- // Fallback: poll tab URL in case webNavigation listeners missed the redirect
|
|
|
- for (let i = 0; i < 30 && !resolved; i++) {
|
|
|
- await new Promise(r => setTimeout(r, 1000));
|
|
|
- try {
|
|
|
- const tab = await chrome.tabs.get(signupTabId);
|
|
|
- if (isLocalhostUrl(tab.url)) {
|
|
|
- captureLocalhostUrl(tab.url);
|
|
|
- break;
|
|
|
- }
|
|
|
- } catch { break; }
|
|
|
- }
|
|
|
+ await addLog('Step 8: "继续" button clicked, waiting for redirect...');
|
|
|
+
|
|
|
+ // Poll tab URL in case webNavigation listeners missed the redirect
|
|
|
+ for (let i = 0; i < 30 && !resolved; i++) {
|
|
|
+ await new Promise(r => setTimeout(r, 1000));
|
|
|
+ try {
|
|
|
+ const tab = await chrome.tabs.get(signupTabId);
|
|
|
+ if (isLocalhostUrl(tab.url)) {
|
|
|
+ captureLocalhostUrl(tab.url);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } catch { break; }
|
|
|
}
|
|
|
} catch (err) {
|
|
|
clearTimeout(timeout);
|