From 77b49e084e5547392f55fdb2becd0629a6a68875 Mon Sep 17 00:00:00 2001 From: sk856 <1793174357@qq.com> Date: Thu, 21 May 2026 01:04:47 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Yahoo=E9=82=AE=E7=AE=B1?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概述 本 PR 新增 Yahoo 邮箱服务,支持通过 Yahoo 邮箱自动创建临时别名邮箱,并将返回的最终邮箱地址回填到注册流程中,同时接入 OpenAI 与 Kiro 的验证码读取链路。 ## 主要变更 新增 Yahoo Mail 工具与内容脚本: - 新增 `yahoo-utils.js` - 新增 `content/yahoo-mail.js` - 支持 Yahoo 邮箱登录态检测和账号密码自动登录 - 支持创建 Yahoo 临时别名邮箱 侧边栏新增 Yahoo 邮箱服务选项: - 新增 Yahoo 邮箱 provider - 新增 Yahoo 临时邮箱 generator - 新增 Yahoo 账号 / 密码配置卡片 - 登录按钮通过后台打开 Yahoo,并可自动填写账号密码 - Yahoo 选项提示默认邮箱页面需要设置为 ALL 注册/自动运行流程集成: - 手动“获取邮箱”、步骤 2、自动运行均可创建 Yahoo 临时别名并回填 - 创建别名完成后恢复到 ChatGPT 注册页继续提交邮箱 - 步骤 4/8 验证码轮询支持 Yahoo 邮箱 - Yahoo 取码使用收件箱顶部邮件优先策略 - 顶部邮件未直接露出验证码时,会点进邮件正文读取 - Yahoo 验证码流程不删除收件箱邮件 Kiro 流程集成: - Kiro 注册邮箱验证码接入 Yahoo 专用取码链路 - Kiro 桌面授权验证码接入 Yahoo 专用取码链路 - 如果 1 分钟内未收到 Kiro 验证码,会回到 Kiro 页面点击重新发送,再切回 Yahoo 收件箱读取新邮件 - 支持 Yahoo 顶部 AWS/Kiro 邮件识别与正文读码 兼容现有逻辑: - 保留现有侧边栏、HeroSMS、source registry、operation delay 等行为 - 补充 Yahoo provider、verification、Kiro、sidepanel、source registry 相关测试覆盖 ## 测试 - `node --check background.js` - `node --check background/generated-email-helpers.js` - `node --check background/verification-flow.js` - `node --check background/kiro/register-runner.js` - `node --check background/kiro/desktop-authorize-runner.js` - `node --check content/yahoo-mail.js` - `node --check content/kiro/register-page.js` - `node --check sidepanel/sidepanel.js` - Yahoo/Kiro 相关测试:69/69 通过 - 侧边栏/source registry/operation delay 回归测试:33/33 通过 --- background.js | 139 +- background/generated-email-helpers.js | 51 + background/kiro/desktop-authorize-runner.js | 123 + background/kiro/register-runner.js | 318 ++- background/logging-status.js | 1 + background/navigation-utils.js | 2 + background/steps/fetch-login-code.js | 56 +- background/steps/fetch-signup-code.js | 60 +- background/steps/submit-signup-email.js | 14 +- background/verification-flow.js | 473 ++- content/kiro/register-page.js | 74 +- content/yahoo-mail.js | 2683 ++++++++++++++++++ flows/openai/mail-rules.js | 12 +- mail-provider-utils.js | 14 + manifest.json | 13 + shared/source-registry.js | 17 + sidepanel/sidepanel.html | 17 + sidepanel/sidepanel.js | 103 +- tests/background-step8-yahoo-polling.test.js | 119 + tests/background-yahoo-provider.test.js | 243 ++ tests/kiro-yahoo-mail-command.test.js | 42 + tests/sidepanel-yahoo-provider.test.js | 99 + tests/signup-page-yahoo-resend-guard.test.js | 11 + tests/source-registry-yahoo.test.js | 32 + tests/verification-flow-yahoo.test.js | 101 + tests/yahoo-mail-content.test.js | 349 +++ tests/yahoo-mail-delete-flow.test.js | 17 + yahoo-utils.js | 121 + 28 files changed, 5264 insertions(+), 40 deletions(-) create mode 100644 content/yahoo-mail.js create mode 100644 tests/background-step8-yahoo-polling.test.js create mode 100644 tests/background-yahoo-provider.test.js create mode 100644 tests/kiro-yahoo-mail-command.test.js create mode 100644 tests/sidepanel-yahoo-provider.test.js create mode 100644 tests/signup-page-yahoo-resend-guard.test.js create mode 100644 tests/source-registry-yahoo.test.js create mode 100644 tests/verification-flow-yahoo.test.js create mode 100644 tests/yahoo-mail-content.test.js create mode 100644 tests/yahoo-mail-delete-flow.test.js create mode 100644 yahoo-utils.js diff --git a/background.js b/background.js index 82021f6a..c5aa64bf 100644 --- a/background.js +++ b/background.js @@ -74,6 +74,7 @@ importScripts( 'background/cloudmail-provider.js', 'yyds-mail-utils.js', 'background/yyds-mail-provider.js', + 'yahoo-utils.js', 'icloud-utils.js', 'mail-provider-utils.js', 'content/activation-utils.js' @@ -390,6 +391,10 @@ const { normalizeYydsMailMessageDetail, normalizeYydsMailMessages, } = self.YydsMailUtils; +const { + YAHOO_PROVIDER = 'yahoo', + YAHOO_GENERATOR = 'yahoo', +} = self.YahooUtils || {}; const { findIcloudAliasByEmail, getConfiguredIcloudHostPreference, @@ -1297,6 +1302,8 @@ const PERSISTED_SETTING_DEFAULTS = { customMailProviderPool: [], customEmailPool: [], customEmailPoolEntries: [], + yahooMailEmail: '', + yahooMailPassword: '', autoDeleteUsedIcloudAlias: false, icloudHostPreference: 'auto', icloudTargetMailboxType: 'icloud-inbox', @@ -2415,6 +2422,9 @@ function normalizeEmailGenerator(value = '') { const yydsMailGenerator = typeof YYDS_MAIL_GENERATOR === 'string' ? YYDS_MAIL_GENERATOR : 'yyds-mail'; + const yahooGenerator = typeof YAHOO_GENERATOR === 'string' + ? YAHOO_GENERATOR + : 'yahoo'; if (normalized === 'custom' || normalized === 'manual') { return 'custom'; } @@ -2430,6 +2440,7 @@ function normalizeEmailGenerator(value = '') { if (normalized === 'cloudflare') return 'cloudflare'; if (normalized === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return CLOUDFLARE_TEMP_EMAIL_GENERATOR; if (normalized === 'cloudmail') return 'cloudmail'; + if (normalized === yahooGenerator) return yahooGenerator; if (normalized === yydsMailGenerator) return yydsMailGenerator; return 'duck'; } @@ -2669,10 +2680,14 @@ function normalizeMailProvider(value = '') { const yydsMailProvider = typeof YYDS_MAIL_PROVIDER === 'string' ? YYDS_MAIL_PROVIDER : 'yyds-mail'; + const yahooProvider = typeof YAHOO_PROVIDER === 'string' + ? YAHOO_PROVIDER + : 'yahoo'; switch (normalized) { case 'custom': case ICLOUD_PROVIDER: case GMAIL_PROVIDER: + case yahooProvider: case HOTMAIL_PROVIDER: case LUCKMAIL_PROVIDER: case CLOUDFLARE_TEMP_EMAIL_PROVIDER: @@ -3334,6 +3349,10 @@ function normalizePersistentSettingValue(key, value) { return normalizeCustomEmailPool(value); case 'customEmailPoolEntries': return normalizeCustomEmailPoolEntryObjects(value); + case 'yahooMailEmail': + return String(value || '').trim(); + case 'yahooMailPassword': + return String(value || ''); case 'autoDeleteUsedIcloudAlias': case 'accountRunHistoryTextEnabled': case 'cloudflareTempEmailUseRandomSubdomain': @@ -8671,6 +8690,8 @@ function matchesSourceUrlFamily(source, candidateUrl, referenceUrl) { return is163MailHost(candidate.hostname); case 'gmail-mail': return candidate.hostname === 'mail.google.com'; + case 'yahoo-mail': + return candidate.hostname === 'mail.yahoo.com'; case 'icloud-mail': return candidate.hostname === 'www.icloud.com' || candidate.hostname === 'www.icloud.com.cn'; case 'inbucket-mail': @@ -8974,6 +8995,7 @@ function getSourceLabel(source) { const labels = { 'openai-auth': '认证页', 'gmail-mail': 'Gmail 邮箱', + 'yahoo-mail': 'Yahoo 邮箱', 'sidepanel': '侧边栏', 'signup-page': '认证页', 'vps-panel': 'CPA 面板', @@ -11579,6 +11601,9 @@ function getEmailGeneratorLabel(generator) { const yydsMailGenerator = typeof YYDS_MAIL_GENERATOR === 'string' ? YYDS_MAIL_GENERATOR : 'yyds-mail'; + const yahooGenerator = typeof YAHOO_GENERATOR === 'string' + ? YAHOO_GENERATOR + : 'yahoo'; if (generator === 'custom') { return '自定义邮箱'; } @@ -11594,6 +11619,7 @@ function getEmailGeneratorLabel(generator) { if (generator === 'cloudflare') return 'Cloudflare 邮箱'; if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email'; if (generator === CLOUD_MAIL_GENERATOR) return 'Cloud Mail'; + if (generator === yahooGenerator) return 'Yahoo 临时邮箱'; if (generator === yydsMailGenerator) return 'YYDS Mail'; return 'Duck 邮箱'; } @@ -11769,6 +11795,10 @@ async function fetchDuckEmail(options = {}) { return generatedEmailHelpers.fetchDuckEmail(options); } +async function fetchYahooTempEmail(state, options = {}) { + return generatedEmailHelpers.fetchYahooTempEmail(state, options); +} + async function fetchGeneratedEmail(state, options = {}) { const currentState = state || await getState(); const yydsMailProvider = typeof YYDS_MAIL_PROVIDER === 'string' @@ -12895,7 +12925,15 @@ async function runAutoSequenceFromNodeGraph(startNodeId, context = {}) { if (resolvedSignupMethod === SIGNUP_METHOD_PHONE) { await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:本轮注册方式为手机号注册,将跳过邮箱预获取 ===`, 'info'); } else { - await ensureAutoEmailReady(targetRun, totalRuns, attemptRuns); + const readyEmail = await ensureAutoEmailReady(targetRun, totalRuns, attemptRuns); + const normalizedReadyEmail = String(readyEmail || '').trim(); + if (normalizedReadyEmail) { + const latestEmailState = await getState(); + const normalizedStateEmail = String(latestEmailState?.email || '').trim(); + if (normalizedStateEmail.toLowerCase() !== normalizedReadyEmail.toLowerCase()) { + await setEmailState(normalizedReadyEmail); + } + } } await executeNodeAndWait('submit-signup-email', getAutoRunNodeDelayMs('submit-signup-email')); }); @@ -13318,12 +13356,14 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create getHotmailVerificationPollConfig, getHotmailVerificationRequestTimestamp, handleMail2925LimitReachedError, + ensureContentScriptReadyOnTab, getState, getTabId, HOTMAIL_PROVIDER, isMail2925LimitReachedError, isRetryableContentScriptTransportError, isStopError, + isTabAlive, LUCKMAIL_PROVIDER, YYDS_MAIL_PROVIDER, MAIL_2925_VERIFICATION_INTERVAL_MS, @@ -13333,6 +13373,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create pollHotmailVerificationCode, pollLuckmailVerificationCode, pollYydsMailVerificationCode, + reuseOrCreateTab, sendToContentScript, sendToContentScriptResilient, sendToMailContentScriptResilient, @@ -13903,6 +13944,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter executeNodeViaCompletionSignal, exportSettingsBundle, fetchGeneratedEmail, + openMailProviderLogin, refreshGpcCardBalance, finalizePhoneActivationAfterSuccessfulFlow, testKiroRsConnection: async (baseUrl, apiKey) => { @@ -14122,8 +14164,17 @@ async function ensureSignupPostEmailPageReadyInTab(tabId, step = 2, options = {} return signupFlowHelpers.ensureSignupPostEmailPageReadyInTab(tabId, step, options); } -async function resolveSignupEmailForFlow(state) { - return signupFlowHelpers.resolveSignupEmailForFlow(state); +async function resolveSignupEmailForFlow(state, options = {}) { + let latestState = null; + try { + latestState = await getState(); + } catch { + latestState = null; + } + return signupFlowHelpers.resolveSignupEmailForFlow({ + ...(state || {}), + ...(latestState || {}), + }, options); } // ============================================================ @@ -14159,12 +14210,26 @@ function getMailConfig(state) { const yydsMailProvider = typeof YYDS_MAIL_PROVIDER === 'string' ? YYDS_MAIL_PROVIDER : 'yyds-mail'; + const yahooProvider = typeof YAHOO_PROVIDER === 'string' + ? YAHOO_PROVIDER + : 'yahoo'; if (provider === 'custom') { return { provider: 'custom', label: '自定义邮箱' }; } if (provider === HOTMAIL_PROVIDER) { return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(API对接/本地助手)' }; } + if (provider === yahooProvider) { + return { + provider: yahooProvider, + source: 'yahoo-mail', + url: 'https://mail.yahoo.com/n/inbox/all?listFilter=ALL_INBOX', + label: 'Yahoo 邮箱', + navigateOnReuse: true, + inject: ['content/activation-utils.js', 'shared/source-registry.js', 'content/utils.js', 'content/yahoo-mail.js'], + injectSource: 'yahoo-mail', + }; + } if (provider === ICLOUD_PROVIDER) { const configuredHost = getConfiguredIcloudHostPreference(state) || normalizeIcloudHost(state?.preferredIcloudHost) @@ -14250,6 +14315,74 @@ function getMailConfig(state) { return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ 邮箱' }; } +async function openMailProviderLogin(payload = {}) { + const state = await getState(); + const provider = String(payload.provider || state.mailProvider || '').trim() || state.mailProvider || 'qq'; + const mail = getMailConfig({ + ...state, + mailProvider: provider, + }); + + if (mail?.error) { + throw new Error(mail.error); + } + + const requestedUrl = String(payload.url || '').trim(); + const targetUrl = requestedUrl || String(mail?.url || '').trim(); + if (!targetUrl) { + throw new Error(`${mail?.label || provider || '当前邮箱服务'}没有可跳转的登录页。`); + } + + if (mail?.source) { + const tabId = await reuseOrCreateTab(mail.source, targetUrl, { + inject: mail.inject, + injectSource: mail.injectSource, + navigateOnReuse: mail.navigateOnReuse !== false, + }); + if (payload.active !== false && Number.isInteger(tabId)) { + await chrome.tabs.update(tabId, { active: true }).catch(() => { }); + } + const yahooMailEmail = String(payload.yahooMailEmail || state.yahooMailEmail || '').trim(); + const yahooMailPassword = String(payload.yahooMailPassword || state.yahooMailPassword || ''); + if (provider === YAHOO_PROVIDER && yahooMailEmail && yahooMailPassword) { + try { + const loginResult = await sendToContentScriptResilient(mail.source, { + type: 'YAHOO_LOGIN_WITH_CREDENTIALS', + payload: { + email: yahooMailEmail, + password: yahooMailPassword, + }, + }, { + responseTimeoutMs: 45000, + logLabel: '填写 Yahoo 邮箱登录账号密码', + }); + if (loginResult?.submitted) { + await addLog('Yahoo 邮箱:已自动填写登录账号密码并提交。', 'ok'); + } + } catch (err) { + await addLog(`Yahoo 邮箱:自动填写登录账号密码失败,请在打开的页面手动完成登录。原因:${getErrorMessage(err)}`, 'warn'); + } + } + await addLog(`${mail.label || '邮箱'}:已打开登录/收件箱页面。`, 'info'); + return { + ok: true, + provider, + source: mail.source, + tabId, + url: targetUrl, + }; + } + + const tab = await chrome.tabs.create({ url: targetUrl, active: true }); + await addLog(`${mail?.label || '邮箱'}:已打开登录页面。`, 'info'); + return { + ok: true, + provider, + tabId: tab?.id ?? null, + url: targetUrl, + }; +} + function normalizeInbucketOrigin(rawValue) { const value = (rawValue || '').trim(); if (!value) return ''; diff --git a/background/generated-email-helpers.js b/background/generated-email-helpers.js index 81367d5c..8ed9ca62 100644 --- a/background/generated-email-helpers.js +++ b/background/generated-email-helpers.js @@ -221,6 +221,53 @@ return result.email; } + async function fetchYahooTempEmail(state, options = {}) { + throwIfStopped(); + const latestState = state || await getState(); + const yahooSettingsUrl = 'https://mail.yahoo.com/n/settings/2'; + await addLog('Yahoo 临时邮箱:正在打开设置页并准备创建新别名...', 'info'); + + const yahooTabOptions = { + navigateOnReuse: true, + inject: ['content/activation-utils.js', 'shared/source-registry.js', 'content/utils.js', 'content/yahoo-mail.js'], + injectSource: 'yahoo-mail', + }; + await reuseOrCreateTab('yahoo-mail', yahooSettingsUrl, yahooTabOptions); + + const createMessage = { + type: 'YAHOO_CREATE_TEMP_ALIAS', + source: 'background', + payload: { + prefix: String(options.prefix || latestState.emailPrefix || '').trim().toLowerCase(), + }, + }; + const sendCreateRequest = async () => sendToContentScript('yahoo-mail', createMessage, { + responseTimeoutMs: 120000, + }); + + let result = await sendCreateRequest(); + if (result?.error && shouldRetryYahooAliasCreationInForeground(result.error)) { + await addLog(`Yahoo 临时邮箱:后台创建需要前台页面交互,正在自动切换到 Yahoo 设置页后重试。原因:${result.error}`, 'warn'); + await reuseOrCreateTab('yahoo-mail', yahooSettingsUrl, yahooTabOptions); + result = await sendCreateRequest(); + } + + if (result?.error) { + throw new Error(result.error); + } + if (!result?.email) { + throw new Error('Yahoo 临时邮箱创建后未返回可用邮箱地址。'); + } + + await setEmailState(result.email); + await addLog(`Yahoo 临时邮箱:已创建 ${result.email}`, 'ok'); + return result.email; + } + + function shouldRetryYahooAliasCreationInForeground(message = '') { + return /未找到“新建一次性电子邮箱|未找到“新建一次性电子邮箱\/地址|点击“添加”后未出现创建面板|未找到一次性邮箱输入框|未找到保存\/创建按钮|创建面板/.test(String(message || '')); + } + async function fetchCustomEmailPoolEmail(state, options = {}) { throwIfStopped(); const latestState = state || await getState(); @@ -340,6 +387,9 @@ if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) { return fetchCloudflareTempEmailAddress(mergedState, options); } + if (generator === 'yahoo') { + return fetchYahooTempEmail(mergedState, options); + } const resolvedDuckBaselineEmail = typeof getRegistrationEmailBaseline === 'function' ? getRegistrationEmailBaseline(mergedState, { preferredEmail: options.currentEmail, @@ -365,6 +415,7 @@ fetchCloudflareTempEmailAddress, fetchDuckEmail, fetchGeneratedEmail, + fetchYahooTempEmail, generateCloudflareAliasLocalPart, requestCloudflareTempEmailJson, }; diff --git a/background/kiro/desktop-authorize-runner.js b/background/kiro/desktop-authorize-runner.js index fb95aa95..bc5ec92a 100644 --- a/background/kiro/desktop-authorize-runner.js +++ b/background/kiro/desktop-authorize-runner.js @@ -913,6 +913,125 @@ return Math.max(45000, maxAttempts * intervalMs + 25000); } + async function pollYahooKiroDesktopOtpCode(step, state = {}, mail = {}, pollPayload = {}, nodeId = '') { + const maxAttempts = Math.max(1, Math.floor(Number(pollPayload?.maxAttempts) || 5)); + const intervalMs = Math.max(1000, Number(pollPayload?.intervalMs) || 3000); + const requestedAt = Math.max(0, Number(pollPayload?.filterAfterTimestamp || Date.now()) || Date.now()); + let lastObservedTopMessageFingerprint = ''; + let lastError = null; + + await log(`步骤 ${step}:Yahoo 使用专用 Kiro 桌面授权取码链路:打开收件箱、检查顶部 AWS/Kiro 邮件,必要时点进邮件正文读码。`, 'warn', nodeId); + await focusOrOpenMailTab(mail); + + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + throwIfStopped(); + let result = await sendToMailContentScriptResilient( + mail, + { + type: 'YAHOO_CHECK_TOP_MESSAGE', + step, + source: 'background', + payload: { + ...pollPayload, + intervalMs, + maxAttempts: 1, + keepRefreshingUntilCode: false, + yahooTopRowOnly: true, + requestedAt, + previousTopMessageFingerprint: lastObservedTopMessageFingerprint, + previousAcceptedEmailTimestamp: 0, + yahooFreshnessSkewMs: Math.max(intervalMs * 2, 180000), + }, + }, + { + timeoutMs: 30000, + responseTimeoutMs: 30000, + maxRecoveryAttempts: 2, + logStep: step, + logStepKey: 'kiro-complete-desktop-authorize', + } + ); + + if (result?.error) { + throw new Error(result.error); + } + if (result?.needsOpenDetails) { + await log(`步骤 ${step}:Yahoo 顶部 Kiro/AWS 桌面授权邮件需要点进正文,正在单独打开详情页。`, 'warn', nodeId); + await sendToMailContentScriptResilient( + mail, + { + type: 'YAHOO_OPEN_TOP_MESSAGE', + step, + source: 'background', + payload: { + ...pollPayload, + forceOpenTopMessage: true, + }, + }, + { + timeoutMs: 6000, + responseTimeoutMs: 3000, + maxRecoveryAttempts: 0, + logStep: step, + logStepKey: 'kiro-complete-desktop-authorize', + } + ).catch((error) => { + log(`步骤 ${step}:Yahoo 打开桌面授权邮件详情命令已发出,但响应异常,将继续尝试读取当前页面正文:${getErrorMessage(error)}`, 'warn', nodeId); + }); + result = { + ...result, + detailOpened: true, + }; + } + if (result?.needsDetailRead || result?.detailOpened) { + await log(`步骤 ${step}:Yahoo 正在重连内容脚本读取桌面授权邮件正文验证码。`, 'warn', nodeId); + await sleepWithStop(1500); + result = await sendToMailContentScriptResilient( + mail, + { + type: 'YAHOO_READ_CURRENT_MESSAGE_CODE', + step, + source: 'background', + payload: { + ...pollPayload, + }, + }, + { + timeoutMs: 25000, + responseTimeoutMs: 20000, + maxRecoveryAttempts: 0, + logStep: step, + logStepKey: 'kiro-complete-desktop-authorize', + } + ); + if (result?.error) { + throw new Error(result.error); + } + } + if (result?.topMessageFingerprint !== undefined) { + lastObservedTopMessageFingerprint = String(result.topMessageFingerprint || '').trim(); + } + if (result?.code && result.freshnessMatched !== false) { + await log(`步骤 ${step}:Yahoo 已从 Kiro/AWS 邮件读取桌面授权验证码 ${result.code}`, 'warn', nodeId); + return result; + } + + lastError = new Error(result?.reason || `步骤 ${step}:Yahoo 顶部 Kiro/AWS 邮件暂未读取到桌面授权验证码。`); + if (attempt < maxAttempts) { + await log(`步骤 ${step}:Yahoo 暂未命中 Kiro 桌面授权验证码,${Math.round(intervalMs / 1000)} 秒后刷新收件箱重试(${attempt}/${maxAttempts})。`, 'warn', nodeId); + await sleepWithStop(intervalMs); + await reuseOrCreateTab(mail.source, mail.url, { + inject: mail.inject, + injectSource: mail.injectSource, + navigateOnReuse: true, + reloadIfSameUrl: true, + }); + } + } + + throw lastError || new Error(`步骤 ${step}:Yahoo Kiro 桌面授权取码链路结束,但未获取到验证码。`); + } + async function pollDesktopOtpCode(step, state = {}, nodeId = '') { if (typeof getMailConfig !== 'function') { throw new Error('Kiro 桌面授权验证码步骤缺少邮箱配置能力,无法继续执行。'); @@ -977,6 +1096,10 @@ throw new Error('Kiro 桌面授权验证码步骤缺少邮箱内容脚本通信能力,无法继续执行。'); } + if (mail.provider === 'yahoo') { + return pollYahooKiroDesktopOtpCode(step, state, mail, pollPayload, nodeId); + } + const responseTimeoutMs = getMailPollingResponseTimeoutMs(pollPayload); const result = await sendToMailContentScriptResilient( mail, diff --git a/background/kiro/register-runner.js b/background/kiro/register-runner.js index c8cfb62a..ade46bed 100644 --- a/background/kiro/register-runner.js +++ b/background/kiro/register-runner.js @@ -32,6 +32,8 @@ 'https://profile.aws.amazon.com', ]); const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000; + const KIRO_YAHOO_VERIFICATION_RESEND_INTERVAL_MS = 60 * 1000; + const KIRO_YAHOO_VERIFICATION_MAX_ATTEMPTS = 45; const KIRO_AWS_VERIFICATION_CODE_PATTERNS = Object.freeze([ Object.freeze({ source: '(?:verification\\s*code|验证码|Your code is|code is)[::\\s]*(\\d{6})', @@ -857,12 +859,16 @@ const runtimeState = readKiroRuntime(state); const targetEmail = cleanString(runtimeState.register?.email || state?.email).toLowerCase(); const targetEmailHints = targetEmail ? [targetEmail] : []; - const isMail2925Provider = String(mail?.provider || '').trim().toLowerCase() === '2925'; const normalizedProvider = String(mail?.provider || '').trim().toLowerCase(); - const maxAttempts = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase() + const isMail2925Provider = normalizedProvider === '2925'; + const isYahooProvider = normalizedProvider === 'yahoo'; + const isLuckmailProvider = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase(); + const maxAttempts = isYahooProvider + ? KIRO_YAHOO_VERIFICATION_MAX_ATTEMPTS + : (isLuckmailProvider ? 3 - : (isMail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5); - const intervalMs = normalizedProvider === String(LUCKMAIL_PROVIDER || '').trim().toLowerCase() + : (isMail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5)); + const intervalMs = isLuckmailProvider ? 15000 : (isMail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000); @@ -889,6 +895,306 @@ return Math.max(45000, maxAttempts * intervalMs + 25000); } + async function readKiroYahooTopMessageViaScripting(step, mail = {}, pollPayload = {}, nodeId = '') { + if (!chrome?.scripting?.executeScript) { + return null; + } + + let tabId = await getTabId(mail.source); + if (!Number.isInteger(tabId) || !(await isTabAlive(mail.source))) { + tabId = await reuseOrCreateTab(mail.source, mail.url, { + inject: mail.inject, + injectSource: mail.injectSource, + navigateOnReuse: true, + }); + } + if (!Number.isInteger(tabId)) { + return null; + } + + const [{ result } = {}] = await chrome.scripting.executeScript({ + target: { tabId }, + func: (payload) => { + const normalizeText = (value) => String(value || '').replace(/\s+/g, ' ').trim(); + const normalizeLowerText = (value) => normalizeText(value).toLowerCase(); + const isVisible = (node) => { + if (!(node instanceof HTMLElement)) return false; + const style = getComputedStyle(node); + if (style.display === 'none' || style.visibility === 'hidden') return false; + const rect = node.getBoundingClientRect(); + return rect.width > 0 + && rect.height > 0 + && rect.bottom > 0 + && rect.right > 0 + && rect.top < (window.innerHeight || document.documentElement.clientHeight || 99999) + && rect.left < (window.innerWidth || document.documentElement.clientWidth || 99999); + }; + const joinUnique = (parts) => { + const seen = new Set(); + return normalizeText(parts + .map((part) => normalizeText(part)) + .filter(Boolean) + .filter((part) => { + if (seen.has(part)) return false; + seen.add(part); + return true; + }) + .join(' ')); + }; + const rowText = (row) => { + const nodes = Array.from(row.querySelectorAll([ + '[id^="email-sender-"]', + '[id^="email-subject-snippet-"]', + '[id^="email-subject-"]', + '[id^="email-snippet-"]', + '[id^="email-date-"]', + '[title]', + ].join(','))); + return joinUnique([ + row.getAttribute('aria-label'), + row.getAttribute('title'), + row.innerText, + row.textContent, + ...nodes.map((node) => [ + node.getAttribute?.('aria-label'), + node.getAttribute?.('title'), + node.innerText, + node.textContent, + ].filter(Boolean).join(' ')), + ]); + }; + const extractCode = (text) => { + const source = String(text || ''); + const patterns = Array.isArray(payload?.codePatterns) ? payload.codePatterns : []; + for (const pattern of patterns) { + try { + const regex = new RegExp(String(pattern?.source || pattern || ''), String(pattern?.flags || 'i')); + const match = source.match(regex); + const code = match?.[1] || match?.[0]; + const normalized = String(code || '').match(/\b(\d{6})\b/)?.[1] || ''; + if (normalized) return normalized; + } catch {} + } + return source.match(/(?:验证码|verification\s*code|code(?:\s+is)?)[^0-9]{0,24}(\d{6})/i)?.[1] + || source.match(/\b(\d{6})\b/)?.[1] + || ''; + }; + const rows = Array.from(document.querySelectorAll([ + 'li[data-test-id="message-list-item"]', + '[data-test-id="message-list"] li[data-test-id="message-list-item"]', + '[data-test-id="virtual-list"] li[data-test-id="message-list-item"]', + 'li.H_A.hd_n.p_a.L_0.R_0', + ].join(','))) + .filter(isVisible) + .sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + if (Math.abs(leftRect.top - rightRect.top) > 4) return leftRect.top - rightRect.top; + return leftRect.left - rightRect.left; + }) + .slice(0, 8); + + const topRow = rows[0] || null; + if (!topRow) { + return { ok: false, code: null, reason: '后台直读未找到可见 Yahoo 邮件行', preview: '' }; + } + const text = rowText(topRow); + const lower = normalizeLowerText(text); + const looksKiroAws = /no-?reply@signin\.aws|signin\.aws|amazon web services|aws/.test(lower) + && /构建者|builder id|verification|验证码|code/.test(lower); + if (!looksKiroAws) { + return { + ok: false, + code: null, + reason: '后台直读顶部邮件不是 AWS/Kiro 验证邮件', + preview: text.slice(0, 200), + topMessageFingerprint: text.slice(0, 240), + }; + } + const code = extractCode(text); + if (!code) { + return { + ok: false, + code: null, + reason: '后台直读顶部 AWS/Kiro 邮件未提取到验证码', + preview: text.slice(0, 200), + topMessageFingerprint: text.slice(0, 240), + needsOpenDetails: true, + needsDetailRead: true, + }; + } + return { + ok: true, + code, + emailTimestamp: Date.now(), + preview: text.slice(0, 200), + topMessageFingerprint: text.slice(0, 240), + freshnessMatched: true, + freshnessReason: '后台 executeScript 直接读取 Yahoo 顶部 AWS/Kiro 邮件行', + }; + }, + args: [pollPayload], + }); + + if (result?.code) { + await log(`步骤 ${step}:Yahoo 后台直读已从顶部 AWS/Kiro 邮件行提取验证码 ${result.code}`, 'warn', nodeId); + } + return result || null; + } + + async function resendKiroVerificationCodeFromRegisterPage(step, state = {}, nodeId = '') { + const tabId = await activateKiroRegisterTab(state, { + missingUrlMessage: '缺少 Kiro 注册页地址,无法重新发送验证码,请先执行步骤 1。', + openFailedMessage: '无法恢复 Kiro 注册页,无法重新发送验证码,请重新执行步骤 1。', + }); + await ensureKiroPageState(tabId, { + step, + targetStates: ['register_otp_page'], + stableMs: 900, + initialDelayMs: 100, + injectLogMessage: `步骤 ${step}:Kiro 验证码页内容脚本未就绪,正在等待页面恢复后重新发送验证码...`, + readyLogMessage: `步骤 ${step}:正在确认 Kiro 验证码页以重新发送验证码...`, + timeoutMessage: 'Kiro 验证码页未恢复,无法点击重新发送验证码。', + }); + const result = await sendToContentScriptResilient(KIRO_REGISTER_PAGE_SOURCE_ID, { + type: 'KIRO_RESEND_VERIFICATION_CODE', + step, + source: 'background', + payload: { + timeoutMs: 30000, + retryDelayMs: 250, + }, + }, { + timeoutMs: 35000, + retryDelayMs: 700, + onRetryableError: buildKiroRetryRecovery(tabId, {}), + logStep: step, + logStepKey: 'kiro-submit-verification-code', + logMessage: `步骤 ${step}:正在回到 Kiro 验证码页点击重新发送...`, + }); + if (result?.error) { + throw new Error(result.error); + } + await log(`步骤 ${step}:已在 Kiro 页面点击重新发送验证码,继续回 Yahoo 读取新邮件。`, 'warn', nodeId); + return result || { resent: true }; + } + + async function pollYahooKiroVerificationCode(step, state = {}, mail = {}, pollPayload = {}, nodeId = '') { + const maxAttempts = Math.max(1, Math.floor(Number(pollPayload?.maxAttempts) || 5)); + const intervalMs = Math.max(1000, Number(pollPayload?.intervalMs) || 3000); + const requestedAt = Math.max(0, Number(pollPayload?.filterAfterTimestamp || Date.now()) || Date.now()); + let lastObservedTopMessageFingerprint = ''; + let lastKiroResendAt = Date.now(); + let lastError = null; + + await log(`步骤 ${step}:Yahoo 使用专用 Kiro 取码链路:打开收件箱、检查顶部 AWS/Kiro 邮件,必要时点进邮件正文读码。`, 'warn', nodeId); + await focusOrOpenMailTab(mail); + + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + throwIfStopped(); + const yahooPayload = { + ...pollPayload, + intervalMs, + maxAttempts: 1, + keepRefreshingUntilCode: false, + yahooTopRowOnly: true, + requestedAt, + previousTopMessageFingerprint: lastObservedTopMessageFingerprint, + previousAcceptedEmailTimestamp: 0, + yahooFreshnessSkewMs: Math.max(intervalMs * 2, 180000), + }; + let result = await readKiroYahooTopMessageViaScripting(step, mail, yahooPayload, nodeId); + if (!result?.code) { + lastError = new Error(result?.reason || `步骤 ${step}:Yahoo 后台直读未获取到 Kiro/AWS 验证码。`); + } + + if (result?.error) { + throw new Error(result.error); + } + if (result?.needsOpenDetails) { + await log(`步骤 ${step}:Yahoo 顶部 Kiro/AWS 邮件需要点进正文,正在单独打开详情页。`, 'warn', nodeId); + await sendToMailContentScriptResilient( + mail, + { + type: 'YAHOO_OPEN_TOP_MESSAGE', + step, + source: 'background', + payload: { + ...pollPayload, + forceOpenTopMessage: true, + }, + }, + { + timeoutMs: 6000, + responseTimeoutMs: 3000, + maxRecoveryAttempts: 0, + logStep: step, + logStepKey: 'kiro-submit-verification-code', + } + ).catch((error) => { + log(`步骤 ${step}:Yahoo 打开邮件详情命令已发出,但响应异常,将继续尝试读取当前页面正文:${getErrorMessage(error)}`, 'warn', nodeId); + }); + result = { + ...result, + detailOpened: true, + }; + } + if (result?.needsDetailRead || result?.detailOpened) { + await log(`步骤 ${step}:Yahoo 正在重连内容脚本读取正文验证码。`, 'warn', nodeId); + await sleepWithStop(1500); + result = await sendToMailContentScriptResilient( + mail, + { + type: 'YAHOO_READ_CURRENT_MESSAGE_CODE', + step, + source: 'background', + payload: { + ...pollPayload, + }, + }, + { + timeoutMs: 25000, + responseTimeoutMs: 20000, + maxRecoveryAttempts: 0, + logStep: step, + logStepKey: 'kiro-submit-verification-code', + } + ); + if (result?.error) { + throw new Error(result.error); + } + } + if (result?.topMessageFingerprint !== undefined) { + lastObservedTopMessageFingerprint = String(result.topMessageFingerprint || '').trim(); + } + if (result?.code && result.freshnessMatched !== false) { + await log(`步骤 ${step}:Yahoo 已从 Kiro/AWS 邮件读取验证码 ${result.code}`, 'warn', nodeId); + return result; + } + + lastError = new Error(result?.reason || `步骤 ${step}:Yahoo 顶部 Kiro/AWS 邮件暂未读取到验证码。`); + if (attempt < maxAttempts) { + if (Date.now() - lastKiroResendAt >= KIRO_YAHOO_VERIFICATION_RESEND_INTERVAL_MS) { + await log(`步骤 ${step}:Yahoo 等待超过 1 分钟仍未收到 Kiro 验证码,回 Kiro 页面点击重新发送。`, 'warn', nodeId); + await resendKiroVerificationCodeFromRegisterPage(step, state, nodeId); + lastKiroResendAt = Date.now(); + lastObservedTopMessageFingerprint = ''; + await focusOrOpenMailTab(mail); + } + await log(`步骤 ${step}:Yahoo 暂未命中 Kiro 验证码,${Math.round(intervalMs / 1000)} 秒后刷新收件箱重试(${attempt}/${maxAttempts})。`, 'warn', nodeId); + await sleepWithStop(intervalMs); + await reuseOrCreateTab(mail.source, mail.url, { + inject: mail.inject, + injectSource: mail.injectSource, + navigateOnReuse: true, + reloadIfSameUrl: true, + }); + } + } + + throw lastError || new Error(`步骤 ${step}:Yahoo Kiro 取码链路结束,但未获取到验证码。`); + } + async function pollKiroVerificationCode(step, state = {}, nodeId = '') { if (typeof getMailConfig !== 'function') { throw new Error('Kiro 验证码步骤缺少邮箱配置能力,无法继续执行。'); @@ -955,6 +1261,10 @@ throw new Error('Kiro 验证码步骤缺少邮箱内容脚本通信能力,无法继续执行。'); } + if (mail.provider === 'yahoo') { + return pollYahooKiroVerificationCode(step, state, mail, pollPayload, nodeId); + } + const responseTimeoutMs = getMailPollingResponseTimeoutMs(pollPayload); const result = await sendToMailContentScriptResilient( mail, diff --git a/background/logging-status.js b/background/logging-status.js index d26ad26c..8833e746 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -21,6 +21,7 @@ const labels = { 'openai-auth': '认证页', 'gmail-mail': 'Gmail 邮箱', + 'yahoo-mail': 'Yahoo 邮箱', 'sidepanel': '侧边栏', 'signup-page': '认证页', 'vps-panel': 'CPA 面板', diff --git a/background/navigation-utils.js b/background/navigation-utils.js index 23539734..4c730530 100644 --- a/background/navigation-utils.js +++ b/background/navigation-utils.js @@ -138,6 +138,8 @@ return is163MailHost(candidate.hostname); case 'gmail-mail': return candidate.hostname === 'mail.google.com'; + case 'yahoo-mail': + return candidate.hostname === 'mail.yahoo.com'; case 'icloud-mail': return candidate.hostname === 'www.icloud.com' || candidate.hostname === 'www.icloud.com.cn'; diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 8b95c47c..75839206 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -2,6 +2,10 @@ root.MultiPageBackgroundStep8 = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() { const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000; + const YAHOO_MAILBOX_REFRESH_INTERVAL_MS = 5 * 1000; + const YAHOO_MAILBOX_REFRESH_ROUNDS_BEFORE_RESEND = 5; + const YAHOO_MAILBOX_REFRESH_MAX_ATTEMPTS = 60; + const YAHOO_MAILBOX_RESEND_INTERVAL_MS = YAHOO_MAILBOX_REFRESH_INTERVAL_MS * YAHOO_MAILBOX_REFRESH_ROUNDS_BEFORE_RESEND; function createStep8Executor(deps = {}) { const { @@ -384,6 +388,27 @@ return String(state?.mail2925BaseEmail || '').trim().toLowerCase(); } + function resolveEffectiveMailProvider(mail = {}, state = {}) { + const explicitProvider = String(mail?.provider || '').trim().toLowerCase(); + if (explicitProvider) return explicitProvider; + + const stateProvider = String(state?.mailProvider || '').trim().toLowerCase(); + if (stateProvider) return stateProvider; + + const source = String(mail?.source || '').trim().toLowerCase(); + const injectSource = String(mail?.injectSource || '').trim().toLowerCase(); + const label = String(mail?.label || '').trim().toLowerCase(); + const url = String(mail?.url || '').trim().toLowerCase(); + const combined = `${source} ${injectSource} ${label} ${url}`; + if (/yahoo/.test(combined)) return 'yahoo'; + if (/2925/.test(combined)) return '2925'; + if (/hotmail/.test(combined)) return HOTMAIL_PROVIDER; + if (/luckmail/.test(combined)) return LUCKMAIL_PROVIDER; + if (/cloudmail/.test(combined)) return CLOUD_MAIL_PROVIDER; + if (/cloudflare/.test(combined)) return CLOUDFLARE_TEMP_EMAIL_PROVIDER; + return explicitProvider || stateProvider || ''; + } + async function focusOrOpenMailTab(mail) { const alive = await isTabAlive(mail.source); if (alive) { @@ -408,6 +433,10 @@ function getStep8ResendIntervalMs(state = {}) { const mail = getMailConfig(state); + const effectiveProvider = resolveEffectiveMailProvider(mail, state); + if (effectiveProvider === 'yahoo') { + return YAHOO_MAILBOX_RESEND_INTERVAL_MS; + } if (mail?.provider === LUCKMAIL_PROVIDER) { return 15000; } @@ -559,8 +588,16 @@ const notifyResendRequestedAt = typeof runtime?.onResendRequestedAt === 'function' ? runtime.onResendRequestedAt : null; - const mail = getMailConfig(preparedState); - if (mail.error) throw new Error(mail.error); + const rawMail = getMailConfig(preparedState); + if (rawMail.error) throw new Error(rawMail.error); + const effectiveProvider = resolveEffectiveMailProvider(rawMail, preparedState); + const mail = effectiveProvider && effectiveProvider !== rawMail.provider + ? { ...rawMail, provider: effectiveProvider } + : { ...rawMail }; + if (effectiveProvider === 'yahoo') { + mail.url = 'https://mail.yahoo.com/n/inbox/all?listFilter=ALL_INBOX'; + mail.navigateOnReuse = true; + } const stepStartedAt = Date.now(); const verificationFilterAfterTimestamp = mail.provider === '2925' ? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS) @@ -608,6 +645,8 @@ || mail.provider === CLOUD_MAIL_PROVIDER ) { await addLog(`步骤 ${visibleStep}:正在通过 ${mail.label} 轮询验证码...`); + } else if (mail.provider === 'yahoo') { + await addLog(`步骤 ${visibleStep}:Yahoo 使用专用取码流程:先在认证页重新获取验证码,再跳转收件箱检查顶部邮件;进入步骤时不预先打开旧收件箱轮询。`, 'warn'); } else { await addLog(`步骤 ${visibleStep}:正在打开${mail.label}...`); if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') { @@ -626,6 +665,7 @@ } } + const useYahooMailboxRefreshResendCycle = effectiveProvider === 'yahoo'; await resolveVerificationStep(8, { ...preparedState, step8VerificationTargetEmail: displayedVerificationEmail || '', @@ -636,7 +676,7 @@ disableTimeBudgetCap: mail.provider === '2925', getRemainingTimeMs: getStep8RemainingTimeResolver(preparedState?.oauthUrl || '', visibleStep), requestFreshCodeFirst: false, - lastResendAt: latestResendAt, + lastResendAt: useYahooMailboxRefreshResendCycle ? (latestResendAt || stepStartedAt) : latestResendAt, onResendRequestedAt: async (requestedAt) => { const numericRequestedAt = Number(requestedAt) || 0; if (numericRequestedAt > 0) { @@ -647,14 +687,20 @@ } }, targetEmail: fixedTargetEmail, - maxResendRequests: mail.provider === '2925' ? 2 : undefined, + maxResendRequests: useYahooMailboxRefreshResendCycle ? 0 : (mail.provider === '2925' ? 2 : undefined), initialPollMaxAttempts: mail.provider === '2925' ? 5 : undefined, pollAttemptPlan: mail.provider === '2925' ? [2, 3, 15] : undefined, + intervalMs: useYahooMailboxRefreshResendCycle ? YAHOO_MAILBOX_REFRESH_INTERVAL_MS : undefined, + maxAttempts: useYahooMailboxRefreshResendCycle ? YAHOO_MAILBOX_REFRESH_MAX_ATTEMPTS : undefined, + refreshesBeforeResend: useYahooMailboxRefreshResendCycle ? YAHOO_MAILBOX_REFRESH_ROUNDS_BEFORE_RESEND : undefined, + keepRefreshingUntilCode: useYahooMailboxRefreshResendCycle ? false : undefined, resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER ? 15000 : ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925') ? 0 - : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS), + : (useYahooMailboxRefreshResendCycle + ? YAHOO_MAILBOX_RESEND_INTERVAL_MS + : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS)), }); return { lastResendAt: latestResendAt, diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index 102cc240..ac080646 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -2,6 +2,10 @@ root.MultiPageBackgroundStep4 = factory(); })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep4Module() { const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000; + const YAHOO_MAILBOX_REFRESH_INTERVAL_MS = 5 * 1000; + const YAHOO_MAILBOX_REFRESH_ROUNDS_BEFORE_RESEND = 5; + const YAHOO_MAILBOX_REFRESH_MAX_ATTEMPTS = 60; + const YAHOO_MAILBOX_RESEND_INTERVAL_MS = YAHOO_MAILBOX_REFRESH_INTERVAL_MS * YAHOO_MAILBOX_REFRESH_ROUNDS_BEFORE_RESEND; function createStep4Executor(deps = {}) { const { @@ -68,6 +72,27 @@ || Boolean(state?.signupPhoneActivation); } + function resolveEffectiveMailProvider(mail = {}, state = {}) { + const explicitProvider = String(mail?.provider || '').trim().toLowerCase(); + if (explicitProvider) return explicitProvider; + + const stateProvider = String(state?.mailProvider || '').trim().toLowerCase(); + if (stateProvider) return stateProvider; + + const source = String(mail?.source || '').trim().toLowerCase(); + const injectSource = String(mail?.injectSource || '').trim().toLowerCase(); + const label = String(mail?.label || '').trim().toLowerCase(); + const url = String(mail?.url || '').trim().toLowerCase(); + const combined = `${source} ${injectSource} ${label} ${url}`; + if (/yahoo/.test(combined)) return 'yahoo'; + if (/2925/.test(combined)) return '2925'; + if (/hotmail/.test(combined)) return HOTMAIL_PROVIDER; + if (/luckmail/.test(combined)) return LUCKMAIL_PROVIDER; + if (/cloudmail/.test(combined)) return CLOUD_MAIL_PROVIDER; + if (/cloudflare/.test(combined)) return CLOUDFLARE_TEMP_EMAIL_PROVIDER; + return explicitProvider || stateProvider || ''; + } + async function executeSignupPhoneCodeStep(state, signupTabId) { if (typeof phoneVerificationHelpers?.completeSignupPhoneVerificationFlow !== 'function') { throw new Error('步骤 4:手机号注册验证码流程不可用,接码模块尚未初始化。'); @@ -98,8 +123,12 @@ return; } - const mail = getMailConfig(state); - if (mail.error) throw new Error(mail.error); + const rawMail = getMailConfig(state); + if (rawMail.error) throw new Error(rawMail.error); + const effectiveProvider = resolveEffectiveMailProvider(rawMail, state); + const mail = effectiveProvider && effectiveProvider !== rawMail.provider + ? { ...rawMail, provider: effectiveProvider } + : { ...rawMail }; const verificationFilterAfterTimestamp = mail.provider === '2925' ? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS) @@ -136,17 +165,22 @@ await focusOrOpenMailTab(mail); } await addLog(`步骤 4:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info'); + } else if (mail.provider === 'yahoo') { + await addLog('步骤 4:Yahoo 使用专用取码流程:先在认证页重新获取验证码,再跳转收件箱检查顶部邮件;进入步骤时不预先打开旧收件箱轮询。', 'warn'); } else { await addLog(`步骤 4:正在打开${mail.label}...`); await focusOrOpenMailTab(mail); } - const shouldRequestFreshCodeFirst = ![ - HOTMAIL_PROVIDER, - LUCKMAIL_PROVIDER, - CLOUDFLARE_TEMP_EMAIL_PROVIDER, - CLOUD_MAIL_PROVIDER, - ].includes(mail.provider); + const useYahooMailboxRefreshResendCycle = effectiveProvider === 'yahoo'; + const shouldRequestFreshCodeFirst = useYahooMailboxRefreshResendCycle + ? false + : ![ + HOTMAIL_PROVIDER, + LUCKMAIL_PROVIDER, + CLOUDFLARE_TEMP_EMAIL_PROVIDER, + CLOUD_MAIL_PROVIDER, + ].includes(mail.provider); const signupProfile = buildSignupProfileForVerificationStep(); await resolveVerificationStep(4, state, mail, { @@ -155,11 +189,19 @@ disableTimeBudgetCap: mail.provider === '2925', requestFreshCodeFirst: shouldRequestFreshCodeFirst, signupProfile, + lastResendAt: useYahooMailboxRefreshResendCycle ? stepStartedAt : 0, + intervalMs: useYahooMailboxRefreshResendCycle ? YAHOO_MAILBOX_REFRESH_INTERVAL_MS : undefined, + maxAttempts: useYahooMailboxRefreshResendCycle ? YAHOO_MAILBOX_REFRESH_MAX_ATTEMPTS : undefined, + refreshesBeforeResend: useYahooMailboxRefreshResendCycle ? YAHOO_MAILBOX_REFRESH_ROUNDS_BEFORE_RESEND : undefined, + maxResendRequests: useYahooMailboxRefreshResendCycle ? 0 : undefined, + keepRefreshingUntilCode: useYahooMailboxRefreshResendCycle ? false : undefined, resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER ? 15000 : ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925') ? 0 - : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS), + : (useYahooMailboxRefreshResendCycle + ? YAHOO_MAILBOX_RESEND_INTERVAL_MS + : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS)), }); } diff --git a/background/steps/submit-signup-email.js b/background/steps/submit-signup-email.js index 0e4a690b..246e6292 100644 --- a/background/steps/submit-signup-email.js +++ b/background/steps/submit-signup-email.js @@ -187,10 +187,15 @@ } async function keepSignupTabWindowInBackgroundForStep2(tabId) { - // Intentionally no-op: the task tab is locked to the selected Chrome - // window by the tab-runtime layer. Step 2 must not focus/raise that - // window while the user is working in another app or browser window. - void tabId; + if (!Number.isInteger(tabId) || typeof chrome?.tabs?.get !== 'function') { + return; + } + try { + const tab = await chrome.tabs.get(tabId); + if (Number.isInteger(tab?.windowId) && typeof chrome?.windows?.update === 'function') { + await chrome.windows.update(tab.windowId, { focused: true }).catch(() => {}); + } + } catch {} } async function ensureSignupPhoneEntryReady(tabId) { @@ -239,6 +244,7 @@ } else { await chrome.tabs.update(signupTabId, { active: true }); await keepSignupTabWindowInBackgroundForStep2(signupTabId); + await addLog('步骤 2:已从邮箱页面恢复到 ChatGPT 注册页,准备提交注册邮箱。', 'info'); await waitForStep2SignupTabToSettle( signupTabId, '步骤 2:已切换到注册页标签,正在等待页面加载完成并额外稳定 3 秒...' diff --git a/background/verification-flow.js b/background/verification-flow.js index 27221f58..24b71c37 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -14,6 +14,7 @@ CLOUD_MAIL_PROVIDER = 'cloudmail', completeNodeFromBackground, confirmCustomVerificationStepBypassRequest, + ensureContentScriptReadyOnTab, getNodeIdByStepForState, getHotmailVerificationPollConfig, getHotmailVerificationRequestTimestamp, @@ -23,6 +24,7 @@ HOTMAIL_PROVIDER, isMail2925LimitReachedError, isStopError, + isTabAlive, LUCKMAIL_PROVIDER, YYDS_MAIL_PROVIDER = 'yyds-mail', MAIL_2925_VERIFICATION_INTERVAL_MS, @@ -32,6 +34,7 @@ pollHotmailVerificationCode, pollLuckmailVerificationCode, pollYydsMailVerificationCode, + reuseOrCreateTab, sendToContentScript, sendToContentScriptResilient, sendToMailContentScriptResilient, @@ -90,6 +93,55 @@ return step === 4 ? '注册' : '登录'; } + function getYahooRejectedCodesFromState(step, state = {}) { + const rejectedCodes = []; + if (step === 8) { + const lastSignupCode = String(state?.lastSignupCode || '').trim(); + if (lastSignupCode) { + rejectedCodes.push(lastSignupCode); + } + } + return rejectedCodes; + } + + function inferMailProvider(mail = {}, state = {}) { + const explicitProvider = String(mail?.provider || '').trim().toLowerCase(); + if (explicitProvider) { + return explicitProvider; + } + + const stateProvider = String(state?.mailProvider || '').trim().toLowerCase(); + if (stateProvider) { + return stateProvider; + } + + const source = String(mail?.source || '').trim().toLowerCase(); + const injectSource = String(mail?.injectSource || '').trim().toLowerCase(); + const label = String(mail?.label || '').trim().toLowerCase(); + const url = String(mail?.url || '').trim().toLowerCase(); + const combined = `${source} ${injectSource} ${label} ${url}`; + + if (/yahoo/.test(combined)) return 'yahoo'; + if (/2925/.test(combined)) return '2925'; + if (/hotmail/.test(combined)) return HOTMAIL_PROVIDER; + if (/luckmail/.test(combined)) return LUCKMAIL_PROVIDER; + if (/cloudmail/.test(combined)) return CLOUD_MAIL_PROVIDER; + if (/yyds/.test(combined)) return YYDS_MAIL_PROVIDER; + if (/cloudflare/.test(combined)) return CLOUDFLARE_TEMP_EMAIL_PROVIDER; + return explicitProvider || stateProvider || ''; + } + + function withResolvedMailProvider(mail = {}, state = {}) { + const provider = inferMailProvider(mail, state); + if (!mail || typeof mail !== 'object') { + return { provider }; + } + if (String(mail?.provider || '').trim().toLowerCase() === provider) { + return mail; + } + return { ...mail, provider }; + } + function isIcloudMail(mail) { return mail?.source === 'icloud-mail' || mail?.provider === 'icloud'; } @@ -429,6 +481,7 @@ } const normalizedStep = Number(step) === 4 ? 4 : 8; const is2925Provider = state?.mailProvider === '2925'; + const isYahooProvider = state?.mailProvider === 'yahoo'; const mail2925MatchTargetEmail = is2925Provider && String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive'; return { @@ -444,8 +497,9 @@ : (String(state?.step8VerificationTargetEmail || '').trim() || state.email), targetEmailHints: [], mail2925MatchTargetEmail, - maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5, - intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000, + maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : (isYahooProvider ? 60 : 5), + intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : (isYahooProvider ? 5000 : 3000), + keepRefreshingUntilCode: isYahooProvider, ...overrides, }; } @@ -677,7 +731,381 @@ }); } + function parseYahooReopenRequirement(error) { + const message = String(error?.message || error || ''); + const match = message.match(/YAHOO_(?:INBOX|SETTINGS)_REOPEN_REQUIRED::(https?:\/\/\S+)/i); + return match ? match[1] : ''; + } + + function getYahooInboxUrl(mail) { + const fallbackUrl = 'https://mail.yahoo.com/n/inbox/all?listFilter=ALL_INBOX'; + const candidateUrl = String(mail?.url || '').trim(); + + try { + const parsed = new URL(candidateUrl); + const isYahooInbox = /(^|\.)mail\.yahoo\.com$/i.test(parsed.hostname) + && /^\/n\/inbox\/all\/?$/i.test(parsed.pathname || ''); + if (isYahooInbox) { + const accountIds = String(parsed.searchParams.get('accountIds') || '').trim(); + return accountIds + ? `${fallbackUrl}&accountIds=${encodeURIComponent(accountIds)}` + : fallbackUrl; + } + } catch (_) {} + + return fallbackUrl; + } + + async function getYahooMailTabSnapshot(mail) { + const source = String(mail?.source || '').trim(); + if (!source) { + return { source, tabId: null, alive: false }; + } + + let tabId = null; + if (typeof getTabId === 'function') { + try { + tabId = await getTabId(source); + } catch (_) { + tabId = null; + } + } + + let alive = Number.isInteger(tabId); + if (typeof isTabAlive === 'function') { + try { + alive = await isTabAlive(source); + } catch (_) { + alive = Number.isInteger(tabId); + } + } + + return { + source, + tabId: Number.isInteger(tabId) ? tabId : null, + alive: Boolean(alive), + }; + } + + async function assertYahooMailTabAvailable(step, mail, contextLabel = '收件箱重建后') { + let snapshot = null; + for (let attempt = 1; attempt <= 5; attempt += 1) { + snapshot = await getYahooMailTabSnapshot(mail); + if (snapshot.alive && Number.isInteger(snapshot.tabId)) { + return snapshot; + } + if (attempt < 5) { + await sleepWithStop(300); + } + } + + const sourceLabel = snapshot?.source || String(mail?.source || '').trim() || 'yahoo-mail'; + throw new Error(`步骤 ${step}:Yahoo ${contextLabel}仍未能定位到可用标签页(source=${sourceLabel},tabId=${snapshot?.tabId ?? 'null'})。请停止当前流程后重试。`); + } + + async function reopenYahooMailTabIfNeeded(step, mail, error) { + const reopenUrl = parseYahooReopenRequirement(error); + if (!reopenUrl || mail?.provider !== 'yahoo' || typeof reuseOrCreateTab !== 'function') { + return false; + } + + const targetUrl = /\/n\/settings(?:\/2)?/i.test(reopenUrl) + ? reopenUrl + : getYahooInboxUrl(mail); + + await addLog(`步骤 ${step}:Yahoo 页面要求重开,正在关闭旧标签并重新打开标准页面。`, 'warn'); + + try { + const snapshot = await getYahooMailTabSnapshot(mail); + if (snapshot.alive && Number.isInteger(snapshot.tabId)) { + await chrome.tabs.remove(snapshot.tabId).catch(() => {}); + await sleepWithStop(400); + } + } catch (err) { + await addLog(`步骤 ${step}:检查 Yahoo 旧标签页失败,将继续创建新标签页:${err?.message || err}`, 'warn'); + } + + const newTabId = await reuseOrCreateTab(mail.source, targetUrl, { + inject: mail.inject, + injectSource: mail.injectSource, + navigateOnReuse: true, + }); + + if (!Number.isInteger(newTabId)) { + throw new Error('Yahoo:重开标签页失败,未能获取有效的标签页 ID。'); + } + + await sleepWithStop(2500); + await assertYahooMailTabAvailable(step, mail, '标签页重开后'); + + if (typeof ensureContentScriptReadyOnTab === 'function') { + try { + await ensureContentScriptReadyOnTab(mail.source, newTabId, { + inject: mail.inject, + injectSource: mail.injectSource, + timeoutMs: 30000, + retryDelayMs: 800, + logMessage: `步骤 ${step}:Yahoo 收件箱标签页重开后内容脚本仍在加载,正在等待就绪...`, + }); + } catch (err) { + await addLog(`步骤 ${step}:Yahoo 内容脚本就绪检查失败,将继续尝试读取顶部邮件:${err?.message || err}`, 'warn'); + } + } + + return true; + } + + async function ensureYahooInboxBeforePolling(step, mail, options = {}) { + const effectiveProvider = inferMailProvider(mail); + if (effectiveProvider !== 'yahoo') { + return false; + } + + const mailSource = String(mail?.source || '').trim(); + if (!mailSource) { + await addLog(`步骤 ${step}:当前未提供可复用的 Yahoo 邮箱标签页上下文,跳过强制切回标准收件箱。`, 'warn'); + return false; + } + + const targetUrl = getYahooInboxUrl(mail); + const waitMs = Math.max(0, Number(options.waitMs) || 1800); + const logMessage = options.logMessage === undefined + ? `步骤 ${step}:轮询 Yahoo 前先强制回到标准收件箱页。` + : String(options.logMessage || '').trim(); + const normalizedMail = { + ...mail, + url: targetUrl, + navigateOnReuse: true, + }; + const snapshotBefore = await getYahooMailTabSnapshot(normalizedMail); + const hasReusableTab = snapshotBefore.alive && Number.isInteger(snapshotBefore.tabId); + + if (hasReusableTab && typeof chrome?.tabs?.get === 'function') { + try { + const currentTab = await chrome.tabs.get(snapshotBefore.tabId); + const currentUrl = String(currentTab?.url || '').trim(); + if (currentUrl && currentUrl.includes('/n/inbox')) { + if (waitMs > 0) { + await sleepWithStop(waitMs); + } + return true; + } + } catch (err) { + await addLog(`步骤 ${step}:检查 Yahoo 标签页当前 URL 失败,将继续强制导航:${err?.message || err}`, 'warn'); + } + } + + if (typeof reuseOrCreateTab === 'function') { + if (logMessage) { + await addLog(logMessage, options.logLevel || 'info'); + } + await reuseOrCreateTab(normalizedMail.source, targetUrl, { + inject: normalizedMail.inject, + injectSource: normalizedMail.injectSource, + reloadIfSameUrl: true, + navigateOnReuse: true, + }); + await assertYahooMailTabAvailable(step, normalizedMail, hasReusableTab ? '收件箱刷新后' : '收件箱标签页重建后'); + if (waitMs > 0) { + await sleepWithStop(waitMs); + } + if (mail && typeof mail === 'object' && mail.url !== targetUrl) { + mail.url = targetUrl; + } + return true; + } + + const mailTabId = hasReusableTab ? snapshotBefore.tabId : null; + if (!Number.isInteger(mailTabId) || !chrome?.tabs?.update) { + throw new Error(`步骤 ${step}:当前没有可用的 Yahoo 标签页可供导航。请停止当前流程后重试。`); + } + + await chrome.tabs.update(mailTabId, { url: targetUrl, active: true }).catch(() => {}); + if (waitMs > 0) { + await sleepWithStop(waitMs); + } + if (mail && typeof mail === 'object' && mail.url !== targetUrl) { + mail.url = targetUrl; + } + return true; + } + + async function pollYahooVerificationCodeWithForegroundRefresh(step, state, mail, pollOverrides = {}) { + mail = withResolvedMailProvider(mail, state); + const stateKey = getVerificationCodeStateKey(step); + const rejectedCodes = new Set(); + if (state[stateKey] && pollOverrides?.seedRejectedCodesFromState !== false) { + rejectedCodes.add(state[stateKey]); + } + for (const code of getYahooRejectedCodesFromState(step, state)) { + if (code) rejectedCodes.add(code); + } + for (const code of (pollOverrides.excludeCodes || [])) { + if (code) rejectedCodes.add(code); + } + + const refreshIntervalMs = Math.max(1000, Number(pollOverrides.intervalMs) || 5000); + const maxPageRefreshes = Math.max(1, Math.floor(Number(pollOverrides.maxAttempts) || 60)); + const refreshesBeforeResend = Math.max(1, Math.floor(Number(pollOverrides.refreshesBeforeResend) || 5)); + const payloadOverrides = { ...pollOverrides }; + delete payloadOverrides.excludeCodes; + delete payloadOverrides.intervalMs; + delete payloadOverrides.maxAttempts; + delete payloadOverrides.refreshesBeforeResend; + delete payloadOverrides.resendIntervalMs; + delete payloadOverrides.lastResendAt; + delete payloadOverrides.maxResendRequests; + delete payloadOverrides.onResendRequestedAt; + delete payloadOverrides.keepRefreshingUntilCode; + delete payloadOverrides.filterAfterTimestamp; + delete payloadOverrides.seedRejectedCodesFromState; + + let totalPageRefreshes = 0; + let resendCycle = 0; + let lastError = null; + let lastResendAt = Number(pollOverrides.lastResendAt) || 0; + let lastObservedTopMessageFingerprint = String( + pollOverrides.previousTopMessageFingerprint || state.lastYahooTopMessageFingerprint || '' + ).trim(); + const previousAcceptedEmailTimestamp = Math.max(0, Number( + pollOverrides.previousAcceptedEmailTimestamp || state.lastEmailTimestamp || 0 + ) || 0); + + await addLog( + `步骤 ${step}:Yahoo 专用取码流程已启动:先重发验证码,再切回收件箱顶部邮件检查,每 ${Math.round(refreshIntervalMs / 1000)} 秒刷新一次。`, + 'warn' + ); + + while (totalPageRefreshes < maxPageRefreshes) { + throwIfStopped(); + resendCycle += 1; + lastResendAt = await requestVerificationCodeResend(step, { + ...pollOverrides, + allowMissingSignupTab: true, + }); + + await ensureYahooInboxBeforePolling(step, mail, { + logMessage: `步骤 ${step}:已跳转到 Yahoo 收件箱页面,开始检查最顶部验证码邮件。`, + logLevel: 'warn', + waitMs: 2500, + }); + + let refreshesThisCycle = 0; + const maxCycleRefreshes = Math.min(refreshesBeforeResend, maxPageRefreshes - totalPageRefreshes); + while (refreshesThisCycle < maxCycleRefreshes && totalPageRefreshes < maxPageRefreshes) { + throwIfStopped(); + const payload = getVerificationPollPayload(step, state, { + ...payloadOverrides, + excludeCodes: [...rejectedCodes], + intervalMs: refreshIntervalMs, + maxAttempts: 1, + keepRefreshingUntilCode: false, + yahooTopRowOnly: true, + filterAfterTimestamp: 0, + requestedAt: lastResendAt, + previousTopMessageFingerprint: lastObservedTopMessageFingerprint, + previousAcceptedEmailTimestamp, + yahooFreshnessSkewMs: Math.max(refreshIntervalMs * 2, 180000), + }); + + const timedPoll = await applyMailPollingTimeBudget( + step, + payload, + { ...pollOverrides, disableTimeBudgetCap: Boolean(pollOverrides.disableTimeBudgetCap) }, + `检查 Yahoo 收件箱顶部${getVerificationCodeLabel(step)}验证码邮件` + ); + + let result; + try { + result = await sendToMailContentScriptResilient( + mail, + { + type: 'YAHOO_CHECK_TOP_MESSAGE', + step, + source: 'background', + payload: { + ...timedPoll.payload, + intervalMs: refreshIntervalMs, + maxAttempts: 1, + keepRefreshingUntilCode: false, + yahooTopRowOnly: true, + filterAfterTimestamp: 0, + }, + }, + { + timeoutMs: Math.min(timedPoll.timeoutMs, 30000), + maxRecoveryAttempts: 2, + responseTimeoutMs: Math.min(timedPoll.responseTimeoutMs, 30000), + logStep: activeVerificationLogStep, + logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code', + } + ); + } catch (err) { + if (await reopenYahooMailTabIfNeeded(step, mail, err)) { + await addLog(`步骤 ${step}:Yahoo 标签页已重开,继续检查顶部邮件。`, 'warn'); + continue; + } + throw err; + } + + if (result?.error) { + const resultError = new Error(result.error); + if (await reopenYahooMailTabIfNeeded(step, mail, resultError)) { + await addLog(`步骤 ${step}:Yahoo 顶部检查要求重开收件箱,已处理并继续。`, 'warn'); + continue; + } + throw resultError; + } + + if (result?.topMessageFingerprint !== undefined) { + lastObservedTopMessageFingerprint = String(result.topMessageFingerprint || '').trim(); + } + + if (result?.code && !rejectedCodes.has(result.code)) { + if (result.freshnessMatched) { + await addLog(`步骤 ${step}:Yahoo 顶部邮件命中${getVerificationCodeLabel(step)}验证码 ${result.code}`, 'warn'); + return { + ...result, + lastResendAt, + remainingResendRequests: 0, + totalPageRefreshes, + }; + } + await addLog(`步骤 ${step}:Yahoo 顶部邮件发现验证码 ${result.code},但不是本轮新验证码,继续等待。`, 'warn'); + } + + lastError = new Error(result?.reason || `步骤 ${step}:Yahoo 顶部邮件未读取到新的验证码。`); + refreshesThisCycle += 1; + totalPageRefreshes += 1; + + if (refreshesThisCycle < maxCycleRefreshes && totalPageRefreshes < maxPageRefreshes) { + await addLog( + `步骤 ${step}:Yahoo 顶部邮件未命中,${Math.round(refreshIntervalMs / 1000)} 秒后刷新收件箱继续检查(${totalPageRefreshes}/${maxPageRefreshes})。`, + 'warn' + ); + await sleepWithStop(refreshIntervalMs); + await ensureYahooInboxBeforePolling(step, mail, { + logMessage: `步骤 ${step}:正在刷新 Yahoo 收件箱并重新检查顶部邮件。`, + logLevel: 'warn', + waitMs: 2500, + }); + } + } + + if (refreshesThisCycle >= maxCycleRefreshes) { + await addLog(`步骤 ${step}:Yahoo 连续 ${refreshesThisCycle} 次前台刷新仍未命中,返回认证页重新获取验证码。`, 'warn'); + } + } + + throw lastError || new Error(`步骤 ${step}:Yahoo 前台刷新 ${maxPageRefreshes} 次后仍未获取到新的${getVerificationCodeLabel(step)}验证码。`); + } + async function pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides = {}) { + mail = withResolvedMailProvider(mail, state); + if (mail?.provider === 'yahoo') { + throw new Error(`步骤 ${step}:Yahoo 已禁用普通定时邮箱轮询,仅允许专用前台刷新取码流程。`); + } + const stateKey = getVerificationCodeStateKey(step); const rejectedCodes = new Set(); if (state[stateKey]) { @@ -944,6 +1372,7 @@ } async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) { + mail = withResolvedMailProvider(mail, state); const { onResendRequestedAt, maxRounds: _ignoredMaxRounds, @@ -994,6 +1423,16 @@ }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`); return pollYydsMailVerificationCode(step, state, timedPoll.payload); } + if (mail.provider === 'yahoo') { + const refreshesBeforeResend = Math.max(1, Number(pollOverrides.refreshesBeforeResend) || 5); + const refreshIntervalSeconds = Math.max(1, Math.round((Number(pollOverrides.intervalMs) || 5000) / 1000)); + const maxRefreshAttempts = Math.max(1, Number(pollOverrides.maxAttempts) || 60); + await addLog( + `步骤 ${step}:本次将走 Yahoo 专用前台刷新取码逻辑:先重发,再跳转收件箱,只检查顶部邮件,每 ${refreshIntervalSeconds} 秒刷新一次,连续 ${refreshesBeforeResend} 次未命中就回认证页重新获取验证码,总刷新上限 ${maxRefreshAttempts} 次。`, + 'warn' + ); + return pollYahooVerificationCodeWithForegroundRefresh(step, state, mail, pollOverrides); + } if (Number(pollOverrides.resendIntervalMs) > 0) { return pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides); @@ -1268,6 +1707,7 @@ } async function resolveVerificationStep(step, state, mail, options = {}) { + mail = withResolvedMailProvider(mail, state); const completionStep = getCompletionStep(step, options); activeVerificationLogStep = completionStep; const completionNodeId = await getNodeIdForStep(completionStep); @@ -1280,9 +1720,14 @@ ? options.beforeSubmit : null; const ignorePersistedLastCode = Boolean(hotmailPollConfig?.ignorePersistedLastCode); - if (state[stateKey] && !ignorePersistedLastCode) { + if (state[stateKey] && !ignorePersistedLastCode && mail?.provider !== 'yahoo') { rejectedCodes.add(state[stateKey]); } + if (mail?.provider === 'yahoo') { + for (const code of getYahooRejectedCodesFromState(step, state)) { + if (code) rejectedCodes.add(code); + } + } let nextFilterAfterTimestamp = options.filterAfterTimestamp ?? null; const requestFreshCodeFirst = options.requestFreshCodeFirst !== undefined @@ -1318,7 +1763,11 @@ await clear2925MailboxBeforePolling(step, mail, options); - if (requestFreshCodeFirst) { + if (requestFreshCodeFirst && mail?.provider === 'yahoo') { + await addLog(`步骤 ${step}:Yahoo 专用取码流程会在主循环内先重发验证码,跳过进入主循环前的额外预发。`, 'warn'); + } + + if (requestFreshCodeFirst && mail?.provider !== 'yahoo') { if (remainingAutomaticResendCount <= 0) { await addLog(`步骤 ${step}:当前自动重新发送验证码次数为 0,将直接使用当前时间窗口轮询邮箱。`, 'info'); } else { @@ -1367,7 +1816,20 @@ resendIntervalMs, lastResendAt, onResendRequestedAt: updateFilterAfterTimestampForVerificationStep, + seedRejectedCodesFromState: !Boolean(mail?.provider === 'yahoo'), }; + if (Number(options.intervalMs) > 0) { + pollOptions.intervalMs = Number(options.intervalMs); + } + if (Number(options.maxAttempts) > 0) { + pollOptions.maxAttempts = Number(options.maxAttempts); + } + if (Number(options.refreshesBeforeResend) > 0) { + pollOptions.refreshesBeforeResend = Number(options.refreshesBeforeResend); + } + if (options.keepRefreshingUntilCode !== undefined) { + pollOptions.keepRefreshingUntilCode = Boolean(options.keepRefreshingUntilCode); + } if (nextFilterAfterTimestamp !== null && nextFilterAfterTimestamp !== undefined) { pollOptions.filterAfterTimestamp = nextFilterAfterTimestamp; } @@ -1420,6 +1882,9 @@ await setState({ lastEmailTimestamp: result.emailTimestamp, [stateKey]: result.code, + ...(mail?.provider === 'yahoo' + ? { lastYahooTopMessageFingerprint: result.topMessageFingerprint || null } + : {}), }); if (!completionNodeId) { diff --git a/content/kiro/register-page.js b/content/kiro/register-page.js index 680f076f..2cc8a566 100644 --- a/content/kiro/register-page.js +++ b/content/kiro/register-page.js @@ -3,6 +3,7 @@ console.log('[MultiPage:kiro-register-page] Content script loaded on', location. const KIRO_REGISTER_PAGE_LISTENER_SENTINEL = 'data-multipage-kiro-register-page-listener'; const DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS = globalThis.MultiPageKiroTimeouts?.DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS || (3 * 60 * 1000); const KIRO_CONTINUE_TEXT_PATTERN = /continue|继续/i; +const KIRO_RESEND_VERIFICATION_CODE_PATTERN = /重新发送(?:验证码)?|再次发送(?:验证码)?|重发(?:验证码)?|未收到(?:验证码|邮件)?|resend(?:\s+(?:code|email))?|send\s+(?:a\s+)?new\s+code|send\s+(?:it\s+)?again|request\s+(?:a\s+)?new\s+code|didn'?t\s+receive|did\s+not\s+receive/i; const KIRO_BUILDER_ID_TEXT_PATTERN = /aws\s*builder\s*id|builder\s*id/i; const KIRO_CONFIRM_CONTINUE_TEXT_PATTERN = /confirm and continue|确认并继续/i; const KIRO_ALLOW_ACCESS_TEXT_PATTERN = /allow access|允许访问/i; @@ -47,7 +48,7 @@ const KIRO_CONFIRM_PASSWORD_TEXT_PATTERN = /confirm\s*password|re[-\s]*enter\s*p const KIRO_PRIMARY_PASSWORD_HINT_PATTERN = /enter\s*password|create\s*password|new\s*password|\u8f93\u5165.*\u5bc6\u7801|\u521b\u5efa.*\u5bc6\u7801|^\s*\u5bc6\u7801\s*$/i; const KIRO_SIGN_IN_TEXT_PATTERN = /sign\s*in\s*with\s*your\s*aws\s*builder\s*id|aws\s*builder\s*id\s*sign\s*in/i; const KIRO_CODE_INVALID_TEXT_PATTERN = /code\s*(?:is\s*)?invalid|invalid\s*code|\u4ee3\u7801\u65e0\u6548|\u9a8c\u8bc1\u7801\u65e0\u6548/i; -const KIRO_RESEND_CODE_TEXT_PATTERN = /resend|send\s*again|\u91cd\u65b0\u53d1\u9001/i; +const KIRO_RESEND_CODE_TEXT_PATTERN = KIRO_RESEND_VERIFICATION_CODE_PATTERN; function isVisibleKiroElement(element) { if (!element) return false; @@ -166,6 +167,12 @@ function getElementActionText(element) { .trim(); } +function isKiroActionEnabled(element) { + return Boolean(element) + && !element.disabled + && element.getAttribute('aria-disabled') !== 'true'; +} + function findActionButton(options = {}) { const { preferredSelectors = [], @@ -175,13 +182,13 @@ function findActionButton(options = {}) { for (const selector of preferredSelectors) { const preferred = findFirstVisible(selector); - if (preferred && !preferred.disabled && preferred.getAttribute('aria-disabled') !== 'true') { + if (preferred && isKiroActionEnabled(preferred)) { return preferred; } } const candidates = collectVisibleElements('button, [role="button"], input[type="submit"], input[type="button"]') - .filter((element) => !element.disabled && element.getAttribute('aria-disabled') !== 'true'); + .filter((element) => isKiroActionEnabled(element)); const prioritized = formOwner ? candidates.filter((element) => (element.form || element.closest?.('form') || null) === formOwner) @@ -386,12 +393,27 @@ function findOtpVerifyButton(otpInput = null) { }); } -function findOtpResendButton(otpInput = null) { +function findOtpResendButton(otpInputOrOptions = null) { + const options = otpInputOrOptions instanceof HTMLElement ? {} : (otpInputOrOptions || {}); + const otpInput = otpInputOrOptions instanceof HTMLElement + ? otpInputOrOptions + : (options.otpInput instanceof HTMLElement ? options.otpInput : null); const form = otpInput?.form || otpInput?.closest?.('form') || null; - return findActionButton({ - textPattern: KIRO_RESEND_CODE_TEXT_PATTERN, - formOwner: form, + const allowDisabled = Boolean(options.allowDisabled); + const candidates = collectVisibleElements( + 'button, a, [role="button"], [role="link"], input[type="submit"], input[type="button"]' + ); + const matches = candidates.filter((element) => { + if (!allowDisabled && !isKiroActionEnabled(element)) { + return false; + } + const text = getElementActionText(element); + return text && KIRO_RESEND_CODE_TEXT_PATTERN.test(text); }); + const sameFormMatches = form + ? matches.filter((element) => (element.form || element.closest?.('form') || null) === form) + : []; + return sameFormMatches[0] || matches[0] || null; } function findPasswordContinueButton(passwordInput = null) { @@ -838,6 +860,41 @@ async function submitKiroVerificationCode(payload = {}) { }; } +async function resendKiroVerificationCode(payload = {}) { + const readyState = await ensureKiroRegisterPageState({ + targetStates: ['register_otp_page'], + timeoutMs: payload?.timeoutMs || DEFAULT_KIRO_PAGE_LOAD_TIMEOUT_MS, + retryDelayMs: payload?.retryDelayMs || 250, + }); + + const timeoutMs = Math.max(1000, Math.floor(Number(payload?.timeoutMs) || 30000)); + const retryDelayMs = Math.max(100, Math.floor(Number(payload?.retryDelayMs) || 250)); + const start = Date.now(); + let resendButton = null; + + while (Date.now() - start < timeoutMs) { + throwIfStopped(); + resendButton = findOtpResendButton({ + otpInput: readyState.otpInput, + allowDisabled: true, + }); + if (resendButton && isKiroActionEnabled(resendButton)) { + const actionText = getElementActionText(resendButton); + await sleep(200); + simulateClick(resendButton); + return { + resent: true, + state: 'verification_resend_requested', + actionText, + url: location.href, + }; + } + await sleep(retryDelayMs); + } + + throw new Error('Kiro 验证码页未找到可用的重新发送验证码按钮。'); +} + async function submitKiroPassword(payload = {}) { const password = String(payload?.password || ''); if (!password) { @@ -946,6 +1003,8 @@ async function handleKiroRegisterCommand(message) { return ensureKiroRegisterPageState(message.payload || {}); case 'ENSURE_KIRO_STATE_CHANGE': return waitForKiroRegisterStateChange(message.payload || {}); + case 'KIRO_RESEND_VERIFICATION_CODE': + return resendKiroVerificationCode(message.payload || {}); case 'EXECUTE_NODE': { const nodeId = String(message.nodeId || message.payload?.nodeId || '').trim(); if (nodeId === 'kiro-open-register-page') { @@ -980,6 +1039,7 @@ if (document.documentElement.getAttribute(KIRO_REGISTER_PAGE_LISTENER_SENTINEL) message.type === 'ENSURE_KIRO_PAGE_STATE' || message.type === 'ENSURE_KIRO_STATE_CHANGE' || message.type === 'GET_KIRO_REGISTER_PAGE_STATE' + || message.type === 'KIRO_RESEND_VERIFICATION_CODE' || message.type === 'EXECUTE_NODE' ) { resetStopState(); diff --git a/content/yahoo-mail.js b/content/yahoo-mail.js new file mode 100644 index 00000000..699e7267 --- /dev/null +++ b/content/yahoo-mail.js @@ -0,0 +1,2683 @@ +const YAHOO_MAIL_PREFIX = '[MultiPage:yahoo-mail]'; +const YAHOO_INBOX_URL = 'https://mail.yahoo.com/n/inbox/all?listFilter=ALL_INBOX'; +const YAHOO_SETTINGS_URL = 'https://mail.yahoo.com/n/settings/2'; +const YAHOO_ROW_SCAN_LIMIT = 80; +console.log(YAHOO_MAIL_PREFIX, 'Content script loaded on', location.href); + +// 监听后台发来的邮件轮询和临时别名创建请求。 +chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (message.type === 'POLL_EMAIL') { + if (Number(message.step) === 4 || Number(message.step) === 8) { + const error = `YAHOO_LEGACY_POLLING_DISABLED::步骤 ${message.step} 已禁用旧版 POLL_EMAIL 轮询,只允许 YAHOO_CHECK_TOP_MESSAGE 专用顶部邮件检查。`; + log(`Yahoo:${error}`, 'warn'); + sendResponse({ error }); + return true; + } + + resetStopState(); + handlePollEmail(message.step, message.payload || {}).then(sendResponse).catch((err) => { + if (isStopError(err)) { + sendResponse({ stopped: true, error: err.message }); + return; + } + sendResponse({ error: err.message }); + }); + return true; + } + + if (message.type === 'YAHOO_CHECK_TOP_MESSAGE') { + resetStopState(); + handleCheckTopMessage(message.step, message.payload || {}).then(sendResponse).catch((err) => { + if (isStopError(err)) { + sendResponse({ stopped: true, error: err.message }); + return; + } + sendResponse({ error: err.message }); + }); + return true; + } + + if (message.type === 'YAHOO_READ_CURRENT_MESSAGE_CODE') { + resetStopState(); + handleReadCurrentMessageCode(message.step, message.payload || {}).then(sendResponse).catch((err) => { + if (isStopError(err)) { + sendResponse({ stopped: true, error: err.message }); + return; + } + sendResponse({ error: err.message }); + }); + return true; + } + + if (message.type === 'YAHOO_OPEN_TOP_MESSAGE') { + resetStopState(); + handleOpenTopMessage(message.step, message.payload || {}).then(sendResponse).catch((err) => { + if (isStopError(err)) { + sendResponse({ stopped: true, error: err.message }); + return; + } + sendResponse({ error: err.message }); + }); + return true; + } + + if (message.type === 'YAHOO_CREATE_TEMP_ALIAS') { + resetStopState(); + handleCreateYahooTempAlias(message.payload || {}).then(sendResponse).catch((err) => { + if (isStopError(err)) { + sendResponse({ stopped: true, error: err.message }); + return; + } + sendResponse({ error: err.message }); + }); + return true; + } + + if (message.type === 'YAHOO_LOGIN_WITH_CREDENTIALS') { + resetStopState(); + handleYahooLoginWithCredentials(message.payload || {}).then(sendResponse).catch((err) => { + if (isStopError(err)) { + sendResponse({ stopped: true, error: err.message }); + return; + } + sendResponse({ error: err.message }); + }); + return true; + } +}); + +// 规范化文本,压缩连续空白并去掉首尾空格。 +function normalizeText(value) { + return String(value || '').replace(/\s+/g, ' ').trim(); +} + +// 规范化文本后统一转成小写,方便做模糊匹配。 +function normalizeLowerText(value) { + return normalizeText(value).toLowerCase(); +} + +function joinUniqueTextParts(parts = []) { + const seen = new Set(); + return normalizeText(parts + .map((part) => normalizeText(part)) + .filter(Boolean) + .filter((part) => { + if (seen.has(part)) return false; + seen.add(part); + return true; + }) + .join(' ')); +} + +// 从邮件或页面文本中提取 6 位验证码。 +function extractVerificationCode(text) { + const source = String(text || ''); + const matchCn = source.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/i); + if (matchCn) return matchCn[1]; + const matchEn = source.match(/(?:your\s+chatgpt\s+code\s+is|verification\s+code|code(?:\s+is)?)[^0-9]{0,16}(\d{6})/i); + if (matchEn) return matchEn[1]; + const matchPlain = source.match(/\b(\d{6})\b/); + return matchPlain ? matchPlain[1] : null; +} + +function extractVerificationCodeWithPatterns(text, payload = {}) { + const source = String(text || ''); + const patterns = Array.isArray(payload?.codePatterns) ? payload.codePatterns : []; + + for (const pattern of patterns) { + try { + const regex = pattern instanceof RegExp + ? pattern + : new RegExp(String(pattern?.source || pattern || ''), String(pattern?.flags || 'i')); + const match = source.match(regex); + const code = match?.[1] || match?.[0]; + const normalizedCode = String(code || '').match(/\b(\d{6})\b/)?.[1] || ''; + if (normalizedCode) return normalizedCode; + } catch {} + } + + return extractVerificationCode(source); +} + +// 判断节点是否真实可见且占据页面空间。 +function isVisibleElement(node) { + if (!(node instanceof HTMLElement)) return false; + const style = getComputedStyle(node); + if (style.display === 'none' || style.visibility === 'hidden') return false; + const rect = node.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; +} + +function isElementInViewport(node) { + if (!(node instanceof HTMLElement) || !isVisibleElement(node)) return false; + const rect = node.getBoundingClientRect(); + const viewportWidth = Number(window.innerWidth) || document.documentElement?.clientWidth || 0; + const viewportHeight = Number(window.innerHeight) || document.documentElement?.clientHeight || 0; + if (!viewportWidth || !viewportHeight) return true; + return rect.bottom > 0 + && rect.right > 0 + && rect.top < viewportHeight + && rect.left < viewportWidth; +} + +// 根据当前 URL 和页面文案判断是否像 Yahoo 未登录状态。 +function looksLikeYahooLoggedOut() { + const url = String(location.href || '').trim(); + + if (/login\.yahoo\.com/i.test(url)) { + return true; + } + + if (/\/account\//i.test(url) && !/mail\.yahoo\.com/i.test(url)) { + return true; + } + + if (isYahooInboxPage(url) || isYahooSettingsPage(url)) { + return false; + } + + const mailboxIndicators = [ + '[data-test-id="message-list"]', + '[data-test-id="virtual-list"]', + '[role="main"]', + 'main', + 'a[href*="/n/inbox"]', + 'a[href*="/d/folders"]', + 'a[href*="/b/folders"]', + 'a[href*="/n/settings"]', + ]; + + if (mailboxIndicators.some((selector) => document.querySelector(selector))) { + return false; + } + + const bodyText = normalizeLowerText(document.body?.innerText || ''); + return /sign in to yahoo mail|yahoo mail sign in|登录 yahoo|登入 yahoo/.test(bodyText); +} + +// 校验当前是否已登录 Yahoo Mail,不满足时直接抛错。 +function ensureYahooLoggedIn(actionLabel = 'Yahoo 邮箱操作') { + const url = String(location.href || '').trim(); + const hasMessageList = Boolean(document.querySelector('[data-test-id="message-list"]')); + const hasMain = Boolean(document.querySelector('main')); + + if (looksLikeYahooLoggedOut()) { + const bodySample = normalizeLowerText(document.body?.innerText || '').slice(0, 200); + log(`Yahoo:登录态检测失败 url=${url} hasMessageList=${hasMessageList} hasMain=${hasMain} bodySample=${bodySample}`, 'warn'); + throw new Error(`${actionLabel}失败:当前未登录 Yahoo Mail,请先手动登录后重试。`); + } + + log(`Yahoo:登录态检测通过 url=${url} hasMessageList=${hasMessageList} hasMain=${hasMain}`, 'info'); +} + +function isYahooLoginPage(url = location.href) { + try { + const parsed = new URL(String(url || ''), location.origin); + return /(^|\.)login\.yahoo\.com$/i.test(parsed.hostname || '') + || /(^|\.)guce\.yahoo\.com$/i.test(parsed.hostname || ''); + } catch { + return /login\.yahoo\.com|guce\.yahoo\.com/i.test(String(url || '')); + } +} + +function findYahooLoginEmailInput() { + return Array.from(document.querySelectorAll([ + 'input#login-username', + 'input[name="username"]', + 'input[name="login"]', + 'input[type="email"]', + 'input[autocomplete="username"]', + ].join(','))).find((node) => node instanceof HTMLInputElement && isVisibleElement(node)) || null; +} + +function findYahooLoginPasswordInput() { + return Array.from(document.querySelectorAll([ + 'input#login-passwd', + 'input[name="password"]', + 'input[type="password"]', + 'input[autocomplete="current-password"]', + ].join(','))).find((node) => node instanceof HTMLInputElement && isVisibleElement(node)) || null; +} + +function findYahooLoginSubmitButton() { + const selectors = [ + 'button#login-signin', + 'input#login-signin', + 'button[name="signin"]', + 'input[name="signin"]', + 'button[type="submit"]', + 'input[type="submit"]', + ]; + return Array.from(document.querySelectorAll(selectors.join(','))) + .find((node) => node instanceof HTMLElement && isVisibleElement(node) && !node.disabled) || null; +} + +async function waitForYahooLoginPasswordInput(timeout = 15000) { + const startedAt = Date.now(); + while (Date.now() - startedAt < timeout) { + throwIfStopped(); + const input = findYahooLoginPasswordInput(); + if (input) return input; + await sleep(250); + } + return findYahooLoginPasswordInput(); +} + +async function clickYahooLoginButton(label) { + const button = findYahooLoginSubmitButton(); + if (!button) { + const activeInput = document.activeElement; + if (activeInput instanceof HTMLElement) { + activeInput.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter' })); + activeInput.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter' })); + return true; + } + return false; + } + try { + button.scrollIntoView?.({ block: 'center', inline: 'center' }); + } catch {} + await sleep(120); + const rect = button.getBoundingClientRect(); + const x = Math.round(rect.left + Math.max(4, Math.min(rect.width / 2, rect.width - 4))); + const y = Math.round(rect.top + Math.max(4, Math.min(rect.height / 2, rect.height - 4))); + dispatchClickSequence(button, x, y); + log(`Yahoo:已点击${label}`, 'info'); + return true; +} + +async function handleYahooLoginWithCredentials(payload = {}) { + if (!isYahooLoginPage()) { + return { ok: true, skipped: true, reason: 'not_yahoo_login_page', url: location.href }; + } + + const email = String(payload.email || '').trim(); + const password = String(payload.password || ''); + if (!email || !password) { + return { ok: true, skipped: true, reason: 'missing_credentials' }; + } + + let passwordInput = findYahooLoginPasswordInput(); + if (!passwordInput) { + const emailInput = findYahooLoginEmailInput(); + if (!emailInput) { + throw new Error('Yahoo 登录页未找到邮箱输入框。'); + } + fillInput(emailInput, email); + log(`Yahoo:已填写登录邮箱 ${email},准备进入密码页。`, 'info'); + await sleep(180); + await clickYahooLoginButton('Yahoo 登录下一步按钮'); + passwordInput = await waitForYahooLoginPasswordInput(); + } + + if (!passwordInput) { + throw new Error('Yahoo 登录页未进入密码输入步骤。'); + } + + fillInput(passwordInput, password); + log('Yahoo:已填写登录密码,准备提交。', 'info'); + await sleep(180); + await clickYahooLoginButton('Yahoo 登录提交按钮'); + return { ok: true, submitted: true }; +} + +// 判断指定地址是否属于 Yahoo 收件箱页面。 +function isYahooInboxPage(url = location.href) { + const value = String(url || '').trim(); + + try { + const parsed = new URL(value, location.origin); + const isYahooMailHost = /(^|\.)mail\.yahoo\.com$/i.test(parsed.hostname); + const isInboxPath = /^\/(?:n|d|b)\/(?:inbox|folders(?:\/\d+)?)/i.test(parsed.pathname || ''); + if (isYahooMailHost && isInboxPath) { + return true; + } + } catch {} + + if (/\/(?:n|d|b)\/(?:inbox|folders(?:\/\d+)?)(?:[/?#].*)?$/i.test(value)) { + return true; + } + + if (document.querySelector('[data-test-id="message-list"], [data-test-id="virtual-list"]')) { + return true; + } + + return false; +} + +// 根据页面文本判断是否像 Yahoo 设置页 DOM。 +function looksLikeYahooSettingsDom(root = document) { + const text = normalizeLowerText(root?.body?.innerText || root?.body?.textContent || root?.innerText || root?.textContent || ''); + return /一次性电子邮件地址|disposable email addresses|disposable email address|disposable address/.test(text) + || /mailboxes|邮箱列表|auto-forwarding|自动转发/.test(text); +} + +// 判断指定地址或 DOM 是否属于 Yahoo 设置页。 +function isYahooSettingsPage(url = location.href) { + const value = String(url || '').trim(); + if (/https:\/\/mail\.yahoo\.com\/(?:n|d|b)\/settings(?:\/2)?(?:[/?#].*)?$/i.test(value)) { + return true; + } + if (/\/(?:n|d|b)\/settings(?:\/2)?(?:[/?#].*)?$/i.test(value)) { + return true; + } + if (/\/settings(?:\/2)?(?:[/?#].*)?$/i.test(value)) { + return true; + } + return looksLikeYahooSettingsDom(document); +} + +// 规范化 Yahoo URL,去掉 hash 便于比较。 +function normalizeComparableYahooUrl(url) { + try { + const value = new URL(String(url || ''), location.origin); + value.hash = ''; + return value.toString(); + } catch { + return String(url || '').trim(); + } +} + +// 查找当前页面可用的 Yahoo“收件箱”入口,优先复用站内导航而不是直接要求后台重开。 +function findYahooInboxLink() { + const directSelectors = [ + 'a[data-test-folder-name="Inbox"]', + 'a[href*="/n/inbox"]', + 'a[href*="/d/folders"]', + 'a[href*="/b/folders"]', + 'button[aria-label*="Inbox"]', + 'button[aria-label*="收件箱"]', + '[role="button"][aria-label*="Inbox"]', + '[role="button"][aria-label*="收件箱"]', + ]; + + for (const selector of directSelectors) { + const matched = Array.from(document.querySelectorAll(selector)).find((node) => isVisibleElement(node)); + if (matched) { + return matched; + } + } + + return Array.from(document.querySelectorAll('a[href], button, [role="button"]')).find((node) => { + if (!(node instanceof HTMLElement) || !isVisibleElement(node)) return false; + const href = String(node.getAttribute?.('href') || '').trim(); + const text = normalizeLowerText(node.getAttribute?.('aria-label') || node.textContent || node.title || ''); + return /\/n\/inbox/i.test(href) || /^(inbox|收件箱)$/.test(text) || /收件箱|inbox/.test(text); + }) || null; +} + +// 等待 Yahoo 收件箱列表真正可见,避免只是 URL 改了但列表尚未 ready。 +async function waitForYahooInboxReady(timeout = 10000) { + const startedAt = Date.now(); + while (Date.now() - startedAt < timeout) { + throwIfStopped(); + if (isYahooInboxPage(location.href) && isYahooInboxListVisible()) { + return true; + } + await sleep(250); + } + return isYahooInboxPage(location.href) && isYahooInboxListVisible(); +} + +// 确保当前处在 Yahoo 收件箱页;优先在当前页内自愈跳转到收件箱,失败后再交由后台重开。 +async function ensureOnYahooInbox() { + ensureYahooLoggedIn('读取 Yahoo 邮件'); + const currentUrl = normalizeComparableYahooUrl(location.href); + const targetUrl = normalizeComparableYahooUrl(YAHOO_INBOX_URL); + + if (isYahooInboxPage(currentUrl) && isYahooInboxListVisible()) { + return; + } + + log(`Yahoo:当前未处于可读取的收件箱列表,先尝试在当前页内切回收件箱 current=${currentUrl} target=${targetUrl}`, 'warn'); + + for (let attempt = 1; attempt <= 3; attempt += 1) { + throwIfStopped(); + + const inboxLink = findYahooInboxLink(); + if (inboxLink) { + const rect = inboxLink.getBoundingClientRect(); + const clickX = Math.round(rect.left + Math.max(6, Math.min(rect.width / 2, Math.max(rect.width - 6, 6)))); + const clickY = Math.round(rect.top + Math.max(6, Math.min(rect.height / 2, Math.max(rect.height - 6, 6)))); + log(`Yahoo:第 ${attempt} 次尝试点击站内”收件箱”入口回到 inbox tag=${inboxLink.tagName || 'unknown'} pos=${clickX},${clickY}`, 'info'); + try { + dispatchHoverSequence(inboxLink, clickX, clickY); + await sleep(120); + dispatchClickSequence(inboxLink, clickX, clickY); + if (typeof inboxLink.click === 'function') inboxLink.click(); + } catch (err) { + log(`Yahoo:收件箱链接点击失败 attempt=${attempt} error=${err?.message || String(err)}`, 'warn'); + } + await sleep(1200); + if (await waitForYahooInboxReady(5000)) { + log(`Yahoo:已在当前页内成功切回收件箱,第 ${attempt} 次尝试完成`, 'ok'); + return; + } + } + + if (isYahooInboxPage(location.href)) { + const refreshed = await refreshYahooInboxList(attempt); + await sleep(1000); + if (refreshed && await waitForYahooInboxReady(4000)) { + log(`Yahoo:当前已位于收件箱 URL,经过第 ${attempt} 次页内刷新后列表已可读`, 'ok'); + return; + } + } + } + + log(`Yahoo:当前不在目标收件箱页,且页内自愈失败,交由后台重开标签页 current=${normalizeComparableYahooUrl(location.href)} target=${targetUrl}`, 'warn'); + throw new Error(`YAHOO_INBOX_REOPEN_REQUIRED::${YAHOO_INBOX_URL}`); +} + +// 确保当前处在 Yahoo 设置页,不在则尝试从站内入口切过去。 +async function ensureOnYahooSettings() { + ensureYahooLoggedIn('创建 Yahoo 临时邮箱'); + if (isYahooSettingsPage(location.href)) { + log(`Yahoo:已位于设置页 href=${location.href}`, 'info'); + return; + } + + log(`Yahoo:当前不在设置页,尝试使用站内入口切换 href=${location.href}`, 'warn'); + + const settingsLink = Array.from(document.querySelectorAll('a[href], button, [role="button"]')).find((node) => { + const href = node.getAttribute?.('href') || ''; + const text = normalizeLowerText(node.getAttribute?.('aria-label') || node.textContent || node.title || ''); + return /\/settings(?:\/2)?(?:[/?#].*)?$/i.test(href) + || /settings/.test(text); + }); + + if (settingsLink) { + try { + settingsLink.click?.(); + } catch (err) { + log(`Yahoo:设置链接点击失败 error=${err?.message || String(err)}`, 'warn'); + } + await sleep(1200); + if (isYahooSettingsPage(location.href)) { + log(`Yahoo:点击设置入口后已进入设置页 href=${location.href}`, 'info'); + return; + } + } + + throw new Error(`YAHOO_SETTINGS_REOPEN_REQUIRED::${YAHOO_SETTINGS_URL}`); +} + +// 等待给定选择器中任意一个在页面上变得可见。 +async function waitForAnySelector(selectors, timeout = 10000) { + const startedAt = Date.now(); + while (Date.now() - startedAt < timeout) { + throwIfStopped(); + for (const selector of selectors) { + const node = document.querySelector(selector); + if (node && isVisibleElement(node)) { + return node; + } + } + await sleep(200); + } + throw new Error(`等待 Yahoo 页面元素超时:${selectors.join(' | ')}`); +} + +// 判断节点是否像 Yahoo 收件箱中的邮件行。 +function isLikelyYahooMessageRow(node) { + if (!(node instanceof HTMLElement) || !isElementInViewport(node)) return false; + const text = getYahooRowFullText(node); + if (!text || text.length < 12) return false; + + const className = String(node.className || ''); + if (/\bH_A\b/.test(className) && /\bhd_n\b/.test(className) && /\bp_a\b/.test(className) && /\bL_0\b/.test(className) && /\bR_0\b/.test(className)) { + return true; + } + + const hasCheckbox = Boolean(node.querySelector('input[type="checkbox"], [role="checkbox"], [data-test-id="checkbox"]')); + const hasTime = Boolean(node.querySelector('time, [datetime]')) || /\b\d{1,2}:\d{2}\s?(?:am|pm)\b/i.test(text) || /\b\d{1,2}:\d{2}\b/.test(text); + const hasMailKeyword = /openai|no-?reply@|signin\.aws|amazon\s+web\s+services|aws|构建者|chatgpt|verification|verify|验证码|安全码|临时/i.test(text) || /@/.test(text) || /\b\d{6}\b/.test(text); + const hasSender = /OpenAI|ChatGPT|no-?reply|signin\.aws|Amazon Web Services/i.test(text); + + return hasMailKeyword && (hasCheckbox || hasTime || hasSender); +} + +function getYahooMessageQueryRoots() { + const roots = [ + document.querySelector('[data-test-id="message-list"]'), + document.querySelector('[data-test-id="virtual-list"]'), + document.querySelector('#mail-app-component'), + document.querySelector('main [role="list"]'), + document.querySelector('main ul'), + document.querySelector('main ol'), + document.querySelector('main'), + document, + ]; + return roots.filter((root, index, array) => root && array.indexOf(root) === index); +} + +function collectLimitedNodes(root, selector, limit = YAHOO_ROW_SCAN_LIMIT) { + if (!root || typeof root.querySelectorAll !== 'function') { + return []; + } + const nodes = []; + for (const node of root.querySelectorAll(selector)) { + nodes.push(node); + if (nodes.length >= limit) { + break; + } + } + return nodes; +} + +// 收集并排序当前页面上的 Yahoo 邮件行。 +function getMessageRows() { + const roots = getYahooMessageQueryRoots(); + const exactRows = roots.flatMap((root) => collectLimitedNodes(root, 'li.H_A.hd_n.p_a.L_0.R_0')) + .filter((node) => isLikelyYahooMessageRow(node)) + .filter((node, index, arr) => arr.indexOf(node) === index) + .sort((left, right) => { + const leftTop = parseFloat(String(left.style?.top || '').replace('px', '')); + const rightTop = parseFloat(String(right.style?.top || '').replace('px', '')); + const safeLeftTop = Number.isFinite(leftTop) ? leftTop : left.getBoundingClientRect().top; + const safeRightTop = Number.isFinite(rightTop) ? rightTop : right.getBoundingClientRect().top; + if (Math.abs(safeLeftTop - safeRightTop) > 1) return safeLeftTop - safeRightTop; + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + return leftRect.left - rightRect.left; + }); + if (exactRows.length) return exactRows; + + const selectors = [ + '[data-test-id="message-list-item"]', + '[data-test-id*="message-list-item"]', + '[data-test-id="message-list"] [role="row"]', + '[data-test-id="message-list"] li', + '[data-test-id="message-list"] > div', + '[data-test-id="virtual-list"] [role="row"]', + '[data-test-id="virtual-list"] li', + 'div[role="row"]', + 'ul[role="list"] li', + 'main li', + 'table tr', + ]; + + for (const selector of selectors) { + const rows = roots.flatMap((root) => collectLimitedNodes(root, selector)) + .filter((node) => isLikelyYahooMessageRow(node)) + .filter((node, index, arr) => arr.indexOf(node) === index) + .sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + if (Math.abs(leftRect.top - rightRect.top) > 4) return leftRect.top - rightRect.top; + return leftRect.left - rightRect.left; + }); + if (rows.length) { + return rows; + } + } + + return []; +} + +function isKiroYahooTopMessagePayload(payload = {}) { + if (String(payload?.flowId || '').trim().toLowerCase() === 'kiro') { + return true; + } + const filters = [ + ...(Array.isArray(payload?.senderFilters) ? payload.senderFilters : []), + ...(Array.isArray(payload?.subjectFilters) ? payload.subjectFilters : []), + ...(Array.isArray(payload?.requiredKeywords) ? payload.requiredKeywords : []), + ].map(normalizeLowerText).join(' '); + return /signin\.aws|aws builder id|构建者|kiro/.test(filters); +} + +function getFastVisibleYahooMessageRows(limit = 12) { + const selectors = [ + 'li[data-test-id="message-list-item"]', + '[data-test-id="message-list"] li[data-test-id="message-list-item"]', + '[data-test-id="virtual-list"] li[data-test-id="message-list-item"]', + 'li.H_A.hd_n.p_a.L_0.R_0', + ]; + const rows = []; + for (const selector of selectors) { + for (const row of document.querySelectorAll(selector)) { + if (!(row instanceof HTMLElement)) continue; + if (!isElementInViewport(row)) continue; + if (rows.includes(row)) continue; + rows.push(row); + if (rows.length >= limit) break; + } + if (rows.length >= limit) break; + } + return rows.sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + if (Math.abs(leftRect.top - rightRect.top) > 4) return leftRect.top - rightRect.top; + return leftRect.left - rightRect.left; + }); +} + +function rowLooksLikeKiroAwsMessage(text = '') { + const lower = normalizeLowerText(text); + return /no-?reply@signin\.aws|signin\.aws|amazon web services|aws/.test(lower) + && /构建者|builder id|verification|验证码|code/.test(lower); +} + +function buildYahooNoCodeTopRowResult(row, rowText, reason, topRowTimestamp = 0) { + return { + ok: false, + code: null, + reason, + preview: rowText.slice(0, 200), + emailTimestamp: topRowTimestamp || Date.now(), + topMessageFingerprint: buildYahooTopMessageFingerprint(row, rowText, null, topRowTimestamp || Date.now()), + }; +} + +function handleKiroYahooTopMessageFastPath(step, payload = {}) { + const rows = getFastVisibleYahooMessageRows(12); + log(`Yahoo:Kiro 快速顶部邮件扫描 rows=${rows.length}`, 'warn'); + if (!rows.length) { + return { + ok: false, + code: null, + reason: 'Kiro 快速扫描未找到可见 Yahoo 邮件行', + preview: '', + emailTimestamp: Date.now(), + topMessageFingerprint: '', + }; + } + + const topRow = rows[0]; + const rowText = getYahooRowFullText(topRow); + const topRowTimestamp = getRowTimestamp(topRow) || Date.now(); + if (!rowText) { + return buildYahooNoCodeTopRowResult(topRow, '', 'Kiro 快速扫描顶部邮件没有可读取文本', topRowTimestamp); + } + + const filterMatched = rowMatchesFilters(rowText, payload) || rowLooksLikeKiroAwsMessage(rowText); + if (!filterMatched) { + return buildYahooNoCodeTopRowResult(topRow, rowText, 'Kiro 快速扫描顶部邮件未命中 AWS/Kiro 过滤条件', topRowTimestamp); + } + + const code = extractVerificationCodeWithPatterns(rowText, payload); + if (!code) { + return buildYahooNoCodeTopRowResult(topRow, rowText, 'Kiro 快速扫描顶部 AWS/Kiro 邮件中未提取到验证码', topRowTimestamp); + } + + const rowCode = { code, text: rowText, source: 'fast-row-snippet' }; + const freshness = evaluateYahooTopMessageFreshness(topRow, rowText, rowCode, topRowTimestamp, payload, true); + if (!freshness.freshnessMatched) { + log(`Yahoo:Kiro 快速扫描验证码 ${code} 缺少新鲜度证据:${freshness.freshnessReason}`, 'warn'); + return { + ok: false, + code: null, + reason: `顶部验证码存在,但缺少本轮新邮件证据:${freshness.freshnessReason}`, + preview: rowText.slice(0, 200), + emailTimestamp: topRowTimestamp, + topMessageFingerprint: freshness.topMessageFingerprint, + }; + } + + log(`Yahoo:Kiro 快速扫描已从顶部邮件行提取验证码 ${code} preview=${rowText.slice(0, 180)}`, 'warn'); + return { + ok: true, + code, + emailTimestamp: topRowTimestamp, + preview: rowText.slice(0, 200), + topMessageFingerprint: freshness.topMessageFingerprint, + freshnessMatched: freshness.freshnessMatched, + freshnessReason: freshness.freshnessReason, + }; +} + +// 取出最靠前或时间最靠上的候选邮件行。 +function getLatestYahooCodeRow() { + const rows = getMessageRows(); + if (!rows.length) return null; + + return findTopMessageRow(rows); +} + +// 判断当前是否已经展示了收件箱列表。 +function isYahooInboxListVisible() { + const listRoot = document.querySelector('[data-test-id="message-list"], [data-test-id="virtual-list"], main [role="list"], main ul, main ol, #mail-app-component'); + if (listRoot && isVisibleElement(listRoot)) { + return true; + } + return getYahooMessageQueryRoots().some((root) => collectLimitedNodes(root, 'li.H_A.hd_n.p_a.L_0.R_0, [data-test-id="message-list-item"], [data-test-id*="message-list-item"], [data-test-id="message-list"] [role="row"], [data-test-id="virtual-list"] [role="row"], div[role="row"], table tr', 8) + .some((node) => isLikelyYahooMessageRow(node))); +} + +// 尝试通过刷新按钮或收件箱入口拉取最新邮件列表。 +async function refreshYahooInboxList(attempt) { + try { + const refreshBtnCandidates = Array.from(document.querySelectorAll('button, [role="button"], span, div')).filter((node) => { + if (!(node instanceof HTMLElement) || !isVisibleElement(node)) return false; + const text = normalizeLowerText(node.getAttribute?.('aria-label') || node.textContent || node.title || ''); + return /refresh|check for new|new mail|更新|刷新|检查新邮件/.test(text); + }); + + const refreshBtn = refreshBtnCandidates[0] + || document.querySelector('button[data-test-id="icon-btn-refresh"]') + || document.querySelector('button[aria-label*="Refresh"], button[title*="Refresh"], button[aria-label*="刷新"], button[title*="刷新"]'); + + if (refreshBtn instanceof HTMLElement && isVisibleElement(refreshBtn)) { + log(`Yahoo:尝试点击刷新按钮拉取新邮件(第 ${attempt} 次)...`, 'info'); + simulateClick(refreshBtn); + await sleep(1000); + if (isYahooInboxListVisible()) return true; + } + + const inboxLinks = Array.from(document.querySelectorAll('a[data-test-folder-name="Inbox"], a[href*="/n/inbox"], a[href*="/d/folders"], a[href*="/b/folders"], button[aria-label*="Inbox"], button[aria-label*="收件箱"]')) + .filter((node) => node instanceof HTMLElement && isVisibleElement(node)); + const inboxBtn = inboxLinks[0] || null; + + if (inboxBtn) { + const rect = inboxBtn.getBoundingClientRect(); + const clickX = Math.round(rect.left + Math.min(Math.max(rect.width / 2, 5), Math.max(rect.width - 5, 5))); + const clickY = Math.round(rect.top + Math.min(Math.max(rect.height / 2, 5), Math.max(rect.height - 5, 5))); + log(`Yahoo:尝试点击收件箱按钮刷新列表(第 ${attempt} 次)...`, 'info'); + dispatchHoverSequence(inboxBtn, clickX, clickY); + await sleep(120); + dispatchClickSequence(inboxBtn, clickX, clickY); + try { + if (typeof inboxBtn.click === 'function') inboxBtn.click(); + } catch (err) { + log(`Yahoo:收件箱按钮点击失败 error=${err?.message || String(err)}`, 'warn'); + } + await sleep(1000); + if (isYahooInboxListVisible()) return true; + } + } catch (error) { + log(`Yahoo:尝试刷新收件箱时发生异常忽略:${error?.message || error}`, 'warn'); + } + + return isYahooInboxListVisible(); +} + +// 直接从邮件行文本里提取 6 位数字验证码。 +function extractSixDigitCodeFromLatestRow(row) { + if (!(row instanceof HTMLElement)) return null; + + const text = getYahooRowFullText(row); + + const match = text.match(/\b(\d{6})\b/); + if (!match) return null; + + return { + code: match[1], + text, + source: 'row-text', + }; +} + +function getYahooRowFullText(row) { + if (!(row instanceof HTMLElement)) return ''; + const explicitTextNodes = Array.from(row.querySelectorAll([ + '[id^="email-sender-"]', + '[id^="email-subject-snippet-"]', + '[id^="email-subject-"]', + '[id^="email-snippet-"]', + '[id^="email-date-"]', + '[title]', + ].join(','))).map((node) => [ + node.getAttribute?.('aria-label'), + node.getAttribute?.('title'), + node.innerText, + node.textContent, + ].filter(Boolean).join(' ')); + + return joinUniqueTextParts([ + row.getAttribute('aria-label'), + row.getAttribute('title'), + row.innerText, + row.textContent, + ...explicitTextNodes, + ]); +} + +// 获取邮件行的可读文本。 +function getRowText(row) { + return getYahooRowFullText(row); +} + +// 找出列表中最靠上的可见邮件行。 +function findTopMessageRow(rows = []) { + return (Array.isArray(rows) ? rows : []) + .filter((row) => row instanceof HTMLElement && isElementInViewport(row)) + .sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + if (Math.abs(leftRect.top - rightRect.top) > 4) return leftRect.top - rightRect.top; + return leftRect.left - rightRect.left; + })[0] || null; +} + +// 尝试把 Yahoo 邮件时间文本解析成时间戳。 +function parseYahooMailboxTimestampCandidate(value) { + const text = normalizeText(value); + if (!text) return null; + + const relativeCnMatch = text.match(/(\d{1,3})\s*(秒|分钟|分|小时|天)前/); + if (relativeCnMatch) { + const amount = Number(relativeCnMatch[1]); + const unit = relativeCnMatch[2] || ''; + const unitMs = /秒/.test(unit) + ? 1000 + : (/分|分钟/.test(unit) + ? 60 * 1000 + : (/小时/.test(unit) + ? 60 * 60 * 1000 + : 24 * 60 * 60 * 1000)); + return Date.now() - (amount * unitMs); + } + + const relativeEnMatch = text.match(/(\d{1,3})\s*(second|sec|minute|min|hour|hr|day)s?\s*ago/i); + if (relativeEnMatch) { + const amount = Number(relativeEnMatch[1]); + const unit = normalizeLowerText(relativeEnMatch[2] || ''); + const unitMs = /second|sec/.test(unit) + ? 1000 + : (/minute|min/.test(unit) + ? 60 * 1000 + : (/hour|hr/.test(unit) + ? 60 * 60 * 1000 + : 24 * 60 * 60 * 1000)); + return Date.now() - (amount * unitMs); + } + + if (/^(just now|刚刚|刚才)$/i.test(text)) { + return Date.now(); + } + + const directParsed = Date.parse(text); + if (Number.isFinite(directParsed)) { + return directParsed; + } + + const localizedDateTimeMatch = text.match(/(\d{1,2})月(\d{1,2})日(?:\s*(上午|下午))?\s*(\d{1,2}):(\d{2})/); + if (localizedDateTimeMatch) { + const now = new Date(); + let hour = Number(localizedDateTimeMatch[4]); + const minute = Number(localizedDateTimeMatch[5]); + const meridiem = localizedDateTimeMatch[3] || ''; + if (meridiem === '下午' && hour < 12) hour += 12; + if (meridiem === '上午' && hour === 12) hour = 0; + const parsed = new Date(now.getFullYear(), Number(localizedDateTimeMatch[1]) - 1, Number(localizedDateTimeMatch[2]), hour, minute, 0, 0).getTime(); + return Number.isFinite(parsed) ? parsed : null; + } + + const timeMatch = text.match(/(?:(今天|today|昨天|yesterday)\s*)?(?:(上午|下午)\s*)?(\d{1,2}):(\d{2})(?:\s*(am|pm))?/i); + if (timeMatch) { + let hour = Number(timeMatch[3]); + const minute = Number(timeMatch[4]); + const dayToken = normalizeLowerText(timeMatch[1] || ''); + const meridiemCn = timeMatch[2] || ''; + const meridiemEn = normalizeLowerText(timeMatch[5] || ''); + if ((meridiemCn === '下午' || meridiemEn === 'pm') && hour < 12) hour += 12; + if ((meridiemCn === '上午' || meridiemEn === 'am') && hour === 12) hour = 0; + const now = new Date(); + if (dayToken === '昨天' || dayToken === 'yesterday') { + now.setDate(now.getDate() - 1); + } + now.setHours(hour, minute, 0, 0); + return now.getTime(); + } + + return null; +} + +// 从邮件行的多个时间候选字段里提取时间戳。 +function getRowTimestamp(row) { + const timeNode = row.querySelector('time, [datetime], [title]'); + const candidates = [ + timeNode?.getAttribute?.('datetime'), + timeNode?.getAttribute?.('title'), + timeNode?.textContent, + row.getAttribute?.('aria-label'), + row.innerText, + row.textContent, + ].filter(Boolean); + + for (const candidate of candidates) { + const parsed = parseYahooMailboxTimestampCandidate(candidate); + if (Number.isFinite(parsed)) return parsed; + } + + return 0; +} + +// 判断邮件文本是否命中发件人或主题过滤条件。 +function rowMatchesFilters(text, payload) { + const lower = normalizeLowerText(text); + const senderFilters = (payload.senderFilters || []).map(normalizeLowerText).filter(Boolean); + const subjectFilters = (payload.subjectFilters || []).map(normalizeLowerText).filter(Boolean); + const senderMatch = senderFilters.length === 0 ? true : senderFilters.some((item) => lower.includes(item)); + const subjectMatch = subjectFilters.length === 0 ? true : subjectFilters.some((item) => lower.includes(item)); + return senderMatch || subjectMatch; +} + +// 预编译正则表达式以提升性能 +const YAHOO_FINGERPRINT_PATTERNS = [ + /\b\d{1,3}\s*(?:seconds?|secs?|minutes?|mins?|hours?|hrs?|days?)\s+ago\b/gi, + /\b(?:just now|today|yesterday)\b/gi, + /\d{1,3}\s*(?:秒|分钟|分|小时|天)前/g, + /(?:今天|昨天)\s*/g, + /(?:上午|下午)?\s*\d{1,2}:\d{2}(?:\s*(?:am|pm))?/gi, + /\s+/g +]; + +function normalizeYahooFingerprintText(value) { + let text = normalizeText(String(value || '')); + + // 使用预编译的正则表达式批量替换 + for (const pattern of YAHOO_FINGERPRINT_PATTERNS) { + text = text.replace(pattern, ' '); + } + + return text.trim().slice(0, 240); +} + +function buildYahooTopMessageFingerprint(row, rowText, rowCode, rowTimestamp) { + if (!(row instanceof HTMLElement)) { + return ''; + } + + const normalizedRowText = normalizeYahooFingerprintText(rowText); + const identityParts = [ + row.getAttribute('data-id'), + row.getAttribute('data-test-id'), + row.getAttribute('id'), + row.dataset?.testid, + row.dataset?.id, + row.querySelector('a[href]')?.getAttribute('href'), + row.querySelector('time, [datetime]')?.getAttribute('datetime'), + rowCode?.code || '', + normalizedRowText, + ].map((item) => normalizeText(item)).filter(Boolean); + + return identityParts.join(' | '); +} + +function evaluateYahooTopMessageFreshness(row, rowText, rowCode, rowTimestamp, payload = {}, filterMatched = false) { + const topMessageFingerprint = buildYahooTopMessageFingerprint(row, rowText, rowCode, rowTimestamp); + const previousTopMessageFingerprint = normalizeText(payload.previousTopMessageFingerprint || ''); + const previousAcceptedEmailTimestamp = Math.max(0, Number(payload.previousAcceptedEmailTimestamp || 0) || 0); + const requestedAt = Math.max(0, Number(payload.requestedAt || 0) || 0); + const freshnessSkewMs = Math.max(60000, Number(payload.yahooFreshnessSkewMs || 180000) || 180000); + const topRowCodeFallbackMatched = Boolean(payload.yahooTopRowOnly && rowCode?.code); + + const fingerprintChanged = Boolean(previousTopMessageFingerprint) + && Boolean(topMessageFingerprint) + && topMessageFingerprint !== previousTopMessageFingerprint; + const timestampNotOlderThanRequest = requestedAt > 0 + && rowTimestamp > 0 + && rowTimestamp >= requestedAt - freshnessSkewMs; + const timestampAdvancedBeyondPreviousSuccess = previousAcceptedEmailTimestamp > 0 + && rowTimestamp > 0 + && rowTimestamp > previousAcceptedEmailTimestamp; + const bootstrapFallbackAllowed = !previousTopMessageFingerprint + && previousAcceptedEmailTimestamp <= 0 + && requestedAt > 0 + && Boolean(payload.yahooTopRowOnly) + && Boolean(rowCode?.code) + && (Boolean(filterMatched) || topRowCodeFallbackMatched); + + let freshnessMatched = false; + let freshnessReason = '未获得本轮新邮件证据'; + + if (timestampNotOlderThanRequest) { + freshnessMatched = true; + freshnessReason = `邮件时间接近本轮重发时间 (${rowTimestamp} >= ${requestedAt} - ${freshnessSkewMs})`; + } else if (timestampAdvancedBeyondPreviousSuccess) { + freshnessMatched = true; + freshnessReason = `邮件时间晚于上次已接受邮件 (${rowTimestamp} > ${previousAcceptedEmailTimestamp})`; + } else if (fingerprintChanged) { + freshnessMatched = true; + freshnessReason = '顶部邮件指纹相较上一轮观察已发生变化'; + } else if (bootstrapFallbackAllowed) { + freshnessMatched = true; + freshnessReason = '缺少历史基线,当前顶部验证码邮件按首次候选放行'; + } + + return { + topMessageFingerprint, + freshnessMatched, + freshnessReason, + previousTopMessageFingerprint, + previousAcceptedEmailTimestamp, + requestedAt, + freshnessSkewMs, + topRowCodeFallbackMatched, + fingerprintChanged, + timestampNotOlderThanRequest, + timestampAdvancedBeyondPreviousSuccess, + bootstrapFallbackAllowed, + }; +} + +function findYahooMessageOpenTarget(row) { + if (!(row instanceof HTMLElement)) return null; + const selectors = [ + '[data-test-id="message-list-item"]', + '[data-test-id*="message-list-item"]', + '[data-test-id*="subject"]', + '[data-test-id*="snippet"]', + 'a[href*="/d/"]', + '[role="link"]', + ]; + for (const selector of selectors) { + const target = row.matches?.(selector) ? row : row.querySelector(selector); + if (target instanceof HTMLElement && isElementInViewport(target)) { + return target; + } + } + return row; +} + +function getYahooMessageOpenClickPoint(row, target = null) { + const rowRect = row?.getBoundingClientRect?.() || { left: 0, top: 0, width: 0, height: 0 }; + const targetRect = target?.getBoundingClientRect?.() || rowRect; + const usableRect = targetRect.width > 40 && targetRect.height > 8 ? targetRect : rowRect; + const relativeSafeX = Math.max(140, Math.min(usableRect.width * 0.42, usableRect.width - 80)); + const x = Math.round(usableRect.left + Math.max(8, relativeSafeX)); + const y = Math.round(usableRect.top + Math.max(6, Math.min(usableRect.height / 2, usableRect.height - 6))); + return { x, y }; +} + +function dispatchYahooMessageOpenSequence(target, x, y) { + const hit = document.elementFromPoint?.(x, y); + const clickTarget = hit?.closest?.('[data-test-id*="message-list-item"], [role="link"], a, span, div') + || hit + || target; + const finalTarget = clickTarget instanceof HTMLElement ? clickTarget : target; + + dispatchHoverSequence(finalTarget, x, y); + dispatchClickSequence(finalTarget, x, y); + try { + finalTarget.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true, clientX: x, clientY: y })); + } catch {} + try { + finalTarget.focus?.(); + finalTarget.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter' })); + finalTarget.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter' })); + } catch {} + try { if (typeof finalTarget.click === 'function') finalTarget.click(); } catch {} +} + +async function waitForYahooMessageDetailText(rowText = '', payload = {}, timeoutMs = 6500) { + const startedAt = Date.now(); + let latestText = ''; + while (Date.now() - startedAt < timeoutMs) { + throwIfStopped(); + latestText = normalizeText(document.body?.innerText || document.body?.textContent || ''); + const combinedText = normalizeText([rowText, latestText].filter(Boolean).join(' ')); + if (extractVerificationCodeWithPatterns(combinedText, payload)) { + return latestText; + } + await sleep(350); + } + return latestText; +} + +async function clickYahooMessageOpenTarget(target, row, label = 'primary') { + const { x: clickX, y: clickY } = getYahooMessageOpenClickPoint(row, target); + log(`Yahoo:尝试打开顶部邮件详情 click=${label} target=${target?.tagName || 'unknown'} point=${clickX},${clickY}`, 'info'); + dispatchYahooMessageOpenSequence(target, clickX, clickY); + await sleep(120); +} + +function scheduleYahooMessageOpenTarget(target, row, label = 'scheduled') { + if (!(target instanceof HTMLElement)) return false; + const { x: clickX, y: clickY } = getYahooMessageOpenClickPoint(row, target); + log(`Yahoo:准备异步打开顶部邮件详情 click=${label} target=${target?.tagName || 'unknown'} point=${clickX},${clickY}`, 'info'); + setTimeout(() => { + try { + dispatchYahooMessageOpenSequence(target, clickX, clickY); + } catch (error) { + log(`Yahoo:异步打开顶部邮件详情失败 error=${error?.message || String(error)}`, 'warn'); + } + }, 30); + return true; +} + +// 点击邮件行并读取正文文本。 +async function openRowAndReadText(row, rowText = '', payload = {}) { + const link = findYahooMessageOpenTarget(row); + if (!(link instanceof HTMLElement)) { + return normalizeText(document.body?.innerText || document.body?.textContent || ''); + } + + await clickYahooMessageOpenTarget(link, row, 'subject'); + let detailText = await waitForYahooMessageDetailText(rowText, payload, 3000); + if (extractVerificationCodeWithPatterns(normalizeText([rowText, detailText].join(' ')), payload)) { + return detailText; + } + + if (isYahooInboxListVisible()) { + log('Yahoo:第一次点击后仍停留在收件箱列表,改用邮件行安全区域重试打开详情。', 'warn'); + await clickYahooMessageOpenTarget(row, row, 'row-safe-area'); + } + + detailText = await waitForYahooMessageDetailText(rowText, payload, 6500); + return detailText; +} + +async function openTopRowAndReadVerificationCode(row, rowText, payload = {}) { + log('Yahoo:顶部目标邮件行未直接露出验证码,正在点进邮件详情读取正文。', 'warn'); + if (payload?.deferDetailReadAfterOpen !== false) { + return { + code: null, + text: rowText, + source: 'message-detail-required', + needsOpenDetails: true, + needsDetailRead: true, + }; + } + + const detailText = await openRowAndReadText(row, rowText, payload); + const combinedText = normalizeText([rowText, detailText].filter(Boolean).join(' ')); + const code = extractVerificationCodeWithPatterns(combinedText, payload); + if (!code) { + log(`Yahoo:邮件详情正文仍未提取到验证码 preview=${combinedText.slice(0, 240)}`, 'warn'); + return null; + } + + log(`Yahoo:已从邮件详情正文提取验证码 ${code}`, 'warn'); + return { + code, + text: combinedText, + source: 'message-detail', + }; +} + +async function handleReadCurrentMessageCode(step, payload = {}) { + ensureYahooLoggedIn('读取 Yahoo 邮件正文'); + const text = await waitForYahooMessageDetailText('', payload, 12000); + const code = extractVerificationCodeWithPatterns(text, payload); + if (!code) { + log(`Yahoo:步骤 ${step} 当前邮件详情页未提取到验证码 preview=${text.slice(0, 240)}`, 'warn'); + return { + ok: false, + code: null, + reason: '当前邮件详情页未提取到验证码', + preview: text.slice(0, 200), + emailTimestamp: Date.now(), + }; + } + + log(`Yahoo:步骤 ${step} 已从当前邮件详情页提取验证码 ${code}`, 'warn'); + return { + ok: true, + code, + emailTimestamp: Date.now(), + preview: text.slice(0, 200), + freshnessMatched: true, + freshnessReason: '已打开顶部邮件详情并读取正文', + }; +} + +async function handleOpenTopMessage(step, payload = {}) { + await ensureOnYahooInbox(); + const rows = getMessageRows(); + const topRow = getLatestYahooCodeRow() || findTopMessageRow(rows); + if (!topRow) { + return { + ok: false, + detailOpened: false, + reason: '未找到可打开的 Yahoo 顶部邮件行', + }; + } + const rowText = getRowText(topRow); + const filterMatched = rowMatchesFilters(rowText, payload); + if (!filterMatched && !payload?.forceOpenTopMessage) { + return { + ok: false, + detailOpened: false, + reason: '顶部邮件未命中过滤条件,未打开详情', + preview: rowText.slice(0, 200), + }; + } + const target = findYahooMessageOpenTarget(topRow); + const scheduled = scheduleYahooMessageOpenTarget(target, topRow, 'open-command'); + return { + ok: scheduled, + detailOpened: scheduled, + preview: rowText.slice(0, 200), + emailTimestamp: getRowTimestamp(topRow) || Date.now(), + topMessageFingerprint: buildYahooTopMessageFingerprint(topRow, rowText, null, getRowTimestamp(topRow) || Date.now()), + }; +} + +// 从邮件行中的 lq_x 结构里提取验证码。 +function getLatestLqxCodeFromRow(row) { + if (!(row instanceof HTMLElement)) return null; + const nodes = Array.from(row.querySelectorAll('.lq_x')).filter((node) => isVisibleElement(node)); + if (!nodes.length) return null; + + const latestNode = nodes.sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + if (Math.abs(leftRect.top - rightRect.top) > 6) return leftRect.top - rightRect.top; + return leftRect.left - rightRect.left; + })[0] || null; + + if (!latestNode) return null; + + const text = normalizeText(latestNode.innerText || latestNode.textContent || latestNode.getAttribute?.('aria-label') || ''); + const code = extractVerificationCode(text); + return code ? { code, text, source: 'lq_x' } : null; +} + +// 从邮件行内的可见文本片段里提取验证码。 +function getTopRowInlineCode(row) { + if (!(row instanceof HTMLElement)) return null; + + const fullText = getYahooRowFullText(row); + const fullTextCode = extractVerificationCode(fullText); + if (fullTextCode) { + return { code: fullTextCode, text: fullText, source: 'full-row-text' }; + } + + const candidates = Array.from(row.querySelectorAll('span, div, a')).filter((node) => { + if (!(node instanceof HTMLElement) || !isVisibleElement(node)) return false; + const text = normalizeText(node.innerText || node.textContent || node.getAttribute?.('aria-label') || ''); + return /\b\d{6}\b/.test(text) || /verification|验证码|临时验证码|code|继续/.test(text); + }).map((node) => { + const text = normalizeText(node.innerText || node.textContent || node.getAttribute?.('aria-label') || ''); + const code = extractVerificationCode(text); + const rect = node.getBoundingClientRect(); + const score = (code ? 100 : 0) + + (/verification|验证码|临时验证码|code/.test(text) ? 20 : 0) + + (/openai|noreply@|chatgpt/.test(text) ? 5 : 0) + - Math.round(rect.left / 100) + - Math.round(rect.top / 50); + return { node, text, code, score, rect }; + }).filter((item) => item.code); + + if (!candidates.length) return null; + + const best = candidates.sort((left, right) => right.score - left.score || left.rect.top - right.rect.top || left.rect.left - right.rect.left)[0]; + return best ? { code: best.code, text: best.text, source: 'inline-row' } : null; +} + +// 从邮件详情页尽量返回到收件箱列表视图。 +async function returnToYahooInboxListFromMessageView() { + if (isYahooInboxListVisible()) { + return true; + } + + const backCandidates = [ + 'button[aria-label^="Back to"]', + 'button[title^="Back to"]', + 'button[aria-label*="返回"]', + 'button[title*="返回"]', + 'a[aria-label*="Inbox"]', + 'a[title*="Inbox"]', + 'a[href*="/n/inbox"]', + 'a[href*="/d/folders"]', + 'a[href*="/b/folders"]', + 'button[aria-label*="Inbox"]', + 'button[aria-label*="收件箱"]', + ]; + + for (const selector of backCandidates) { + const btn = document.querySelector(selector); + if (!(btn instanceof HTMLElement) || !isVisibleElement(btn)) continue; + log(`Yahoo:尝试返回收件箱 selector=${selector}`, 'info'); + simulateClick(btn); + await sleep(1500); + if (isYahooInboxListVisible()) { + return true; + } + } + + if (history.length > 1) { + try { + log('Yahoo:未找到明确返回按钮,尝试 history.back() 返回收件箱', 'warn'); + history.back(); + await sleep(2000); + if (isYahooInboxListVisible()) { + return true; + } + } catch (err) { + log(`Yahoo:history.back() 失败 error=${err?.message || String(err)}`, 'warn'); + } + } + + return false; +} + +// 在两次轮询之间做一次软等待,并记录原因。 +async function addSoftInboxPollDelay(intervalMs, attempt, maxAttempts, reason = '') { + const seconds = Math.max(1, Math.round(intervalMs / 1000)); + log(`Yahoo:第 ${attempt}/${maxAttempts} 次检查未命中验证码,等待 ${seconds} 秒后再次读取当前收件箱。${reason ? `原因:${reason}` : ''}`, 'info'); + await sleep(intervalMs); +} + +// 刷新收件箱前先保证还停留在正确页面。 +async function refreshInbox(attempt) { + ensureYahooLoggedIn('刷新 Yahoo 邮箱'); + const currentUrl = normalizeComparableYahooUrl(location.href); + + if (!isYahooInboxPage(currentUrl)) { + log(`Yahoo:当前页不属于 all 收件箱路径,要求后台先回到 ${YAHOO_INBOX_URL} 再刷新。href=${location.href}`, 'warn'); + throw new Error(`YAHOO_INBOX_REOPEN_REQUIRED::${YAHOO_INBOX_URL}`); + } + + if (!isYahooInboxListVisible()) { + const returned = await returnToYahooInboxListFromMessageView(); + if (!returned) { + log(`Yahoo:当前不在可刷新的收件箱列表视图,要求后台先回到 ${YAHOO_INBOX_URL} 再刷新。href=${location.href}`, 'warn'); + throw new Error(`YAHOO_INBOX_REOPEN_REQUIRED::${YAHOO_INBOX_URL}`); + } + } + + await refreshYahooInboxList(attempt); + await sleep(800); +} + +async function handleCheckTopMessage(step, payload) { + log(`Yahoo:handleCheckTopMessage start step=${step} href=${location.href} excludedCodes=${(payload.excludeCodes || []).join(',') || '(none)'}`, 'warn'); + if (isKiroYahooTopMessagePayload(payload)) { + return handleKiroYahooTopMessageFastPath(step, payload); + } + return handlePollEmail(step, { + ...payload, + yahooTopRowOnly: true, + keepRefreshingUntilCode: false, + maxAttempts: 1, + }); +} + +// 轮询 Yahoo 收件箱,并只读取最顶部那封邮件中的验证码。 +async function handlePollEmail(step, payload) { + await ensureOnYahooInbox(); + await waitForAnySelector([ + '[data-test-id="message-list"]', + 'main', + ], 15000); + ensureYahooLoggedIn('读取 Yahoo 邮件'); + + const excludedCodeSet = new Set((payload.excludeCodes || []).map((item) => String(item || '').trim()).filter(Boolean)); + log(`Yahoo:handlePollEmail start step=${step} href=${location.href} yahooTopRowOnly=${Boolean(payload.yahooTopRowOnly)} excludedCodes=${[...excludedCodeSet].join(',') || '(none)'}`, 'warn'); + const rows = getMessageRows(); + log(`Yahoo:当前可见邮件行数量 rows=${rows.length}`, 'info'); + const topRow = getLatestYahooCodeRow() || findTopMessageRow(rows); + + if (!topRow) { + log(`Yahoo:步骤 ${step} 未找到可见的收件箱顶部邮件行。`, 'warn'); + return { + ok: false, + code: null, + reason: '未找到收件箱顶部邮件行', + preview: '', + emailTimestamp: Date.now(), + }; + } + + const rowText = getRowText(topRow); + const topRowTimestamp = getRowTimestamp(topRow) || Date.now(); + log(`Yahoo:顶部邮件摘要=${rowText.slice(0, 200) || '(empty)'} timestamp=${topRowTimestamp}`, 'warn'); + if (!rowText) { + log(`Yahoo:步骤 ${step} 的顶部邮件行没有可读取文本。`, 'warn'); + return { + ok: false, + code: null, + reason: '收件箱顶部邮件没有可读取文本', + preview: '', + emailTimestamp: getRowTimestamp(topRow) || Date.now(), + topMessageFingerprint: '', + }; + } + + let rowCode = getLatestLqxCodeFromRow(topRow) + || getTopRowInlineCode(topRow) + || extractSixDigitCodeFromLatestRow(topRow); + const filterMatched = rowMatchesFilters(rowText, payload); + const topRowCodeFallbackMatched = Boolean(payload.yahooTopRowOnly && rowCode?.code); + + if (!filterMatched && !topRowCodeFallbackMatched) { + log(`Yahoo:步骤 ${step} 的顶部邮件既未命中过滤条件,也未直接提取到验证码。`, 'warn'); + return { + ok: false, + code: null, + reason: '当前收件箱顶部邮件既未命中过滤条件,也未直接提取到验证码', + preview: rowText.slice(0, 200), + emailTimestamp: getRowTimestamp(topRow) || Date.now(), + topMessageFingerprint: buildYahooTopMessageFingerprint(topRow, rowText, null, topRowTimestamp), + }; + } + + if (!rowCode?.code) { + if (filterMatched) { + rowCode = await openTopRowAndReadVerificationCode(topRow, rowText, payload); + } + } + + if (!rowCode?.code) { + log(`Yahoo:步骤 ${step} 的顶部目标邮件中还没有读到 6 位验证码。`, 'warn'); + if (rowCode?.needsDetailRead) { + return { + ok: false, + code: null, + needsOpenDetails: Boolean(rowCode?.needsOpenDetails), + needsDetailRead: true, + reason: '顶部目标邮件需要打开详情读取正文', + preview: rowText.slice(0, 200), + emailTimestamp: getRowTimestamp(topRow) || Date.now(), + topMessageFingerprint: buildYahooTopMessageFingerprint(topRow, rowText, null, topRowTimestamp), + }; + } + return { + ok: false, + code: null, + reason: '收件箱顶部目标邮件中未提取到验证码', + preview: rowText.slice(0, 200), + emailTimestamp: getRowTimestamp(topRow) || Date.now(), + topMessageFingerprint: buildYahooTopMessageFingerprint(topRow, rowText, null, topRowTimestamp), + }; + } + + if (!filterMatched && topRowCodeFallbackMatched) { + log(`Yahoo:步骤 ${step} 的顶部邮件未命中过滤条件,但已在顶部行直接提取到验证码 ${rowCode.code},按顶部验证码兜底候选继续判定。`, 'warn'); + } + + const freshness = evaluateYahooTopMessageFreshness(topRow, rowText, rowCode, topRowTimestamp, payload, filterMatched); + if (!freshness.freshnessMatched) { + log(`Yahoo:步骤 ${step} 的顶部验证码 ${rowCode.code} 缺少本轮新鲜度证据。reason=${freshness.freshnessReason} fingerprintChanged=${freshness.fingerprintChanged} previousTopFingerprint=${freshness.previousTopMessageFingerprint || '(none)'} requestedAt=${freshness.requestedAt} previousAcceptedEmailTimestamp=${freshness.previousAcceptedEmailTimestamp}`, 'warn'); + return { + ok: false, + code: null, + reason: `顶部验证码存在,但缺少本轮新邮件证据:${freshness.freshnessReason}`, + preview: rowCode.text ? rowCode.text.slice(0, 200) : rowText.slice(0, 200), + emailTimestamp: getRowTimestamp(topRow) || Date.now(), + topMessageFingerprint: freshness.topMessageFingerprint, + }; + } + + if (excludedCodeSet.has(rowCode.code)) { + log(`Yahoo:步骤 ${step} 的顶部验证码 ${rowCode.code} 已被排除。`, 'warn'); + return { + ok: false, + code: null, + reason: `收件箱顶部验证码 ${rowCode.code} 已被排除`, + preview: rowCode.text ? rowCode.text.slice(0, 200) : rowText.slice(0, 200), + emailTimestamp: getRowTimestamp(topRow) || Date.now(), + topMessageFingerprint: freshness.topMessageFingerprint, + }; + } + + const emailTimestamp = getRowTimestamp(topRow) || Date.now(); + log(`Yahoo:已从收件箱顶部邮件命中验证码 code=${rowCode.code} timestamp=${new Date(emailTimestamp).toISOString()} freshness=${freshness.freshnessReason} preview=${String(rowCode.text || rowText).slice(0, 180)}`, 'warn'); + return { + ok: true, + code: rowCode.code, + emailTimestamp, + preview: (rowCode.text || rowText).slice(0, 200), + topMessageFingerprint: freshness.topMessageFingerprint, + freshnessMatched: freshness.freshnessMatched, + freshnessReason: freshness.freshnessReason, + }; +} + +// 给一次性邮箱设置区域打分,帮助定位正确的卡片容器。 +function scoreDisposableAliasSection(node) { + if (!(node instanceof HTMLElement)) return -Infinity; + const text = normalizeLowerText(node.innerText || node.textContent || ''); + if (!/一次性电子邮件地址|disposable email addresses|disposable email address|disposable address/.test(text)) { + return -Infinity; + } + + const rect = node.getBoundingClientRect(); + if (rect.width < 260 || rect.height < 80) return -Infinity; + + let score = 0; + if (/protect your privacy|最多\s*3\s*个免费|during your free trial|protect your real email address|alias email addresses/.test(text)) score += 8; + if (/添加|add/.test(text)) score += 3; + if (/@yahoo\.com/.test(text)) score += 3; + if (/mailbox list|邮箱列表/.test(text)) score -= 8; + if (/send-only email address|仅发送电子邮件地址/.test(text)) score -= 8; + if (/auto-forwarding|自动转发/.test(text)) score -= 5; + if (/mailboxes/.test(text)) score -= 6; + score -= (rect.width * rect.height) / 200000; + return score; +} + +// 找到 Yahoo 一次性邮箱设置区域的主容器。 +function findDisposableAliasSection() { + const heading = Array.from(document.querySelectorAll('h1, h2, h3, h4, div, span')).find((node) => { + const text = normalizeLowerText(node.innerText || node.textContent || ''); + return node instanceof HTMLElement && /一次性电子邮件地址|disposable email addresses|disposable email address|disposable address/.test(text); + }); + + const candidates = []; + if (heading) { + let current = heading.parentElement; + while (current && current !== document.body) { + candidates.push(current); + current = current.parentElement; + } + } + + candidates.push(...Array.from(document.querySelectorAll('section, div, main, article'))); + + return candidates + .filter((node, index, arr) => node instanceof HTMLElement && arr.indexOf(node) === index) + .map((node) => ({ node, score: scoreDisposableAliasSection(node) })) + .filter((item) => Number.isFinite(item.score) && item.score > -Infinity) + .sort((left, right) => right.score - left.score)[0]?.node || null; +} + +async function focusDisposableAliasSection() { + for (let attempt = 1; attempt <= 4; attempt += 1) { + try { + window.scrollTo({ left: 0, behavior: 'instant' }); + document.scrollingElement?.scrollTo?.({ left: 0, behavior: 'instant' }); + } catch { + window.scrollTo(0, window.scrollY || 0); + } + + const section = findDisposableAliasSection(); + const heading = findYahooDisposableSectionHeading(); + const target = heading || section; + + if (target instanceof HTMLElement) { + try { + target.scrollIntoView?.({ block: 'center', inline: 'center' }); + } catch {} + await sleep(500); + const refreshedSection = findDisposableAliasSection(); + const addButton = findYahooDisposableAddButton(); + if (addButton) { + log(`Yahoo:已定位 Disposable email addresses 区域并找到 Add 按钮(attempt=${attempt})`, 'info'); + return { section: refreshedSection || section, addButton }; + } + log(`Yahoo:已滚动到 Disposable email addresses 区域,但暂未找到 Add 按钮(attempt=${attempt})`, 'warn'); + } + + try { + window.scrollBy({ top: attempt % 2 ? 420 : -240, left: -1200, behavior: 'instant' }); + } catch { + window.scrollBy(-1200, attempt % 2 ? 420 : -240); + } + await sleep(450); + } + + return { + section: findDisposableAliasSection(), + addButton: findYahooDisposableAddButton(), + }; +} + +// 在祖先链里找最贴近的一次性邮箱卡片。 +function findTightAliasCard(node) { + if (!(node instanceof HTMLElement)) return null; + const chain = []; + let current = node; + while (current && current !== document.body) { + if (isVisibleElement(current)) { + const rect = current.getBoundingClientRect(); + const text = normalizeLowerText(current.innerText || current.textContent || ''); + if (rect.width >= 220 && rect.height >= 40 && rect.height <= 120 && /@yahoo\.com/.test(text)) { + chain.push(current); + } + } + current = current.parentElement; + } + return chain.sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + const leftScore = Math.abs(leftRect.height - 64) + Math.abs(leftRect.width - 420) / 10; + const rightScore = Math.abs(rightRect.height - 64) + Math.abs(rightRect.width - 420) / 10; + return leftScore - rightScore; + })[0] || node; +} + +// 等待一次性邮箱列表稳定后再继续操作。 +async function waitForAliasListStable(timeoutMs = 2500) { + const startedAt = Date.now(); + let lastSignature = ''; + let stableRounds = 0; + while (Date.now() - startedAt < timeoutMs) { + const aliases = collectAliasItems().map((node) => extractAliasEmailFromItem(node)).filter(Boolean); + const signature = aliases.join('|'); + if (signature && signature === lastSignature) { + stableRounds += 1; + if (stableRounds >= 3) return aliases; + } else { + stableRounds = 0; + lastSignature = signature; + } + await sleep(180); + } + return collectAliasItems().map((node) => extractAliasEmailFromItem(node)).filter(Boolean); +} + +// 收集当前页面可见的一次性邮箱条目。 +function collectAliasItems() { + const section = findDisposableAliasSection(); + const scope = section || document; + const allEmailTexts = normalizeLowerText(scope.innerText || scope.textContent || '').match(/[a-z0-9._%+-]+@yahoo\.com/g) || []; + log(`Yahoo:旧邮箱扫描区域命中 ${allEmailTexts.length} 个邮箱文本`, 'info'); + + const unique = []; + const seen = new Set(); + + const emailNodes = Array.from(scope.querySelectorAll('*')).filter((node) => { + if (!(node instanceof HTMLElement) || !isVisibleElement(node)) return false; + const text = normalizeLowerText((node.innerText || node.textContent || '').replace(/\s+/g, '')); + if (!/^[a-z0-9._%+-]+@yahoo\.com$/.test(text)) return false; + + let current = node.parentElement; + while (current && current !== scope) { + const currentText = normalizeLowerText(current.innerText || current.textContent || ''); + if (/mailbox list|邮箱列表|send-only email address|仅发送电子邮件地址/.test(currentText) + && !/一次性电子邮件地址|disposable email addresses|disposable email address|disposable address/.test(currentText)) { + return false; + } + current = current.parentElement; + } + return true; + }); + + for (const node of emailNodes) { + const email = extractAliasEmailFromItem(node); + if (!email || seen.has(email)) continue; + const card = findTightAliasCard(node); + seen.add(email); + unique.push(card || node); + } + + if (!unique.length) { + const blockCandidates = Array.from(scope.querySelectorAll('button, [role="button"], li, div, article, section')).filter((node) => { + if (!isVisibleElement(node)) return false; + const text = normalizeLowerText(node.innerText || node.textContent || ''); + if (!/@yahoo\.com/.test(text)) return false; + if (/添加一次性电子邮件地址|add disposable|说明|描述|姓名|关键词|自动转发|移除地址|remove address|编辑|edit|取消|节省|保存|升级|you've reached|达到极限/.test(text)) return false; + const rect = node.getBoundingClientRect(); + return rect.width > 220 && rect.height >= 40; + }); + + for (const node of blockCandidates) { + const email = extractAliasEmailFromItem(node); + if (!email || seen.has(email)) continue; + seen.add(email); + unique.push(node); + } + } + + log(`Yahoo:当前检测到 ${unique.length} 个旧一次性邮箱`, 'info'); + return unique; +} + +// 按邮箱地址查找对应的一次性邮箱条目。 +function findAliasItemByEmail(email = '') { + const normalizedEmail = normalizeLowerText(email).replace(/\s+/g, ''); + if (!normalizedEmail) return null; + const section = findDisposableAliasSection(); + const scope = section || document; + + const exactNode = Array.from(scope.querySelectorAll('*')).find((node) => { + if (!(node instanceof HTMLElement) || !isVisibleElement(node)) return false; + return normalizeLowerText((node.innerText || node.textContent || '').replace(/\s+/g, '')) === normalizedEmail; + }); + if (exactNode) { + return findTightAliasCard(exactNode) || exactNode; + } + + return collectAliasItems().find((node) => extractAliasEmailFromItem(node) === normalizedEmail) || null; +} + +// 从条目文本中抽取一次性邮箱地址。 +function extractAliasEmailFromItem(item) { + const text = normalizeText(item.innerText || item.textContent || ''); + const match = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i); + return match ? match[0].toLowerCase() : ''; +} + +// 派发一组悬停事件,模拟鼠标移入目标元素。 +function dispatchHoverSequence(target, x, y) { + if (!target) return; + target.dispatchEvent(new MouseEvent('pointerover', { bubbles: true, cancelable: true, clientX: x, clientY: y })); + target.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true, clientX: x, clientY: y })); + target.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true, clientX: x, clientY: y })); + target.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: x, clientY: y })); +} + +// 派发一组点击事件,尽量模拟真实鼠标点击。 +function dispatchClickSequence(target, x, y) { + if (!target) return false; + dispatchHoverSequence(target, x, y); + target.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, cancelable: true, clientX: x, clientY: y })); + target.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: x, clientY: y })); + target.dispatchEvent(new MouseEvent('pointerup', { bubbles: true, cancelable: true, clientX: x, clientY: y })); + target.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, clientX: x, clientY: y })); + target.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: x, clientY: y })); + if (typeof target.click === 'function') { + try { target.click(); } catch {} + } + return true; +} + +// 从编辑面板文本里提取邮箱地址。 +function extractEditorPanelEmail(panel) { + if (!panel) return ''; + const text = normalizeText(panel.innerText || panel.textContent || ''); + const match = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i); + return match ? match[0].toLowerCase() : ''; +} + +// 定位指定一次性邮箱的编辑面板。 +function findAliasEditorPanel(email = '') { + const normalizedEmail = normalizeLowerText(email).replace(/\s+/g, ''); + const panels = Array.from(document.querySelectorAll('section, div, aside, article')).filter((node) => { + if (!isVisibleElement(node)) return false; + const text = normalizeLowerText(node.innerText || node.textContent || ''); + const compactText = text.replace(/\s+/g, ''); + if (!/编辑|edit/.test(text)) return false; + if (!/移除地址|remove address|姓名|描述|取消|节省|保存/.test(text)) return false; + if (!normalizedEmail) return true; + return compactText.includes(normalizedEmail); + }); + return panels.sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + return rightRect.left - leftRect.left; + })[0] || null; +} + +// 在悬停后的条目中寻找隐藏的删除按钮。 +function findHoveredAliasDeleteButton(item) { + const itemRect = item.getBoundingClientRect(); + const localCandidates = Array.from(item.querySelectorAll('button, [role="button"], span, div, svg')).map((node) => { + const host = node instanceof SVGElement ? node.parentElement : node; + return host; + }).filter((node) => node && isVisibleElement(node)); + + const localMatch = localCandidates.find((node) => { + const text = normalizeLowerText(node.getAttribute?.('aria-label') || node.textContent || node.title || ''); + const rect = node.getBoundingClientRect(); + return rect.left >= itemRect.left + itemRect.width * 0.78 + && rect.right <= itemRect.right + 20 + && rect.width > 0 + && rect.height > 0 + && rect.width <= 44 + && rect.height <= 44 + && (/删除|移除|remove|delete|×|x/.test(text) || rect.width === rect.height); + }); + if (localMatch) return localMatch; + + const globalCandidates = Array.from(document.querySelectorAll('button, [role="button"], span, div, svg')).map((node) => { + const host = node instanceof SVGElement ? node.parentElement : node; + return host; + }).filter((node) => node && isVisibleElement(node)); + + return globalCandidates.find((node) => { + const text = normalizeLowerText(node.getAttribute?.('aria-label') || node.textContent || node.title || ''); + const rect = node.getBoundingClientRect(); + const verticallyAligned = Math.abs((rect.top + rect.height / 2) - (itemRect.top + itemRect.height / 2)) <= Math.max(18, itemRect.height * 0.45); + return verticallyAligned + && rect.left >= itemRect.right - 60 + && rect.left <= itemRect.right + 40 + && rect.width > 0 + && rect.height > 0 + && rect.width <= 44 + && rect.height <= 44 + && (/删除|移除|remove|delete|×|x/.test(text) || rect.width === rect.height); + }) || null; +} + +// 在编辑面板内寻找“移除地址”按钮。 +function findRemoveAddressButton(panel) { + const scope = panel || document; + const textNodes = Array.from(scope.querySelectorAll('button, [role="button"], a, span, div')).filter((node) => { + const text = normalizeLowerText(node.getAttribute?.('aria-label') || node.textContent || node.title || ''); + return isVisibleElement(node) && /^(移除地址|remove address|删除地址|remove)$/.test(text); + }); + if (textNodes.length) { + return textNodes.sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + return (rightRect.width * rightRect.height) - (leftRect.width * leftRect.height); + })[0] || null; + } + + const fuzzy = Array.from(scope.querySelectorAll('button, [role="button"], a, span, div')).filter((node) => { + const text = normalizeLowerText(node.getAttribute?.('aria-label') || node.textContent || node.title || ''); + return isVisibleElement(node) && /移除地址|remove address|删除地址|remove/.test(text) && !/cancel|取消/.test(text); + }); + return fuzzy.sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + return (rightRect.width * rightRect.height) - (leftRect.width * leftRect.height); + })[0] || null; +} + +// 找到节点中心点附近真正可点击的命中目标。 +function findClickableTargetAtCenter(node) { + if (!node || typeof node.getBoundingClientRect !== 'function') { + return null; + } + const rect = node.getBoundingClientRect(); + const points = [ + { x: Math.round(rect.left + rect.width / 2), y: Math.round(rect.top + rect.height / 2) }, + { x: Math.round(rect.left + rect.width * 0.3), y: Math.round(rect.top + rect.height / 2) }, + { x: Math.round(rect.left + rect.width * 0.7), y: Math.round(rect.top + rect.height / 2) }, + ]; + + for (const point of points) { + const hit = document.elementFromPoint(point.x, point.y); + const clickable = hit?.closest?.('button, [role="button"], a, span, div') || hit; + if (clickable && isVisibleElement(clickable)) { + return { target: clickable, x: point.x, y: point.y }; + } + } + return null; +} + +// 选择某个一次性邮箱条目并打开其编辑面板。 +async function selectAliasItemForEditing(item, email = '') { + const normalizedEmail = normalizeLowerText(email).replace(/\s+/g, ''); + + for (let attempt = 0; attempt < 6; attempt += 1) { + await waitForAliasListStable(1800); + const freshItem = findAliasItemByEmail(email) || item; + freshItem.scrollIntoView?.({ block: 'center', inline: 'nearest' }); + await sleep(320); + + const directEmailNode = Array.from(freshItem.querySelectorAll('*')).find((node) => { + if (!(node instanceof HTMLElement) || !isVisibleElement(node)) return false; + return normalizeLowerText((node.innerText || node.textContent || '').replace(/\s+/g, '')) === normalizedEmail; + }) || (normalizeLowerText((freshItem.innerText || freshItem.textContent || '').replace(/\s+/g, '')) === normalizedEmail ? freshItem : null); + + const targetNode = findTightAliasCard(directEmailNode || freshItem) || directEmailNode || freshItem; + const rect = targetNode.getBoundingClientRect(); + const hoverX = Math.round(rect.left + Math.max(28, Math.min(90, rect.width * 0.18))); + const hoverY = Math.round(rect.top + rect.height / 2); + dispatchHoverSequence(targetNode, hoverX, hoverY); + log(`Yahoo:已悬停旧一次性邮箱 ${email || '(unknown)'} (attempt ${attempt + 1})`, 'info'); + await sleep(550); + + const clickPoints = [ + [Math.round(rect.left + Math.max(36, Math.min(96, rect.width * 0.20))), Math.round(rect.top + rect.height / 2)], + [Math.round(rect.left + rect.width / 2), Math.round(rect.top + rect.height / 2)], + [Math.round(rect.left + Math.max(48, Math.min(140, rect.width * 0.28))), Math.round(rect.top + rect.height / 2)], + ]; + + for (const [clickX, clickY] of clickPoints) { + dispatchClickSequence(targetNode, clickX, clickY); + try { if (typeof targetNode.click === 'function') targetNode.click(); } catch {} + log(`Yahoo:已点击旧一次性邮箱卡片主体 ${email || '(unknown)'} point=${clickX},${clickY} (attempt ${attempt + 1})`, 'info'); + + let lastPanelEmail = ''; + for (let i = 0; i < 10; i += 1) { + await sleep(220); + const panel = findAliasEditorPanel(email) || findAliasEditorPanel(''); + if (!panel) continue; + const panelEmail = extractEditorPanelEmail(panel); + if (panelEmail && panelEmail !== lastPanelEmail) { + lastPanelEmail = panelEmail; + log(`Yahoo:已打开编辑面板,面板邮箱=${panelEmail}`, 'info'); + } + if (!normalizedEmail || panelEmail.replace(/\s+/g, '') === normalizedEmail) { + return panel; + } + } + } + } + + const snippets = Array.from(document.querySelectorAll('section, div, aside, article')) + .filter((node) => isVisibleElement(node)) + .map((node) => normalizeText(node.innerText || node.textContent || '')) + .filter((text) => /编辑|edit|移除地址|remove address/.test(text)) + .slice(0, 3) + .join(' || '); + log(`Yahoo:编辑面板候选片段=${snippets || '(none)'}`, 'warn'); + return null; +} + +// 按文本模式筛选可能的对话框候选项。 +function getDialogCandidates(pattern) { + return Array.from(document.querySelectorAll('[role="dialog"], dialog, [aria-modal="true"], div')).filter((node) => { + if (!isVisibleElement(node)) return false; + const text = normalizeLowerText(node.innerText || node.textContent || ''); + if (!pattern.test(text)) return false; + const rect = node.getBoundingClientRect(); + const style = getComputedStyle(node); + return rect.width >= 260 + && rect.height >= 120 + && rect.left >= 0 + && rect.top >= 0 + && rect.left + rect.width <= window.innerWidth + 20 + && rect.top + rect.height <= window.innerHeight + 20 + && (style.position === 'fixed' || style.position === 'absolute'); + }).sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + return (leftRect.width * leftRect.height) - (rightRect.width * rightRect.height); + }); +} + +// 直接按文本模式取第一个匹配到的对话框。 +function findDialogByText(pattern) { + return getDialogCandidates(pattern)[0] || null; +} + +// 查找“达到极限”之类的提示弹窗。 +function findVisibleDialog() { + return findDialogByText(/你已经达到极限了|达到极限|limit|upgrade|升级/); +} + +function getYahooClickableText(node) { + if (!node) return ''; + return normalizeLowerText([ + node.getAttribute?.('aria-label') || '', + node.getAttribute?.('title') || '', + node.getAttribute?.('data-test-id') || '', + node.getAttribute?.('data-test') || '', + node.getAttribute?.('id') || '', + node.textContent || '', + ].join(' ')); +} + +function isLikelyYahooDisposableAddText(text, options = {}) { + const normalized = normalizeLowerText(text); + const allowShort = Boolean(options.allowShort); + if (!normalized) return false; + if (allowShort && /^(添加|新增|新建|创建|add|new|create|\+)$/.test(normalized)) { + return true; + } + return /(添加|新增|新建|创建).{0,16}(一次性|临时|分身|电子邮件地址|电子邮箱|邮箱|地址)/.test(normalized) + || /(一次性|临时|分身|电子邮件地址|电子邮箱|邮箱|地址).{0,16}(添加|新增|新建|创建)/.test(normalized) + || /(add|new|create).{0,24}(disposable|alias|address|email)/.test(normalized) + || /(disposable|alias).{0,24}(add|new|create)/.test(normalized); +} + +function isUsableYahooButtonCandidate(node) { + if (!(node instanceof HTMLElement) || !isVisibleElement(node)) return false; + if (node.disabled || node.getAttribute?.('aria-disabled') === 'true') return false; + const rect = node.getBoundingClientRect(); + return rect.width >= 24 && rect.height >= 20 && rect.width <= 380 && rect.height <= 100; +} + +function collectYahooVisibleButtonLabels(root = document, limit = 12) { + return Array.from(root.querySelectorAll('button, [role="button"], a, span, div')) + .filter((node) => isUsableYahooButtonCandidate(node)) + .map((node) => getYahooClickableText(node)) + .filter(Boolean) + .filter((text, index, arr) => arr.indexOf(text) === index) + .slice(0, limit); +} + +function getYahooDialogText(dialog) { + return normalizeText(dialog?.innerText || dialog?.textContent || '').slice(0, 220); +} + +function buildYahooAliasCreateDiagnostics() { + let aliases = []; + try { + aliases = collectAliasItems().map(extractAliasEmailFromItem).filter(Boolean); + } catch {} + + const section = findDisposableAliasSection(); + const limitDialog = findVisibleDialog(); + const buttonLabels = collectYahooVisibleButtonLabels(section || document, 10); + const pageText = normalizeText(document.body?.innerText || document.body?.textContent || '').slice(0, 280); + const limitHint = limitDialog + ? `检测到 Yahoo 限制弹窗:“${getYahooDialogText(limitDialog)}”。` + : ''; + const aliasHint = aliases.length >= 3 + ? `当前页面已识别 ${aliases.length} 个旧别名;已按当前配置不自动清理旧别名。` + : `当前页面已识别 ${aliases.length} 个旧别名。`; + return [ + `url=${location.href}`, + `section=${section ? 'yes' : 'no'}`, + aliasHint, + limitHint, + `可见按钮=${buttonLabels.join(' / ') || '(none)'}`, + `页面片段=${pageText || '(empty)'}`, + ].filter(Boolean).join(';'); +} + +// 查找一次性邮箱删除确认弹窗。 +function findDeleteConfirmDialog() { + const candidates = getDialogCandidates(/删除.*@yahoo\.com|这将从您的一次性电子邮件地址列表中删除|remove.*@yahoo\.com|delete.*@yahoo\.com/); + return candidates.find((node) => { + const text = normalizeLowerText(node.innerText || node.textContent || ''); + return /取消|cancel/.test(text) && /删除|remove|delete/.test(text); + }) || candidates[0] || null; +} + +// 如果出现“达到极限”提示弹窗就尝试关闭。 +async function closeLimitDialogIfPresent() { + const dialog = findVisibleDialog(); + if (!dialog) { + return false; + } + + log('Yahoo:检测到“达到极限”弹窗,正在尝试关闭...', 'warn'); + + const closeButton = Array.from(dialog.querySelectorAll('button, [role="button"], span, div')).find((node) => { + const text = normalizeLowerText(node.getAttribute?.('aria-label') || node.textContent || node.title || ''); + return isVisibleElement(node) && (/close|关闭|×|x/.test(text)); + }); + + if (closeButton) { + simulateClick(closeButton); + await sleep(800); + return true; + } + + const clickableNodes = Array.from(dialog.querySelectorAll('button, [role="button"], span, div, svg')).filter((node) => { + const host = node instanceof SVGElement ? node.parentElement : node; + if (!host || !isVisibleElement(host)) return false; + const rect = host.getBoundingClientRect(); + const dialogRect = dialog.getBoundingClientRect(); + return rect.width > 0 + && rect.height > 0 + && rect.width <= 40 + && rect.height <= 40 + && rect.top <= dialogRect.top + 60 + && rect.left >= dialogRect.left + dialogRect.width * 0.7; + }); + + const fallbackClose = clickableNodes[0] instanceof SVGElement ? clickableNodes[0].parentElement : clickableNodes[0]; + if (fallbackClose) { + simulateClick(fallbackClose); + await sleep(800); + return true; + } + + return false; +} + +// 在删除确认弹窗中定位确认删除按钮。 +function findDeleteConfirmButton(dialog) { + if (!dialog) return null; + + const exactCandidates = Array.from(dialog.querySelectorAll('button, [role="button"], span, div')).filter((node) => { + const text = normalizeLowerText(node.getAttribute?.('aria-label') || node.textContent || node.title || ''); + return isVisibleElement(node) && /^(删除|delete|remove)$/.test(text); + }); + if (exactCandidates.length) { + return exactCandidates.sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + return (rightRect.width * rightRect.height) - (leftRect.width * leftRect.height); + })[0] || null; + } + + const buttonCandidates = Array.from(dialog.querySelectorAll('button, [role="button"]')).filter((node) => { + if (!isVisibleElement(node)) return false; + const text = normalizeLowerText(node.getAttribute?.('aria-label') || node.textContent || node.title || ''); + if (/取消|cancel|关闭|close/.test(text)) return false; + const rect = node.getBoundingClientRect(); + const dialogRect = dialog.getBoundingClientRect(); + return rect.width >= 60 + && rect.height >= 32 + && rect.top >= dialogRect.top + dialogRect.height * 0.45 + && rect.left >= dialogRect.left + dialogRect.width * 0.45; + }); + if (buttonCandidates.length) { + return buttonCandidates.sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + if (Math.abs(rightRect.top - leftRect.top) > 8) return rightRect.top - leftRect.top; + return rightRect.left - leftRect.left; + })[0] || null; + } + + const genericCandidates = Array.from(dialog.querySelectorAll('span, div')).filter((node) => { + if (!isVisibleElement(node)) return false; + const text = normalizeLowerText(node.getAttribute?.('aria-label') || node.textContent || node.title || ''); + if (/取消|cancel|关闭|close/.test(text)) return false; + const rect = node.getBoundingClientRect(); + const dialogRect = dialog.getBoundingClientRect(); + return rect.width >= 60 + && rect.height >= 24 + && rect.top >= dialogRect.top + dialogRect.height * 0.45 + && rect.left >= dialogRect.left + dialogRect.width * 0.45 + && /删除|delete|remove/.test(text); + }); + return genericCandidates.sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + if (Math.abs(rightRect.top - leftRect.top) > 8) return rightRect.top - leftRect.top; + return rightRect.left - leftRect.left; + })[0] || null; +} + +// 如果删除确认弹窗出现,就点击确认按钮完成删除。 +async function confirmDeleteDialogIfPresent(email = '') { + const normalizedEmail = normalizeLowerText(email); + let dialog = null; + for (let i = 0; i < 20 && !dialog; i += 1) { + const candidate = findDeleteConfirmDialog(); + const dialogEmail = extractEditorPanelEmail(candidate || null); + if (candidate && (!normalizedEmail || !dialogEmail || dialogEmail === normalizedEmail)) { + dialog = candidate; + break; + } + await sleep(200); + } + + if (!dialog) { + log(`Yahoo:未检测到 ${email || '(unknown)'} 的删除确认弹窗`, 'warn'); + return false; + } + + const dialogEmail = extractEditorPanelEmail(dialog || null); + log(`Yahoo:已检测到 ${email || '(unknown)'} 的删除确认弹窗 dialogEmail=${dialogEmail || '(unknown)'}`, 'info'); + + const confirmButton = findDeleteConfirmButton(dialog); + if (!confirmButton) { + log(`Yahoo:未找到 ${email || '(unknown)'} 删除确认按钮`, 'warn'); + return false; + } + + const rect = confirmButton.getBoundingClientRect(); + const x = Math.round(rect.left + rect.width / 2); + const y = Math.round(rect.top + rect.height / 2); + dispatchHoverSequence(confirmButton, x, y); + await sleep(120); + dispatchClickSequence(confirmButton, x, y); + try { + if (typeof confirmButton.click === 'function') confirmButton.click(); + } catch {} + log(`Yahoo:已确认删除 ${email || '(unknown)'} target=${confirmButton.tagName || 'unknown'} rect=${Math.round(rect.width)}x${Math.round(rect.height)}`, 'info'); + await sleep(2200); + return true; +} + +// 删除单个一次性邮箱条目,优先走行内删除,失败再走编辑面板。 +async function clickDeleteForAliasItem(item) { + const email = extractAliasEmailFromItem(item); + log(`Yahoo:准备删除旧一次性邮箱 ${email || '(unknown)'}`, 'info'); + + const freshItem = findAliasItemByEmail(email) || item; + freshItem.scrollIntoView?.({ block: 'center', inline: 'nearest' }); + await sleep(260); + + const hoverTarget = findTightAliasCard(freshItem) || freshItem; + const hoverRect = hoverTarget.getBoundingClientRect(); + const hoverX = Math.round(hoverRect.left + Math.max(32, Math.min(88, hoverRect.width * 0.18))); + const hoverY = Math.round(hoverRect.top + hoverRect.height / 2); + dispatchHoverSequence(hoverTarget, hoverX, hoverY); + await sleep(520); + + let inlineDelete = findHoveredAliasDeleteButton(hoverTarget) || findHoveredAliasDeleteButton(freshItem); + if (inlineDelete) { + const deleteRect = inlineDelete.getBoundingClientRect(); + const deleteX = Math.round(deleteRect.left + deleteRect.width / 2); + const deleteY = Math.round(deleteRect.top + deleteRect.height / 2); + const deleteText = normalizeLowerText(inlineDelete.getAttribute?.('aria-label') || inlineDelete.textContent || inlineDelete.title || '') || '(empty)'; + log(`Yahoo:已命中行内删除按钮 text=${deleteText} target=${inlineDelete.tagName || 'unknown'} rect=${Math.round(deleteRect.width)}x${Math.round(deleteRect.height)}`, 'info'); + dispatchHoverSequence(inlineDelete, deleteX, deleteY); + await sleep(120); + dispatchClickSequence(inlineDelete, deleteX, deleteY); + try { if (typeof inlineDelete.click === 'function') inlineDelete.click(); } catch {} + await sleep(900); + + const confirmedInline = await confirmDeleteDialogIfPresent(email); + if (confirmedInline) { + await closeLimitDialogIfPresent(); + return true; + } + log(`Yahoo:行内删除按钮点击后未出现 ${email || '(unknown)'} 的确认弹窗,回退编辑面板方案`, 'warn'); + } + + const editorPanel = await selectAliasItemForEditing(freshItem, email); + if (!editorPanel) { + log(`Yahoo:未能打开 ${email || '(unknown)'} 的编辑面板`, 'warn'); + return false; + } + log(`Yahoo:已打开 ${email || '(unknown)'} 的编辑面板`, 'info'); + + const removeButton = findRemoveAddressButton(editorPanel); + if (!removeButton) { + log(`Yahoo:未找到 ${email || '(unknown)'} 的“移除地址”按钮`, 'warn'); + return false; + } + + removeButton.scrollIntoView?.({ block: 'center', inline: 'nearest' }); + await sleep(300); + + const removeTarget = removeButton.closest?.('button, [role="button"], a, div') || removeButton; + const hit = findClickableTargetAtCenter(removeTarget) || findClickableTargetAtCenter(removeButton); + const rect = (hit?.target || removeTarget).getBoundingClientRect(); + const x = hit?.x ?? Math.round(rect.left + rect.width / 2); + const y = hit?.y ?? Math.round(rect.top + rect.height / 2); + const clickTarget = hit?.target || removeTarget; + const labelText = normalizeLowerText(removeButton.getAttribute?.('aria-label') || removeButton.textContent || removeButton.title || '') || '(empty)'; + log(`Yahoo:已命中“移除地址”按钮 text=${labelText} target=${clickTarget.tagName || 'unknown'} rect=${Math.round(rect.width)}x${Math.round(rect.height)}`, 'info'); + + dispatchHoverSequence(clickTarget, x, y); + await sleep(180); + dispatchClickSequence(clickTarget, x, y); + try { + if (typeof clickTarget.click === 'function') clickTarget.click(); + else if (typeof removeTarget.click === 'function') removeTarget.click(); + } catch {} + + await sleep(1000); + + const confirmed = await confirmDeleteDialogIfPresent(email); + if (!confirmed) { + log(`Yahoo:点击“移除地址”后未出现 ${email || '(unknown)'} 的确认弹窗`, 'warn'); + return false; + } + await closeLimitDialogIfPresent(); + return true; +} + +// 逐个删除旧的一次性邮箱,直到不再需要清理为止。 +async function deleteAllOldAliases() { + const deleted = []; + for (let round = 0; round < 10; round += 1) { + throwIfStopped(); + await waitForAliasListStable(2200); + const aliases = collectAliasItems(); + log(`Yahoo:当前检测到 ${aliases.length} 个旧一次性邮箱`, 'info'); + if (!aliases.length) { + break; + } + + if (aliases.length < 3) { + log(`Yahoo:当前仅剩 ${aliases.length} 个旧一次性邮箱,未达到上限,跳过后续删除并直接进入创建`, 'warn'); + break; + } + + const item = aliases[0]; + const email = extractAliasEmailFromItem(item); + const freshItem = findAliasItemByEmail(email) || item; + const beforeCount = aliases.length; + const ok = await clickDeleteForAliasItem(freshItem); + if (!ok) { + log(`Yahoo:未能命中 ${email || '(unknown)'} 的删除按钮`, 'warn'); + break; + } + + let removed = false; + for (let waitRound = 0; waitRound < 15; waitRound += 1) { + await sleep(500); + const currentAliases = collectAliasItems().map(extractAliasEmailFromItem).filter(Boolean); + if (!currentAliases.includes(email) || currentAliases.length < beforeCount) { + removed = true; + log(`Yahoo:旧一次性邮箱已删除 ${email || '(unknown)'}`, 'ok'); + break; + } + } + + if (removed && email) { + deleted.push(email); + } else { + log(`Yahoo:等待删除 ${email || '(unknown)'} 生效超时`, 'warn'); + break; + } + } + return deleted; +} + +// 生成一个默认的一次性邮箱前缀。 +function generateAliasPrefix(length = 10) { + const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + let output = ''; + for (let i = 0; i < length; i += 1) { + output += chars[Math.floor(Math.random() * chars.length)]; + } + return output; +} + +// 在指定范围内按文本匹配查找可见按钮。 +function findYahooButtonByText(patterns = [], root = document) { + return Array.from(root.querySelectorAll('button, [role="button"], a, span')).find((node) => { + const text = normalizeLowerText(node.getAttribute?.('aria-label') || node.textContent || node.title || ''); + return patterns.some((pattern) => pattern.test(text)) && isVisibleElement(node); + }) || null; +} + +// 查找 Yahoo 创建一次性邮箱的右侧面板。 +function findYahooCreatePanel() { + const panels = Array.from(document.querySelectorAll('section, div, aside, article, [role="dialog"], dialog')).filter((node) => { + if (!isVisibleElement(node)) return false; + const text = normalizeLowerText(node.innerText || node.textContent || ''); + const rect = node.getBoundingClientRect(); + if (rect.width < 260 || rect.height < 180) return false; + if (rect.left < window.innerWidth * 0.45) return false; + if (!/添加一次性电子邮件地址|add disposable address|add disposable email address|创建一次性电子邮件地址|create disposable|name your address|choose.*address|keyword|关键词/.test(text)) { + return false; + } + if (!/关键词|keyword|姓名|描述|save|保存|节省|cancel|取消|description|name|address/.test(text)) { + return false; + } + return true; + }); + return panels.sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + if (Math.abs(rightRect.left - leftRect.left) > 20) return rightRect.left - leftRect.left; + return (rightRect.width * rightRect.height) - (leftRect.width * leftRect.height); + })[0] || null; +} + +// 查找创建面板里的一次性邮箱输入框。 +function findYahooAliasInput() { + const panel = findYahooCreatePanel(); + const scopes = [panel, document].filter(Boolean); + + for (const scope of scopes) { + const inputs = Array.from(scope.querySelectorAll('input, textarea, [contenteditable="true"], [role="textbox"]')); + + const semanticMatch = inputs.find((node) => { + const host = node instanceof HTMLElement ? node : null; + if (!host || !isVisibleElement(host)) return false; + if (host instanceof HTMLInputElement || host instanceof HTMLTextAreaElement) { + if (host.disabled || host.readOnly) return false; + } + const text = normalizeLowerText([ + host.getAttribute?.('placeholder') || '', + host.getAttribute?.('aria-label') || '', + host.getAttribute?.('name') || '', + host.getAttribute?.('id') || '', + host.getAttribute?.('data-test-id') || '', + host.getAttribute?.('data-test') || '', + host.labels?.[0]?.textContent || '', + host.parentElement?.innerText || '', + host.closest('div,section,form,[role="dialog"]')?.innerText || '', + ].join(' ')); + return /关键词|keyword|alias|address|name your address|create your address|choose.*address/.test(text); + }); + if (semanticMatch) return semanticMatch; + + const sizedMatch = inputs.find((node) => { + const host = node instanceof HTMLElement ? node : null; + if (!host || !isVisibleElement(host)) return false; + if (host instanceof HTMLInputElement || host instanceof HTMLTextAreaElement) { + if (host.disabled || host.readOnly) return false; + } + const rect = host.getBoundingClientRect(); + const type = normalizeLowerText(host.getAttribute?.('type') || ''); + if (/hidden|checkbox|radio|submit|button/.test(type)) return false; + return rect.width >= 120 && rect.height >= 24; + }); + if (sizedMatch) return sizedMatch; + } + + return null; +} + +// 查找一次性邮箱设置区域的标题节点。 +function findYahooDisposableSectionHeading() { + const candidates = Array.from(document.querySelectorAll('h1, h2, h3, h4, div, span')).filter((node) => { + if (!(node instanceof HTMLElement)) return false; + const text = normalizeLowerText(node.innerText || node.textContent || ''); + return /一次性电子邮件地址|disposable email addresses|disposable email address|disposable address/.test(text) + && !/@yahoo\.com/.test(text); + }); + return candidates.sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + return leftRect.top - rightRect.top; + })[0] || null; +} + +// 查找新增一次性邮箱的“添加”按钮。 +function findYahooDisposableAddButton() { + const buttonSelectors = 'button, [role="button"], a, span, div'; + const section = findDisposableAliasSection(); + + if (section) { + const sectionRect = section.getBoundingClientRect(); + const scoped = Array.from(section.querySelectorAll(buttonSelectors)).filter((node) => { + if (!isUsableYahooButtonCandidate(node)) return false; + const text = getYahooClickableText(node); + if (!isLikelyYahooDisposableAddText(text, { allowShort: true })) return false; + const rect = node.getBoundingClientRect(); + return rect.width >= 40 + && rect.height >= 24 + && rect.top >= sectionRect.top - 6 + && rect.top <= sectionRect.top + 90 + && rect.left >= sectionRect.left + sectionRect.width * 0.72; + }); + if (scoped.length) { + return scoped.sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + return Math.abs(leftRect.top - sectionRect.top) - Math.abs(rightRect.top - sectionRect.top) + || (rightRect.left - leftRect.left); + })[0]; + } + } + + const heading = findYahooDisposableSectionHeading(); + if (heading) { + const headingRect = heading.getBoundingClientRect(); + const nearHeading = Array.from(document.querySelectorAll(buttonSelectors)).filter((node) => { + if (!isUsableYahooButtonCandidate(node)) return false; + const text = getYahooClickableText(node); + if (!isLikelyYahooDisposableAddText(text, { allowShort: true })) return false; + const rect = node.getBoundingClientRect(); + return rect.width >= 40 + && rect.height >= 24 + && Math.abs((rect.top + rect.height / 2) - (headingRect.top + headingRect.height / 2)) <= 28 + && rect.left >= headingRect.left + Math.max(240, headingRect.width * 0.8); + }); + if (nearHeading.length) { + return nearHeading.sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + return Math.abs(leftRect.top - headingRect.top) - Math.abs(rightRect.top - headingRect.top) + || (rightRect.left - leftRect.left); + })[0]; + } + } + + const globalCandidates = Array.from(document.querySelectorAll(buttonSelectors)).filter((node) => { + if (!isUsableYahooButtonCandidate(node)) return false; + const text = getYahooClickableText(node); + return isLikelyYahooDisposableAddText(text); + }); + return globalCandidates.sort((left, right) => { + const leftRect = left.getBoundingClientRect(); + const rightRect = right.getBoundingClientRect(); + return rightRect.top - leftRect.top || rightRect.left - leftRect.left; + })[0] || findYahooButtonByText([ + /new\s+disposable\s+address/i, + /新建一次性电子邮件地址/i, + /新建一次性电子邮箱/i, + /新增一次性电子邮件地址/i, + /添加一次性电子邮件地址/i, + /一次性电子邮箱/i, + /一次性电子邮件地址/i, + /create/i, + /new/i, + /添加/i, + /创建/i, + ]); +} + +function getDisposableAliasUsageCount() { + const section = findDisposableAliasSection(); + const text = normalizeLowerText([ + section?.innerText || section?.textContent || '', + document.body?.innerText || document.body?.textContent || '', + ].join(' ')); + const match = text.match(/disposable email addresses[^0-9]{0,40}(\d+)\s+of\s+(\d+)/i) + || text.match(/一次性电子邮件地址[^0-9]{0,40}(\d+)\s*(?:of|\/)\s*(\d+)/i); + if (!match) return null; + return { + used: Number(match[1]), + limit: Number(match[2]), + }; +} + +function inferYahooAliasFromExistingAliases(prefix = '', aliases = []) { + const normalizedPrefix = normalizeLowerText(prefix).replace(/[^a-z0-9._%+-]/g, ''); + if (!normalizedPrefix) return ''; + const sample = (aliases || []).find((email) => /@yahoo\.com$/i.test(email) && email.includes('-')); + if (!sample) return ''; + const local = sample.split('@')[0] || ''; + const base = local.slice(0, local.lastIndexOf('-')); + return base ? `${base}-${normalizedPrefix}@yahoo.com` : ''; +} + +// 创建 Yahoo 临时邮箱,并返回新生成的地址。 +async function handleCreateYahooTempAlias(payload) { + log('Yahoo:已进入创建临时邮箱流程', 'info'); + await ensureOnYahooSettings(); + await waitForAnySelector(['main', 'form', 'button'], 15000); + ensureYahooLoggedIn('创建 Yahoo 临时邮箱'); + + await closeLimitDialogIfPresent(); + log('Yahoo:跳过旧一次性邮箱清理,直接创建新邮箱', 'info'); + await focusDisposableAliasSection(); + + const remainingAliases = collectAliasItems().map(extractAliasEmailFromItem).filter(Boolean); + const beforeUsage = getDisposableAliasUsageCount(); + if (remainingAliases.length > 0) { + log(`Yahoo:当前已有 ${remainingAliases.length} 个一次性邮箱,将保留现有邮箱并继续直接创建新邮箱`, 'info'); + } + + const prefix = normalizeLowerText(payload.prefix || '') || generateAliasPrefix(10); + log(`Yahoo:准备创建新一次性邮箱,关键词=${prefix}`, 'info'); + + const addButton = findYahooDisposableAddButton(); + if (!addButton) { + throw new Error(`创建 Yahoo 临时邮箱失败:未找到“新建一次性电子邮箱/地址”按钮,请确认 Yahoo 设置页已加载到一次性邮箱区域;${buildYahooAliasCreateDiagnostics()}`); + } + + const addRect = addButton.getBoundingClientRect(); + const addX = Math.round(addRect.left + addRect.width / 2); + const addY = Math.round(addRect.top + addRect.height / 2); + log(`Yahoo:已定位“添加”按钮 tag=${addButton.tagName || 'unknown'} rect=${Math.round(addRect.width)}x${Math.round(addRect.height)} pos=${addX},${addY}`, 'info'); + + let createPanel = null; + const clickTargets = [ + addButton.closest?.('button, [role="button"], a, div') || addButton, + document.elementFromPoint(addX, addY)?.closest?.('button, [role="button"], a, div') || addButton, + ].filter(Boolean); + + for (const target of clickTargets) { + dispatchHoverSequence(target, addX, addY); + await sleep(120); + dispatchClickSequence(target, addX, addY); + try { if (typeof target.click === 'function') target.click(); } catch {} + log(`Yahoo:已点击“添加”按钮 target=${target.tagName || 'unknown'}`, 'info'); + + for (let i = 0; i < 8 && !createPanel; i += 1) { + await sleep(250); + createPanel = findYahooCreatePanel(); + } + if (createPanel) break; + } + + if (!createPanel) { + const section = findDisposableAliasSection(); + if (section) { + const sectionRect = section.getBoundingClientRect(); + const fallbackX = Math.round(sectionRect.right - 32); + const fallbackY = Math.round(sectionRect.top + 34); + const fallbackTarget = document.elementFromPoint(fallbackX, fallbackY)?.closest?.('button, [role="button"], a, div'); + if (fallbackTarget) { + dispatchHoverSequence(fallbackTarget, fallbackX, fallbackY); + await sleep(120); + dispatchClickSequence(fallbackTarget, fallbackX, fallbackY); + try { if (typeof fallbackTarget.click === 'function') fallbackTarget.click(); } catch {} + log(`Yahoo:已执行“添加”按钮兜底点击 target=${fallbackTarget.tagName || 'unknown'} pos=${fallbackX},${fallbackY}`, 'warn'); + for (let i = 0; i < 8 && !createPanel; i += 1) { + await sleep(250); + createPanel = findYahooCreatePanel(); + } + } + } + } + if (!createPanel) { + throw new Error('创建 Yahoo 临时邮箱失败:点击“添加”后未出现创建面板。'); + } + log('Yahoo:已识别到右侧创建面板', 'info'); + + let input = findYahooAliasInput(); + if (!input) { + for (let i = 0; i < 20 && !input; i += 1) { + await sleep(300); + input = findYahooAliasInput(); + } + } + if (!input) { + const panelText = normalizeText((findYahooCreatePanel()?.innerText || findYahooCreatePanel()?.textContent || '')).slice(0, 500); + throw new Error(`创建 Yahoo 临时邮箱失败:未找到一次性邮箱输入框。创建面板片段:${panelText || '(empty)'}`); + } + fillInput(input, prefix); + try { input.focus?.(); } catch {} + if (input instanceof HTMLElement && input.getAttribute('contenteditable') === 'true') { + input.textContent = prefix; + input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: prefix })); + } + log(`Yahoo:已填写关键词输入框 tag=${input.tagName || 'unknown'}`, 'info'); + await sleep(500); + + const saveButton = findYahooButtonByText([ + /save/i, + /done/i, + /create/i, + /add/i, + /完成/i, + /保存/i, + /节省/i, + /创建/i, + /添加/i, + ], findYahooCreatePanel() || document); + if (!saveButton) { + throw new Error('创建 Yahoo 临时邮箱失败:未找到保存/创建按钮。'); + } + + simulateClick(saveButton); + log('Yahoo:已点击”节省/保存”按钮', 'info'); + await sleep(1500); + + // 等待创建面板消失 + for (let i = 0; i < 20; i += 1) { + const panel = findYahooCreatePanel(); + if (!panel) { + log('Yahoo:创建面板已关闭', 'info'); + break; + } + await sleep(300); + } + + await sleep(1000); + const limitDialogAfterSave = findVisibleDialog(); + if (limitDialogAfterSave) { + const limitText = getYahooDialogText(limitDialogAfterSave); + await closeLimitDialogIfPresent(); + throw new Error(`创建 Yahoo 临时邮箱失败:Yahoo 页面提示已达到一次性邮箱/别名上限;当前已按要求不自动清理旧别名,所以无法继续创建新别名。弹窗内容=${limitText || '(empty)'}`); + } + + let afterUsage = null; + for (let attempt = 1; attempt <= 10; attempt += 1) { + afterUsage = getDisposableAliasUsageCount(); + if (beforeUsage?.used >= 0 && afterUsage?.used > beforeUsage.used) { + break; + } + if (attempt < 10) { + log(`Yahoo:等待一次性邮箱数量增加(${attempt}/10)...`, 'info'); + await sleep(600); + } + } + + if (!(beforeUsage?.used >= 0 && afterUsage?.used > beforeUsage.used)) { + const beforeText = beforeUsage ? `${beforeUsage.used}/${beforeUsage.limit}` : 'unknown'; + const afterText = afterUsage ? `${afterUsage.used}/${afterUsage.limit}` : 'unknown'; + throw new Error(`创建 Yahoo 临时邮箱失败:保存后一次性邮箱数量未增加(before=${beforeText}, after=${afterText})。`); + } + + const createdAlias = inferYahooAliasFromExistingAliases(prefix, remainingAliases); + if (!createdAlias) { + throw new Error(`创建 Yahoo 临时邮箱失败:一次性邮箱数量已从 ${beforeUsage.used} 增至 ${afterUsage.used},但无法从现有别名推断完整邮箱地址。`); + } + + log(`Yahoo:一次性邮箱数量已从 ${beforeUsage.used} 增至 ${afterUsage.used},按现有别名格式推断新别名 ${createdAlias}`, 'info'); + return { + ok: true, + email: createdAlias, + deletedAliases: [], + }; +} diff --git a/flows/openai/mail-rules.js b/flows/openai/mail-rules.js index d51a2362..731937cd 100644 --- a/flows/openai/mail-rules.js +++ b/flows/openai/mail-rules.js @@ -53,6 +53,10 @@ return String(state?.mailProvider || '').trim().toLowerCase() === '2925'; } + function isYahooProvider(state = {}) { + return String(state?.mailProvider || '').trim().toLowerCase() === 'yahoo'; + } + function shouldMatchMail2925TargetEmail(state = {}) { return isMail2925Provider(state) && String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive'; @@ -78,10 +82,13 @@ const nodeId = resolveVerificationNodeId(input); const normalizedStep = getVisibleStepForNode(nodeId, state); const mail2925Provider = isMail2925Provider(state); + const yahooProvider = isYahooProvider(state); const signupStep = nodeId === SIGNUP_CODE_NODE_ID; const targetEmail = signupStep ? state?.email : (String(state?.step8VerificationTargetEmail || '').trim() || state?.email); + const defaultMaxAttempts = yahooProvider ? 60 : 5; + const defaultIntervalMs = yahooProvider ? 5000 : 3000; return { flowId: 'openai', @@ -105,8 +112,9 @@ targetEmail, targetEmailHints: buildTargetEmailHints(targetEmail), mail2925MatchTargetEmail: shouldMatchMail2925TargetEmail(state), - maxAttempts: mail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5, - intervalMs: mail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000, + maxAttempts: mail2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : defaultMaxAttempts, + intervalMs: mail2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : defaultIntervalMs, + ...(yahooProvider ? { keepRefreshingUntilCode: true } : {}), }; } diff --git a/mail-provider-utils.js b/mail-provider-utils.js index c17a37a8..896c030b 100644 --- a/mail-provider-utils.js +++ b/mail-provider-utils.js @@ -11,6 +11,7 @@ })(typeof self !== 'undefined' ? self : globalThis, function createMailProviderUtils() { const HOTMAIL_PROVIDER = 'hotmail-api'; const GMAIL_PROVIDER = 'gmail'; + const YAHOO_PROVIDER = 'yahoo'; const YYDS_MAIL_PROVIDER = 'yyds-mail'; const NETEASE_LIST_PATH = '/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'; const ICLOUD_TARGET_MAILBOX_TYPE_INBOX = 'icloud-inbox'; @@ -27,6 +28,7 @@ const normalized = String(value || '').trim().toLowerCase(); switch (normalized) { case HOTMAIL_PROVIDER: + case YAHOO_PROVIDER: case YYDS_MAIL_PROVIDER: case '163': case '163-vip': @@ -78,6 +80,17 @@ if (provider === HOTMAIL_PROVIDER) { return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(微软 Graph)' }; } + if (provider === YAHOO_PROVIDER) { + return { + provider: YAHOO_PROVIDER, + source: 'yahoo-mail', + url: 'https://mail.yahoo.com/n/inbox/all?listFilter=ALL_INBOX', + label: 'Yahoo 邮箱', + navigateOnReuse: true, + inject: ['content/activation-utils.js', 'shared/source-registry.js', 'content/utils.js', 'content/yahoo-mail.js'], + injectSource: 'yahoo-mail', + }; + } if (provider === YYDS_MAIL_PROVIDER) { return { provider: YYDS_MAIL_PROVIDER, label: 'YYDS Mail' }; } @@ -126,6 +139,7 @@ return { GMAIL_PROVIDER, HOTMAIL_PROVIDER, + YAHOO_PROVIDER, YYDS_MAIL_PROVIDER, getIcloudForwardMailConfig, getIcloudForwardMailProviderOptions, diff --git a/manifest.json b/manifest.json index c10b7bcd..b78bcecc 100644 --- a/manifest.json +++ b/manifest.json @@ -104,6 +104,19 @@ "all_frames": true, "run_at": "document_idle" }, + { + "matches": [ + "https://mail.yahoo.com/*" + ], + "js": [ + "content/activation-utils.js", + "shared/source-registry.js", + "content/utils.js", + "content/yahoo-mail.js" + ], + "all_frames": false, + "run_at": "document_idle" + }, { "matches": [ "https://duckduckgo.com/email/settings/autofill*" diff --git a/shared/source-registry.js b/shared/source-registry.js index 587420f0..179e38c7 100644 --- a/shared/source-registry.js +++ b/shared/source-registry.js @@ -36,6 +36,15 @@ driverId: 'content/gmail-mail', cleanupScopes: [], }, + 'yahoo-mail': { + flowId: null, + kind: 'mail-provider', + label: 'Yahoo 邮箱', + readyPolicy: 'top-frame-only', + family: 'yahoo-mail-family', + driverId: 'content/yahoo-mail', + cleanupScopes: [], + }, 'icloud-mail': { flowId: null, kind: 'mail-provider', @@ -96,6 +105,10 @@ sourceId: 'gmail-mail', commands: ['POLL_EMAIL'], }, + 'content/yahoo-mail': { + sourceId: 'yahoo-mail', + commands: ['POLL_EMAIL', 'YAHOO_CHECK_TOP_MESSAGE', 'YAHOO_OPEN_TOP_MESSAGE', 'YAHOO_READ_CURRENT_MESSAGE_CODE', 'YAHOO_CREATE_TEMP_ALIAS', 'YAHOO_LOGIN_WITH_CREDENTIALS'], + }, 'content/icloud-mail': { sourceId: 'icloud-mail', commands: ['POLL_EMAIL'], @@ -120,6 +133,7 @@ 'qq-mail', 'mail-163', 'gmail-mail', + 'yahoo-mail', 'mail-2925', 'inbucket-mail', 'plus-checkout', @@ -300,6 +314,8 @@ return is163MailHost(candidate.hostname); case 'gmail-mail': return candidate.hostname === 'mail.google.com'; + case 'yahoo-mail': + return candidate.hostname === 'mail.yahoo.com'; case 'icloud-mail': return candidate.hostname === 'www.icloud.com' || candidate.hostname === 'www.icloud.com.cn'; @@ -359,6 +375,7 @@ if (normalizedHostname === 'mail.qq.com' || normalizedHostname === 'wx.mail.qq.com') return 'qq-mail'; if (is163MailHost(normalizedHostname)) return 'mail-163'; if (normalizedHostname === 'mail.google.com') return 'gmail-mail'; + if (normalizedHostname === 'mail.yahoo.com') return 'yahoo-mail'; if (normalizedHostname === 'www.icloud.com' || normalizedHostname === 'www.icloud.com.cn') return 'icloud-mail'; if (normalizedUrl.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail'; if (normalizedUrl.includes('2925.com')) return 'mail-2925'; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 286d2621..58e67405 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -499,6 +499,7 @@ + @@ -512,6 +513,20 @@ +
+