From 64868e05c1d3d946d33faad6a53d96246bec2d00 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Wed, 15 Apr 2026 10:03:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AF=B9=20Gmail=20?= =?UTF-8?q?=E9=82=AE=E7=AE=B1=E7=9A=84=E6=94=AF=E6=8C=81=EF=BC=8C=E5=8C=85?= =?UTF-8?q?=E6=8B=AC=E9=82=AE=E7=AE=B1=E5=89=8D=E7=BC=80=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=92=8C=E9=AA=8C=E8=AF=81=E7=A0=81=E6=8F=90=E5=8F=96=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 64 +++++ content/gmail-mail.js | 604 +++++++++++++++++++++++++++++++++++++++ content/utils.js | 3 +- manifest.json | 4 +- sidepanel/sidepanel.html | 1 + sidepanel/sidepanel.js | 85 +++++- 6 files changed, 749 insertions(+), 12 deletions(-) create mode 100644 content/gmail-mail.js diff --git a/background.js b/background.js index 4b196c0..5bf22a7 100644 --- a/background.js +++ b/background.js @@ -92,6 +92,7 @@ const ICLOUD_LOGIN_URLS = [ 'https://www.icloud.com.cn/', 'https://www.icloud.com/', ]; +const GMAIL_PROVIDER = 'gmail'; const HOTMAIL_PROVIDER = 'hotmail-api'; const LUCKMAIL_PROVIDER = 'luckmail-api'; const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email'; @@ -486,6 +487,7 @@ function normalizeMailProvider(value = '') { const normalized = String(value || '').trim().toLowerCase(); switch (normalized) { case 'custom': + case GMAIL_PROVIDER: case HOTMAIL_PROVIDER: case LUCKMAIL_PROVIDER: case CLOUDFLARE_TEMP_EMAIL_PROVIDER: @@ -1791,10 +1793,42 @@ function generateRandomSuffix(length = 6) { return suffix; } +const GMAIL_ALIAS_WORDS = [ + 'amber', 'apple', 'ash', 'berry', 'birch', 'blue', 'brook', 'cedar', + 'cloud', 'clover', 'coast', 'cocoa', 'coral', 'dawn', 'delta', 'echo', + 'ember', 'field', 'flint', 'flora', 'forest', 'frost', 'glade', 'harbor', + 'hazel', 'honey', 'ivory', 'jade', 'lake', 'leaf', 'light', 'lilac', + 'lotus', 'lunar', 'maple', 'meadow', 'mist', 'moon', 'nova', 'oasis', + 'olive', 'opal', 'pearl', 'pine', 'pixel', 'plum', 'quartz', 'rain', + 'raven', 'river', 'rose', 'sage', 'shore', 'sky', 'solar', 'spark', + 'stone', 'storm', 'sun', 'terra', 'vale', 'wave', 'willow', 'zephyr', +]; + +function generateRandomWordAliasTag(parts = 3) { + const selected = []; + for (let i = 0; i < parts; i++) { + selected.push(GMAIL_ALIAS_WORDS[Math.floor(Math.random() * GMAIL_ALIAS_WORDS.length)]); + } + return selected.join(''); +} + +function parseGmailBaseEmail(rawValue) { + const value = String(rawValue || '').trim().toLowerCase(); + const match = value.match(/^([^@\s+]+)@((?:gmail|googlemail)\.com)$/i); + if (!match) return null; + return { + localPart: match[1], + domain: match[2].toLowerCase(), + }; +} + function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) { const provider = typeof stateOrProvider === 'string' ? stateOrProvider : stateOrProvider?.mailProvider; + if (provider === GMAIL_PROVIDER) { + return true; + } const resolvedMail2925Mode = mail2925Mode !== undefined ? normalizeMail2925Mode(mail2925Mode) : getMail2925Mode(stateOrProvider); @@ -1812,6 +1846,17 @@ function buildGeneratedAliasEmail(state) { const provider = state.mailProvider || '163'; const emailPrefix = (state.emailPrefix || '').trim(); + if (provider === GMAIL_PROVIDER) { + if (!emailPrefix) { + throw new Error('Gmail 原邮箱未设置,请先在侧边栏填写。'); + } + const parsed = parseGmailBaseEmail(emailPrefix); + if (!parsed) { + throw new Error('Gmail 原邮箱格式不正确,请填写类似 name@gmail.com 的地址。'); + } + return `${parsed.localPart}+${generateRandomWordAliasTag()}@${parsed.domain}`; + } + if (!emailPrefix) { throw new Error('2925 邮箱前缀未设置,请先在侧边栏填写。'); } @@ -3151,6 +3196,8 @@ function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) { return candidate.hostname === 'mail.qq.com' || candidate.hostname === 'wx.mail.qq.com'; case 'mail-163': return is163MailHost(candidate.hostname); + case 'gmail-mail': + return candidate.hostname === 'mail.google.com'; case 'inbucket-mail': return Boolean(reference) && candidate.origin === reference.origin @@ -3856,6 +3903,7 @@ function getStep8CallbackUrlFromTabUpdate(tabId, changeInfo, tab, signupTabId) { function getSourceLabel(source) { const labels = { + 'gmail-mail': 'Gmail 邮箱', 'sidepanel': '侧边栏', 'signup-page': '认证页', 'vps-panel': 'CPA 面板', @@ -5606,6 +5654,13 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { } if (isGeneratedAliasProvider(currentState)) { + if (currentState.mailProvider === GMAIL_PROVIDER) { + if (!currentState.emailPrefix) { + throw new Error('Gmail 原邮箱未设置,请先在侧边栏填写。'); + } + await addLog(`=== 鐩爣 ${targetRun}/${totalRuns} 杞細Gmail +tag 妯″紡宸插惎鐢紝灏嗗湪姝ラ 3 鑷姩鐢熸垚閭锛堢 ${attemptRuns} 娆″皾璇曪級===`, 'info'); + return null; + } if (!currentState.emailPrefix) { throw new Error('2925 邮箱前缀未设置,请先在侧边栏填写。'); } @@ -6531,6 +6586,15 @@ function getMailConfig(state) { if (provider === HOTMAIL_PROVIDER) { return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(API对接/本地助手)' }; } + if (provider === GMAIL_PROVIDER) { + return { + source: 'gmail-mail', + url: 'https://mail.google.com/mail/u/0/#inbox', + label: 'Gmail 邮箱', + inject: ['content/activation-utils.js', 'content/utils.js', 'content/gmail-mail.js'], + injectSource: 'gmail-mail', + }; + } if (provider === LUCKMAIL_PROVIDER) { return { provider: LUCKMAIL_PROVIDER, label: 'LuckMail(API 购邮)' }; } diff --git a/content/gmail-mail.js b/content/gmail-mail.js new file mode 100644 index 0000000..d40efe9 --- /dev/null +++ b/content/gmail-mail.js @@ -0,0 +1,604 @@ +// content/gmail-mail.js — Content script for Gmail polling (steps 4, 7) +// Injected dynamically on: mail.google.com + +const GMAIL_PREFIX = '[MultiPage:gmail-mail]'; +const GMAIL_SEEN_CODES_KEY = 'seenGmailCodes'; +const GMAIL_FALLBACK_AFTER = 3; +const isTopFrame = window === window.top; + +console.log(GMAIL_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child'); + +if (!isTopFrame) { + console.log(GMAIL_PREFIX, 'Skipping child frame'); +} else { + +let seenCodes = new Set(); + +async function loadSeenCodes() { + try { + const data = await chrome.storage.session.get(GMAIL_SEEN_CODES_KEY); + if (Array.isArray(data[GMAIL_SEEN_CODES_KEY])) { + seenCodes = new Set(data[GMAIL_SEEN_CODES_KEY]); + console.log(GMAIL_PREFIX, `Loaded ${seenCodes.size} previously seen codes`); + } + } catch (err) { + console.warn(GMAIL_PREFIX, 'Session storage unavailable, using in-memory seen codes:', err?.message || err); + } +} + +async function persistSeenCodes() { + try { + await chrome.storage.session.set({ [GMAIL_SEEN_CODES_KEY]: [...seenCodes] }); + } catch (err) { + console.warn(GMAIL_PREFIX, 'Could not persist seen codes, continuing in-memory only:', err?.message || err); + } +} + +loadSeenCodes(); + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'POLL_EMAIL') { + resetStopState(); + handlePollEmail(message.step, message.payload).then((result) => { + sendResponse(result); + }).catch((err) => { + if (isStopError(err)) { + log(`步骤 ${message.step}:已被用户停止。`, 'warn'); + sendResponse({ stopped: true, error: err.message }); + return; + } + log(`步骤 ${message.step}:Gmail 轮询失败:${err.message}`, 'warn'); + sendResponse({ error: err.message }); + }); + return true; + } +}); + +function normalizeText(value) { + return String(value || '').replace(/\s+/g, ' ').trim(); +} + +function isDisplayed(element) { + if (!element) return false; + const style = window.getComputedStyle(element); + return style.display !== 'none' && style.visibility !== 'hidden'; +} + +function isVisibleElement(element) { + if (!isDisplayed(element)) return false; + const rect = element.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; +} + +function normalizeMinuteTimestamp(timestamp) { + if (!Number.isFinite(timestamp) || timestamp <= 0) return 0; + const date = new Date(timestamp); + date.setSeconds(0, 0); + return date.getTime(); +} + +function extractEmails(text) { + const matches = String(text || '').match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig) || []; + return [...new Set(matches.map((item) => item.toLowerCase()))]; +} + +function emailMatchesTarget(candidate, targetEmail) { + const normalizedCandidate = String(candidate || '').trim().toLowerCase(); + const normalizedTarget = String(targetEmail || '').trim().toLowerCase(); + return Boolean(normalizedCandidate && normalizedTarget && normalizedCandidate === normalizedTarget); +} + +function getTargetEmailMatchState(text, targetEmail) { + const normalizedTarget = String(targetEmail || '').trim().toLowerCase(); + if (!normalizedTarget) { + return { matches: true, hasExplicitEmail: false }; + } + + const normalizedText = String(text || '').toLowerCase(); + if (normalizedText.includes(normalizedTarget)) { + return { matches: true, hasExplicitEmail: true }; + } + + const atIndex = normalizedTarget.indexOf('@'); + if (atIndex > 0) { + const encodedTarget = `${normalizedTarget.slice(0, atIndex)}=${normalizedTarget.slice(atIndex + 1)}`; + if (normalizedText.includes(encodedTarget)) { + return { matches: true, hasExplicitEmail: true }; + } + } + + const emails = extractEmails(text); + if (!emails.length) { + return { matches: false, hasExplicitEmail: false }; + } + + return { + matches: emails.some((email) => emailMatchesTarget(email, normalizedTarget)), + hasExplicitEmail: true, + }; +} + +const MONTH_INDEX_MAP = { + jan: 0, + feb: 1, + mar: 2, + apr: 3, + may: 4, + jun: 5, + jul: 6, + aug: 7, + sep: 8, + oct: 9, + nov: 10, + dec: 11, +}; + +function parseGmailTimestampText(rawText) { + const text = normalizeText(rawText); + if (!text) return null; + + const parsedNative = Date.parse(text); + if (Number.isFinite(parsedNative)) { + return parsedNative; + } + + let match = text.match(/(\d{4})[年/-](\d{1,2})[月/-](\d{1,2})日?\s+(\d{1,2}):(\d{2})(?:\s*([AP]M))?/i); + if (match) { + const [, year, month, day, hourText, minute, meridiem] = match; + let hour = Number(hourText); + if (/pm/i.test(meridiem) && hour < 12) hour += 12; + if (/am/i.test(meridiem) && hour === 12) hour = 0; + return new Date(Number(year), Number(month) - 1, Number(day), hour, Number(minute), 0, 0).getTime(); + } + + match = text.match(/\b([A-Za-z]{3,9})\s+(\d{1,2}),\s*(\d{4}),?\s*(\d{1,2}):(\d{2})\s*([AP]M)\b/i); + if (match) { + const [, monthText, day, year, hourText, minute, meridiem] = match; + const month = MONTH_INDEX_MAP[monthText.slice(0, 3).toLowerCase()]; + if (month !== undefined) { + let hour = Number(hourText); + if (/pm/i.test(meridiem) && hour < 12) hour += 12; + if (/am/i.test(meridiem) && hour === 12) hour = 0; + return new Date(Number(year), month, Number(day), hour, Number(minute), 0, 0).getTime(); + } + } + + match = text.match(/今天\s*(\d{1,2}):(\d{2})/); + if (match) { + const now = new Date(); + return new Date(now.getFullYear(), now.getMonth(), now.getDate(), Number(match[1]), Number(match[2]), 0, 0).getTime(); + } + + match = text.match(/昨天\s*(\d{1,2}):(\d{2})/); + if (match) { + const now = new Date(); + now.setDate(now.getDate() - 1); + return new Date(now.getFullYear(), now.getMonth(), now.getDate(), Number(match[1]), Number(match[2]), 0, 0).getTime(); + } + + match = text.match(/^(\d{1,2}):(\d{2})$/); + if (match) { + const now = new Date(); + return new Date(now.getFullYear(), now.getMonth(), now.getDate(), Number(match[1]), Number(match[2]), 0, 0).getTime(); + } + + return null; +} + +function extractVerificationCode(text) { + const normalized = String(text || ''); + + const cnMatch = normalized.match(/(?:验证码|代码)[^0-9]{0,16}(\d{6})/i); + if (cnMatch) return cnMatch[1]; + + const enMatch = normalized.match(/(?:verification\s+code|temporary\s+verification\s+code|your\s+chatgpt\s+code|code(?:\s+is)?)[^0-9]{0,16}(\d{6})/i); + if (enMatch) return enMatch[1]; + + const plainMatch = normalized.match(/\b(\d{6})\b/); + if (plainMatch) return plainMatch[1]; + + return null; +} + +function findInboxLink() { + const selectors = [ + 'a[href*="#inbox"]', + 'a[aria-label*="收件箱"]', + 'a[aria-label*="Inbox"]', + ]; + + for (const selector of selectors) { + const candidates = Array.from(document.querySelectorAll(selector)); + const visible = candidates.find(isVisibleElement); + if (visible) return visible; + if (candidates[0]) return candidates[0]; + } + + return Array.from(document.querySelectorAll('a, [role="link"]')).find((element) => { + const text = normalizeText( + element.getAttribute('aria-label') + || element.getAttribute('title') + || element.textContent + ); + return /收件箱|Inbox/i.test(text); + }) || null; +} + +function findRefreshButton() { + const selectors = [ + 'div[role="button"][data-tooltip="刷新"]', + 'div[role="button"][aria-label="刷新"]', + 'div[role="button"][data-tooltip*="刷新"]', + 'div[role="button"][aria-label*="刷新"]', + 'div[role="button"][data-tooltip="Refresh"]', + 'div[role="button"][aria-label="Refresh"]', + 'div[role="button"][data-tooltip*="Refresh"]', + 'div[role="button"][aria-label*="Refresh"]', + 'div[act="20"][role="button"]', + 'div.asf.T-I-J3.J-J5-Ji', + ]; + + for (const selector of selectors) { + const matched = document.querySelector(selector); + const button = matched?.closest?.('[role="button"]') || matched; + if (button && isVisibleElement(button)) { + return button; + } + } + + return Array.from(document.querySelectorAll('div[role="button"], button')).find((element) => { + const text = normalizeText( + element.getAttribute('aria-label') + || element.getAttribute('data-tooltip') + || element.getAttribute('title') + || element.textContent + ); + return /刷新|Refresh/i.test(text); + }) || null; +} + +function collectThreadRows() { + const candidates = [ + ...document.querySelectorAll('tr.zA'), + ...document.querySelectorAll('tr[role="row"]'), + ]; + + const rows = []; + const seenRows = new Set(); + + candidates.forEach((row) => { + if (!row || seenRows.has(row)) return; + seenRows.add(row); + + if (!isDisplayed(row)) return; + + const text = normalizeText(row.textContent || row.innerText || ''); + if (!text) return; + + if ( + row.matches('tr.zA') + || row.querySelector('.bog, .y6, .y2, .afn, [data-thread-id], [data-legacy-thread-id], [data-legacy-last-message-id]') + || /openai|chatgpt|verify|verification|code|验证码/i.test(text) + ) { + rows.push(row); + } + }); + + return rows; +} + +function getRowPreviewText(row) { + const sender = normalizeText( + row.querySelector('.zF, .yP, span[email], [email]')?.textContent + || row.querySelector('[email]')?.getAttribute?.('email') + || '' + ); + + const subject = normalizeText( + row.querySelector('.bog [data-thread-id], .bog [data-legacy-thread-id], .bog, .y6, .bqe')?.textContent + || '' + ); + + const digest = normalizeText( + row.querySelector('.y2, .afn, .a4W, .bog + .y2')?.textContent + || '' + ); + + const timeText = normalizeText( + row.querySelector('td.xW span')?.getAttribute?.('title') + || row.querySelector('td.xW span, td.xW time')?.getAttribute?.('title') + || row.querySelector('td.xW span, td.xW time')?.textContent + || '' + ); + + const fullText = normalizeText(row.textContent || row.innerText || ''); + + return { + sender, + subject, + digest, + timeText, + fullText, + combinedText: normalizeText([sender, subject, digest, timeText, fullText].filter(Boolean).join(' ')), + }; +} + +function getRowTimestamp(row) { + const timeCell = row.querySelector('td.xW span, td.xW time, td.xW [title]'); + const candidates = [ + timeCell?.getAttribute?.('title'), + timeCell?.getAttribute?.('aria-label'), + timeCell?.textContent, + ].filter(Boolean); + + for (const candidate of candidates) { + const parsed = parseGmailTimestampText(candidate); + if (parsed) return parsed; + } + + return null; +} + +function getRowFingerprint(row, index = 0) { + const marker = row.querySelector('[data-thread-id], [data-legacy-thread-id], [data-legacy-last-message-id]'); + const stableId = row.getAttribute('data-thread-id') + || row.getAttribute('data-legacy-thread-id') + || row.getAttribute('data-legacy-last-message-id') + || marker?.getAttribute?.('data-thread-id') + || marker?.getAttribute?.('data-legacy-thread-id') + || marker?.getAttribute?.('data-legacy-last-message-id') + || row.getAttribute('id') + || `row-${index}`; + const preview = getRowPreviewText(row); + return `${stableId}::${preview.subject}::${preview.timeText}`.slice(0, 300); +} + +function getCurrentMailIds(rows = []) { + const ids = new Set(); + const sourceRows = rows.length ? rows : collectThreadRows(); + sourceRows.forEach((row, index) => { + ids.add(getRowFingerprint(row, index)); + }); + return ids; +} + +function rowMatchesFilters(preview, senderFilters, subjectFilters) { + const senderText = normalizeText(preview.sender).toLowerCase(); + const subjectText = normalizeText(preview.subject).toLowerCase(); + const combinedText = normalizeText(preview.combinedText).toLowerCase(); + + const senderMatch = senderFilters.some((filter) => { + const value = String(filter || '').toLowerCase(); + return value && (senderText.includes(value) || combinedText.includes(value)); + }); + + const subjectMatch = subjectFilters.some((filter) => { + const value = String(filter || '').toLowerCase(); + return value && (subjectText.includes(value) || combinedText.includes(value)); + }); + + return senderMatch || subjectMatch; +} + +async function ensureInboxReady(step) { + if (!/#inbox/i.test(location.href)) { + const inboxLink = findInboxLink(); + if (inboxLink) { + simulateClick(inboxLink); + await sleep(800); + log(`步骤 ${step}:已切回 Gmail 收件箱。`); + } else { + location.hash = '#inbox'; + await sleep(800); + } + } + + for (let i = 0; i < 20; i++) { + const rows = collectThreadRows(); + if (rows.length > 0) { + return rows; + } + await sleep(400); + } + + return []; +} + +async function refreshInbox(step) { + const refreshButton = findRefreshButton(); + if (refreshButton) { + simulateClick(refreshButton); + log(`步骤 ${step}:已点击 Gmail 刷新。`); + await sleep(1500); + return; + } + + const inboxLink = findInboxLink(); + if (inboxLink) { + simulateClick(inboxLink); + log(`步骤 ${step}:未找到刷新按钮,已重新进入收件箱。`); + await sleep(1200); + return; + } + + location.reload(); + log(`步骤 ${step}:未找到刷新按钮,已直接刷新页面。`); + await sleep(2500); +} + +async function returnToInbox() { + if (/#inbox/i.test(location.href) && collectThreadRows().length > 0) { + return; + } + + const inboxLink = findInboxLink(); + if (inboxLink) { + simulateClick(inboxLink); + } else { + location.hash = '#inbox'; + } + + for (let i = 0; i < 20; i++) { + if (collectThreadRows().length > 0) { + return; + } + await sleep(250); + } +} + +async function openRowAndGetMessageText(row) { + simulateClick(row); + + for (let i = 0; i < 20; i++) { + const messageContainer = document.querySelector('div[role="main"] .a3s, div[role="main"] [data-message-id], h2[data-thread-perm-id]'); + if (messageContainer || !/#inbox/i.test(location.href)) { + break; + } + await sleep(250); + } + + await sleep(900); + const main = document.querySelector('div[role="main"]'); + const text = normalizeText(main?.innerText || document.body?.innerText || document.body?.textContent || ''); + await returnToInbox(); + return text; +} + +async function handlePollEmail(step, payload) { + const { + senderFilters = [], + subjectFilters = [], + maxAttempts = 5, + intervalMs = 3000, + filterAfterTimestamp = 0, + excludeCodes = [], + targetEmail = '', + } = payload || {}; + + const excludedCodeSet = new Set(excludeCodes.filter(Boolean)); + const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0); + + log(`步骤 ${step}:开始轮询 Gmail(最多 ${maxAttempts} 次)`); + if (filterAfterMinute) { + log(`步骤 ${step}:仅尝试 ${new Date(filterAfterMinute).toLocaleString('zh-CN', { hour12: false })} 及之后时间的邮件。`); + } + + let initialRows = await ensureInboxReady(step); + if (!initialRows.length) { + await refreshInbox(step); + initialRows = await ensureInboxReady(step); + } + + if (!initialRows.length) { + throw new Error('Gmail 收件箱列表未加载完成,请确认当前已打开 Gmail 收件箱。'); + } + + const existingMailIds = getCurrentMailIds(initialRows); + log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`); + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + log(`步骤 ${step}:正在轮询 Gmail,第 ${attempt}/${maxAttempts} 次`); + + if (attempt > 1) { + await refreshInbox(step); + } + + const rows = collectThreadRows(); + const useFallback = attempt > GMAIL_FALLBACK_AFTER; + + for (let index = 0; index < rows.length; index++) { + const row = rows[index]; + const rowId = getRowFingerprint(row, index); + const rowTimestamp = getRowTimestamp(row); + const rowMinute = normalizeMinuteTimestamp(rowTimestamp || 0); + const passesTimeFilter = !filterAfterMinute || (rowMinute && rowMinute >= filterAfterMinute); + const shouldBypassOldSnapshot = Boolean(filterAfterMinute && passesTimeFilter && rowMinute > 0); + + if (!passesTimeFilter) { + continue; + } + + if (!useFallback && !shouldBypassOldSnapshot && existingMailIds.has(rowId)) { + continue; + } + + const preview = getRowPreviewText(row); + if (!rowMatchesFilters(preview, senderFilters, subjectFilters)) { + continue; + } + + const previewTargetState = getTargetEmailMatchState(preview.combinedText, targetEmail); + const previewEmails = extractEmails(preview.combinedText); + if (targetEmail && previewEmails.length > 0 && !previewTargetState.matches) { + continue; + } + + const previewCode = extractVerificationCode(preview.combinedText); + if (previewCode && previewTargetState.matches) { + if (excludedCodeSet.has(previewCode)) { + log(`步骤 ${step}:跳过排除的验证码:${previewCode}`, 'info'); + continue; + } + if (seenCodes.has(previewCode)) { + log(`步骤 ${step}:跳过已处理过的验证码:${previewCode}`, 'info'); + continue; + } + seenCodes.add(previewCode); + persistSeenCodes(); + const source = useFallback && existingMailIds.has(rowId) ? '回退匹配邮件' : '新邮件'; + const timeLabel = rowTimestamp ? `,时间:${new Date(rowTimestamp).toLocaleString('zh-CN', { hour12: false })}` : ''; + log(`步骤 ${step}:已在 Gmail 找到验证码:${previewCode}(来源:${source}${timeLabel})`, 'ok'); + return { + ok: true, + code: previewCode, + emailTimestamp: Date.now(), + mailId: rowId, + }; + } + + const openedText = await openRowAndGetMessageText(row); + const openedTargetState = getTargetEmailMatchState(openedText, targetEmail); + if (targetEmail && openedTargetState.hasExplicitEmail && !openedTargetState.matches) { + continue; + } + + const bodyCode = extractVerificationCode(openedText); + if (!bodyCode) { + continue; + } + if (excludedCodeSet.has(bodyCode)) { + log(`步骤 ${step}:跳过排除的验证码:${bodyCode}`, 'info'); + continue; + } + if (seenCodes.has(bodyCode)) { + log(`步骤 ${step}:跳过已处理过的验证码:${bodyCode}`, 'info'); + continue; + } + seenCodes.add(bodyCode); + persistSeenCodes(); + const source = useFallback && existingMailIds.has(rowId) ? '回退匹配邮件正文' : '新邮件正文'; + const timeLabel = rowTimestamp ? `,时间:${new Date(rowTimestamp).toLocaleString('zh-CN', { hour12: false })}` : ''; + log(`步骤 ${step}:已在 Gmail 正文中找到验证码:${bodyCode}(来源:${source}${timeLabel})`, 'ok'); + return { + ok: true, + code: bodyCode, + emailTimestamp: Date.now(), + mailId: rowId, + }; + } + + if (attempt === GMAIL_FALLBACK_AFTER + 1) { + log(`步骤 ${step}:连续 ${GMAIL_FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn'); + } + + if (attempt < maxAttempts) { + await sleep(intervalMs); + } + } + + throw new Error( + `${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 Gmail 中找到匹配邮件。请手动检查 Gmail 收件箱。` + ); +} + +} diff --git a/content/utils.js b/content/utils.js index 205bde5..9c30d4a 100644 --- a/content/utils.js +++ b/content/utils.js @@ -9,6 +9,7 @@ const SCRIPT_SOURCE = (() => { if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page'; if (hostname === 'mail.qq.com' || hostname === 'wx.mail.qq.com') return 'qq-mail'; if (hostname === 'mail.163.com' || hostname.endsWith('.mail.163.com') || hostname === 'webmail.vip.163.com') return 'mail-163'; + if (hostname === 'mail.google.com') return 'gmail-mail'; if (url.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail'; if (url.includes('chatgpt.com')) return 'chatgpt'; if (url.includes("2925.com")) return "mail-2925"; @@ -405,7 +406,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' || SCRIPT_SOURCE === 'mail-2925' || SCRIPT_SOURCE === 'inbucket-mail') && window !== window.top; +const _isMailChildFrame = (SCRIPT_SOURCE === 'qq-mail' || SCRIPT_SOURCE === 'mail-163' || SCRIPT_SOURCE === 'gmail-mail' || SCRIPT_SOURCE === 'mail-2925' || SCRIPT_SOURCE === 'inbucket-mail') && window !== window.top; if (!_isMailChildFrame) { reportReady(); } diff --git a/manifest.json b/manifest.json index 9292ff0..03063ec 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "多页面自动化", - "version": "9.0.0", + "version": "9.2.0", "description": "用于自动执行多步骤 OAuth 注册流程", "permissions": [ "sidePanel", @@ -99,4 +99,4 @@ "48": "icons/icon48.png", "128": "icons/icon128.png" } -} +} \ No newline at end of file diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 133a64a..190ec3a 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -160,6 +160,7 @@ + diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 55537d7..643b907 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -199,6 +199,7 @@ const AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS = 15; const AUTO_RUN_FALLBACK_RISK_RECOMMENDED_THREAD_INTERVAL_MINUTES = 5; const HOTMAIL_SERVICE_MODE_REMOTE = 'remote'; const HOTMAIL_SERVICE_MODE_LOCAL = 'local'; +const GMAIL_PROVIDER = 'gmail'; const LUCKMAIL_PROVIDER = 'luckmail-api'; const DEFAULT_LUCKMAIL_BASE_URL = 'https://mails.luckyous.com'; const DEFAULT_LUCKMAIL_EMAIL_TYPE = 'ms_graph'; @@ -263,6 +264,11 @@ let luckmailRefreshQueued = false; btnAutoCancelSchedule?.remove(); const MAIL_PROVIDER_LOGIN_CONFIGS = { + [GMAIL_PROVIDER]: { + label: 'Gmail 邮箱', + url: 'https://mail.google.com/mail/u/0/#inbox', + buttonLabel: '登录', + }, '163': { label: '163 邮箱', url: 'https://mail.163.com/', @@ -309,9 +315,33 @@ const LOG_LEVEL_LABELS = { error: '错误', }; -function usesGeneratedAliasMailProvider(provider) { - return String(provider || '').trim() === '2925' - && getSelectedMail2925Mode() === MAIL_2925_MODE_PROVIDE; +function usesGeneratedAliasMailProvider(provider, mail2925Mode = getSelectedMail2925Mode()) { + const normalizedProvider = String(provider || '').trim().toLowerCase(); + if (normalizedProvider === GMAIL_PROVIDER) { + return true; + } + return normalizedProvider === '2925' + && normalizeMail2925Mode(mail2925Mode) === MAIL_2925_MODE_PROVIDE; +} + +function parseGmailBaseEmail(rawValue = '') { + const value = String(rawValue || '').trim().toLowerCase(); + const match = value.match(/^([^@\s+]+)@((?:gmail|googlemail)\.com)$/i); + if (!match) return null; + return { + localPart: match[1], + domain: match[2].toLowerCase(), + }; +} + +function isManagedGmailAlias(value, baseEmail) { + const parsedBase = parseGmailBaseEmail(baseEmail); + if (!parsedBase) return false; + + const match = String(value || '').trim().toLowerCase().match(/^([^@\s+]+)(?:\+[^@\s]+)?@((?:gmail|googlemail)\.com)$/i); + if (!match) return false; + + return match[1] === parsedBase.localPart && match[2] === parsedBase.domain; } function showToast(message, type = 'error', duration = 4000) { @@ -1413,7 +1443,7 @@ function applySettingsState(state) { inputSub2ApiPassword.value = state?.sub2apiPassword || ''; inputSub2ApiGroup.value = state?.sub2apiGroupName || ''; const restoredMailProvider = isCustomMailProvider(state?.mailProvider) - || ['hotmail-api', 'luckmail-api', '163', '163-vip', 'qq', 'inbucket', '2925', 'cloudflare-temp-email'].includes(String(state?.mailProvider || '').trim()) + || ['hotmail-api', GMAIL_PROVIDER, 'luckmail-api', '163', '163-vip', 'qq', 'inbucket', '2925', 'cloudflare-temp-email'].includes(String(state?.mailProvider || '').trim()) ? String(state?.mailProvider || '163').trim() : (String(state?.emailGenerator || '').trim().toLowerCase() === 'custom' || String(state?.emailGenerator || '').trim().toLowerCase() === 'manual' @@ -2292,13 +2322,18 @@ function isCurrentEmailManagedByGeneratedAlias( mail2925Mode = latestState?.mail2925Mode ) { const normalizedProvider = String(provider || '').trim(); - if (!(normalizedProvider === '2925' && normalizeMail2925Mode(mail2925Mode) === MAIL_2925_MODE_PROVIDE)) { + if (!usesGeneratedAliasMailProvider(normalizedProvider, mail2925Mode)) { return false; } const inputEmailValue = String(inputEmail.value || '').trim().toLowerCase(); const stateEmailValue = String(state?.email || '').trim().toLowerCase(); + if (normalizedProvider === GMAIL_PROVIDER) { + const baseEmail = String(state?.emailPrefix || inputEmailPrefix.value || '').trim(); + return isManagedGmailAlias(inputEmailValue, baseEmail) || isManagedGmailAlias(stateEmailValue, baseEmail); + } + if (normalizedProvider === '2925') { return inputEmailValue.endsWith('@2925.com') || stateEmailValue.endsWith('@2925.com'); } @@ -2306,6 +2341,26 @@ function isCurrentEmailManagedByGeneratedAlias( return false; } +async function maybeClearGeneratedAliasAfterEmailPrefixChange() { + const provider = selectMailProvider.value; + const mail2925Mode = latestState?.mail2925Mode; + if (!usesGeneratedAliasMailProvider(provider, mail2925Mode)) { + return; + } + + const previousPrefix = String(latestState?.emailPrefix || '').trim(); + const nextPrefix = inputEmailPrefix.value.trim(); + if (previousPrefix === nextPrefix) { + return; + } + + if (!isCurrentEmailManagedByGeneratedAlias(provider, latestState, mail2925Mode)) { + return; + } + + await clearRegistrationEmail({ silent: true }); +} + function updateMailLoginButtonState() { if (!btnMailLogin) { return; @@ -2527,8 +2582,9 @@ function renderHotmailAccounts() { function updateMailProviderUI() { const use2925 = selectMailProvider.value === '2925'; + const useGmail = selectMailProvider.value === GMAIL_PROVIDER; const mail2925Mode = getSelectedMail2925Mode(); - const useGeneratedAlias = use2925 && mail2925Mode === MAIL_2925_MODE_PROVIDE; + const useGeneratedAlias = usesGeneratedAliasMailProvider(selectMailProvider.value, mail2925Mode); const useInbucket = selectMailProvider.value === 'inbucket'; const useHotmail = selectMailProvider.value === 'hotmail-api'; const useLuckmail = isLuckmailProvider(); @@ -2590,6 +2646,10 @@ function updateMailProviderUI() { labelEmailPrefix.textContent = '邮箱前缀'; inputEmailPrefix.placeholder = '例如 abc'; selectEmailGenerator.disabled = useHotmail || useLuckmail || useGeneratedAlias || useCustomEmail; + if (useGmail) { + labelEmailPrefix.textContent = 'Gmail 原邮箱'; + inputEmailPrefix.placeholder = '例如 yourname@gmail.com'; + } if (rowHotmailServiceMode) { rowHotmailServiceMode.style.display = useHotmail ? '' : 'none'; } @@ -2607,6 +2667,9 @@ function updateMailProviderUI() { : (useLuckmail ? '步骤 3 自动购买 LuckMail 邮箱并回填' : (useGeneratedAlias ? '步骤 3 自动生成 2925 邮箱并回填' : uiCopy.placeholder)); + if (useGmail && useGeneratedAlias) { + inputEmail.placeholder = '步骤 3 自动生成 Gmail +tag 邮箱并回填'; + } btnFetchEmail.disabled = useGeneratedAlias || useLuckmail || useCustomEmail || isAutoRunLockedPhase(); if (!btnFetchEmail.disabled) { btnFetchEmail.textContent = uiCopy.buttonLabel; @@ -2620,6 +2683,9 @@ function updateMailProviderUI() { ? '步骤 3 会自动生成邮箱,无需手动获取' : (useCustomEmail ? '请先填写自定义注册邮箱,成功一轮后会自动清空' : `先自动获取${uiCopy.label},或手动粘贴邮箱后再继续`))); } + if (autoHintText && useGmail && useGeneratedAlias) { + autoHintText.textContent = '请先填写 Gmail 原邮箱,步骤 3 会自动生成 Gmail +tag 地址'; + } if (useHotmail) { inputEmail.value = getCurrentHotmailEmail(); } else if (useLuckmail) { @@ -3581,7 +3647,7 @@ document.querySelectorAll('.step-btn').forEach(btn => { } else if (usesGeneratedAliasMailProvider(selectMailProvider.value)) { const emailPrefix = inputEmailPrefix.value.trim(); if (!emailPrefix) { - showToast('请先填写 2925 邮箱前缀。', 'warn'); + showToast(selectMailProvider.value === GMAIL_PROVIDER ? '请先填写 Gmail 原邮箱。' : '请先填写 2925 邮箱前缀。', 'warn'); return; } const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, emailPrefix } }); @@ -4345,8 +4411,7 @@ selectMailProvider.addEventListener('change', async () => { const leavingGeneratedAlias = ( previousProvider !== nextProvider || (previousProvider === '2925' && normalizeMail2925Mode(previousMail2925Mode) !== getSelectedMail2925Mode()) - ) && previousProvider === '2925' - && normalizeMail2925Mode(previousMail2925Mode) === MAIL_2925_MODE_PROVIDE + ) && usesGeneratedAliasMailProvider(previousProvider, previousMail2925Mode) && isCurrentEmailManagedByGeneratedAlias(previousProvider, latestState, previousMail2925Mode); if (leavingHotmail || leavingLuckmail || leavingGeneratedAlias) { await clearRegistrationEmail({ silent: true }).catch(() => { }); @@ -4513,10 +4578,12 @@ inputSub2ApiGroup.addEventListener('blur', () => { }); inputEmailPrefix.addEventListener('input', () => { + maybeClearGeneratedAliasAfterEmailPrefixChange().catch(() => { }); markSettingsDirty(true); scheduleSettingsAutoSave(); }); inputEmailPrefix.addEventListener('blur', () => { + maybeClearGeneratedAliasAfterEmailPrefixChange().catch(() => {}); saveSettings({ silent: true }).catch(() => {}); });