// content/gmail-mail.js — Content script for Gmail inbox polling (steps 4, 7) // Injected on: mail.google.com const GMAIL_PREFIX = '[MultiPage:gmail-mail]'; const isTopFrame = window === window.top; console.log(GMAIL_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child'); if (isTopFrame) { reportReady(); } chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'POLL_EMAIL') { if (!isTopFrame) { sendResponse({ ok: false, reason: 'wrong-frame' }); return; } 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 getInboxRows() { return Array.from(document.querySelectorAll('tr.zA')); } function getRowId(row, index = 0) { return row.getAttribute('data-legacy-message-id') || row.getAttribute('data-legacy-thread-id') || row.dataset.threadPermId || row.id || `gmail-row-${index}`; } function getRowText(row) { return [ row.getAttribute('aria-label') || '', row.textContent || '', row.querySelector('[email]')?.getAttribute('email') || '', row.querySelector('[data-hovercard-id]')?.getAttribute('data-hovercard-id') || '', row.querySelector('.yP')?.getAttribute('email') || '', row.querySelector('.bA4 span')?.textContent || '', row.querySelector('.bog')?.textContent || '', row.querySelector('.y2')?.textContent || '', ].join(' ').replace(/\s+/g, ' ').trim(); } 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 snapshotInboxState() { const snapshot = new Map(); getInboxRows().forEach((row, index) => { const id = getRowId(row, index); snapshot.set(id, { text: getRowText(row), unread: isUnreadRow(row), }); }); return snapshot; } async function applyTargetSearch(targetEmail) { if (!targetEmail) return; const searchInput = document.querySelector('input[placeholder*="Search mail" i], input[aria-label*="Search mail" i], input[placeholder*="搜索邮件"], input[aria-label*="搜索邮件"]'); if (!searchInput) return; const query = `to:${targetEmail}`; if ((searchInput.value || '').trim() === query) return; fillInput(searchInput, query); await sleep(200); searchInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true })); searchInput.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', bubbles: true })); await sleep(1500); log(`Applied Gmail search filter: ${query}`); } function getOpenedMessageText() { const emailAttrs = Array.from(document.querySelectorAll('[email]')) .map(el => el.getAttribute('email') || '') .join(' '); const hovercardIds = Array.from(document.querySelectorAll('[data-hovercard-id]')) .map(el => el.getAttribute('data-hovercard-id') || '') .join(' '); const bodyText = document.body?.innerText || ''; return `${bodyText}\n${emailAttrs}\n${hovercardIds}`.toLowerCase(); } async function openRowAndReadMessageText(row) { const clickable = row.querySelector('.bog, .y6, td, div[role="link"]') || row; simulateClick(clickable); const start = Date.now(); while (Date.now() - start < 12000) { const opened = document.querySelector('h2.hP, div[role="main"] .ii.gt'); if (opened) { await sleep(800); return getOpenedMessageText(); } await sleep(200); } return ''; } async function returnToInboxView(targetEmail = '') { history.back(); const start = Date.now(); while (Date.now() - start < 10000) { if (getInboxRows().length > 0) { await sleep(400); if (targetEmail) { await applyTargetSearch(targetEmail); } return; } await sleep(200); } } async function refreshInbox() { const refreshBtn = document.querySelector('div[role="button"][data-tooltip="Refresh"], div[role="button"][aria-label*="Refresh"], div[role="button"][aria-label*="刷新"]'); if (refreshBtn) { simulateClick(refreshBtn); console.log(GMAIL_PREFIX, 'Clicked refresh button'); await sleep(1000); return; } const inboxLink = Array.from(document.querySelectorAll('a[title], a[aria-label], div[role="link"]')) .find(el => /inbox|收件箱/i.test((el.getAttribute('title') || '') + ' ' + (el.getAttribute('aria-label') || '') + ' ' + (el.textContent || ''))); if (inboxLink) { simulateClick(inboxLink); console.log(GMAIL_PREFIX, 'Clicked inbox link'); await sleep(1000); } } function normalizeGmailAlias(email) { const lower = String(email || '').trim().toLowerCase(); const match = lower.match(/^([^@+]+)(?:\+([^@]+))?@gmail\.com$/i); if (!match) return { full: lower, base: lower, plus: '' }; return { full: lower, base: `${match[1]}@gmail.com`, plus: match[2] || '', }; } function rowMatchesTargetEmail(combinedText, targetEmail) { if (!targetEmail) return true; const text = String(combinedText || '').toLowerCase(); const target = normalizeGmailAlias(targetEmail); if (text.includes(target.full)) return true; if (target.plus && text.includes(`+${target.plus}`)) return true; return false; } function isUnreadRow(row) { return row?.classList?.contains('zE'); } async function handlePollEmail(step, payload) { const { senderFilters, subjectFilters, maxAttempts, intervalMs, usedMailIds, usedCodes, targetEmail } = payload; const usedMailIdSet = new Set((usedMailIds || []).map(String)); const usedCodeSet = new Set(usedCodes || []); log(`Step ${step}: Starting email poll on Gmail (max ${maxAttempts} attempts, every ${intervalMs / 1000}s)`); try { await waitForElement('tr.zA, div[role="main"] table', 15000); log(`Step ${step}: Gmail inbox loaded`); } catch { throw new Error('Gmail inbox did not load. Please open https://mail.google.com/ and ensure you are logged in.'); } await applyTargetSearch(targetEmail); const existingInboxState = snapshotInboxState(); log(`Step ${step}: Snapshotted ${existingInboxState.size} existing Gmail emails`); const FALLBACK_AFTER = 6; for (let attempt = 1; attempt <= maxAttempts; attempt++) { log(`Polling Gmail... attempt ${attempt}/${maxAttempts}`); if (attempt > 1) { await refreshInbox(); await sleep(1200); } const rows = getInboxRows(); const useFallback = attempt > FALLBACK_AFTER; for (let index = 0; index < rows.length; index++) { const row = rows[index]; const mailId = String(getRowId(row, index)); const previousState = existingInboxState.get(mailId); const isNewRow = !previousState; if (usedMailIdSet.has(mailId)) continue; const combinedText = getRowText(row); const lower = combinedText.toLowerCase(); const targetMatch = rowMatchesTargetEmail(combinedText, targetEmail); const senderMatch = senderFilters.some(f => lower.includes(f.toLowerCase())); const subjectMatch = subjectFilters.some(f => lower.includes(f.toLowerCase())); const code = extractVerificationCode(combinedText); const unread = isUnreadRow(row); const rowStateChanged = Boolean(previousState) && ( previousState.text !== combinedText || (!previousState.unread && unread) ); if (!useFallback && !isNewRow && !rowStateChanged) continue; if (rowStateChanged) { log(`Step ${step}: Gmail thread updated for ${mailId}, re-checking row`, 'info'); } if ((senderMatch || subjectMatch || code) && code && (targetMatch || isNewRow)) { if (usedCodeSet.has(code)) { log(`Step ${step}: Skipping already-used code ${code} (mailId: ${mailId})`); continue; } const source = targetMatch ? (useFallback && previousState ? 'fallback-first-match' : (isNewRow ? 'new' : 'updated-thread-match')) : (isNewRow ? 'new-row-match' : 'updated-thread-match'); log(`Step ${step}: Code found: ${code} (${source}, row: ${combinedText.slice(0, 60)})`, 'ok'); return { ok: true, code, emailTimestamp: Date.now(), mailId }; } if (code && !targetMatch && useFallback) { const openedMessageText = await openRowAndReadMessageText(row); const detailMatched = rowMatchesTargetEmail(openedMessageText, targetEmail); if (detailMatched) { if (usedCodeSet.has(code)) { log(`Step ${step}: Skipping already-used code ${code} (mailId: ${mailId})`); await returnToInboxView(targetEmail); continue; } const source = useFallback && previousState ? 'fallback-opened-match' : 'opened-match'; log(`Step ${step}: Code found: ${code} (${source}, target alias matched in opened email)`, 'ok'); return { ok: true, code, emailTimestamp: Date.now(), mailId }; } // As a last resort in fallback mode, trust unread OpenAI verification emails // that appeared near the top of the filtered result set. if (unread && (senderMatch || subjectMatch)) { if (usedCodeSet.has(code)) { log(`Step ${step}: Skipping already-used code ${code} (mailId: ${mailId})`); await returnToInboxView(targetEmail); continue; } log(`Step ${step}: Code found: ${code} (fallback-unread-match, alias hidden in Gmail detail)`, 'ok'); return { ok: true, code, emailTimestamp: Date.now(), mailId }; } await returnToInboxView(targetEmail); log(`Step ${step}: Skipping Gmail code ${code} because neither row nor opened email matched target alias ${targetEmail}`, 'info'); } } if (attempt === FALLBACK_AFTER + 1) { log(`Step ${step}: No new Gmail emails after ${FALLBACK_AFTER} attempts, falling back to first matching email`, 'warn'); } if (attempt < maxAttempts) { await sleep(intervalMs); } } throw new Error( `No matching verification email found on Gmail after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` + 'Check Gmail manually and make sure the inbox tab is open.' ); }