From 4b6f69fa14b9bb3164d6fb5f07282f243d94a957 Mon Sep 17 00:00:00 2001 From: P0me1oo <152391722+ayearofficial@users.noreply.github.com> Date: Wed, 10 Jun 2026 05:45:49 +0800 Subject: [PATCH] Add outlookEmail provider support --- Release.md | 86 +--- background.js | 221 ++++++++- background/logging-status.js | 1 + background/outlook-email-provider.js | 639 ++++++++++++++++++++++++++ background/steps/fetch-login-code.js | 2 + background/steps/fetch-signup-code.js | 3 + background/verification-flow.js | 9 + docs/releases/v0.2.1.md | 33 ++ mail-provider-utils.js | 6 + manifest.json | 4 +- outlook-email-utils.js | 243 ++++++++++ sidepanel/sidepanel.html | 74 ++- sidepanel/sidepanel.js | 268 +++++++++++ 13 files changed, 1518 insertions(+), 71 deletions(-) create mode 100644 background/outlook-email-provider.js create mode 100644 docs/releases/v0.2.1.md create mode 100644 outlook-email-utils.js diff --git a/Release.md b/Release.md index 5477213..2ae4ee0 100644 --- a/Release.md +++ b/Release.md @@ -1,79 +1,33 @@ -# GuJumpgate v0.2.0 +# GuJumpgate v0.2.1 -发布日期:2026-06-09 - -## 最短使用路径 -全程日本IP -启用PP爆破模式,安装指纹浏览器: RoxyBrowser 或 AdsPower 配置对应API +发布日期:2026-06-10 ## 本次更新 -### 版本与发布包 - -- 扩展版本号升级到 `0.2.0`,侧边栏显示为 `GuJumpgate V0.2.0`。 -- 新增 PPBoom 本地助手启动脚本: - - Windows:`start-ppboom.bat` - - macOS:`start-ppboom.command` -- Windows 发布包支持携带便携 Python 运行时,降低本机 Python 环境缺失或版本不一致导致的启动失败概率。 - -### PPBoom / PP爆破模式 - -- 新增 PPBoom 本地助手链路,步骤 6 可通过本地 helper 串行创建 PayPal 订阅入口。 -- 侧边栏新增 `PP爆破模式` 配置区,支持启用/关闭、保存、清除、运行状态展示、暂停任务和继续任务。 -- 新增 PPBoom 浏览器后端选择: - - 当前浏览器 - - AdsPower - - RoxyBrowser -- AdsPower / RoxyBrowser 支持配置 API 地址、API Key、窗口 ID,并由本地 helper 接管独立浏览器执行。 -- PPBoom 支持分别配置 `JP` 初始阶段代理与 `US` Provider 阶段代理,手机号注册时可按阶段切换代理。 -- 新增 PPBoom 运行参数:连续串行次数、支付页语言、Stripe Publishable Key、Device ID、User Agent、重建 Checkout 次数。 -- 新增 PPBoom 专属流程定义,启用后步骤 6 显示为 `爆破 Plus Checkout`,并兼容邮箱注册、手机号注册、绑定邮箱后重登、Sub2API Session 与 CPA Session 流程。 -- PPBoom 任务状态会回写扩展运行态,侧边栏可显示当前 attempt、pending/running/paused/succeeded/failed 状态。 - -### 支付完成与恢复链路 - -- PPBoom 命中 OpenAI 支付完成页后,会新开 ChatGPT 会话页复核 Plus 状态,确认 Plus 生效后再完成步骤 9。 -- AdsPower / PPBoom 独立支付链路完成后,会读取 ChatGPT session,确认付费 plan 或订阅状态。 -- PPBoom 返回 `User is already paid` / `already subscribed` 时,会按已有订阅处理并跳过后续支付节点。 -- PayPal genericError / 授权页异常恢复时,会先清理 PayPal 会话再重建 Checkout。 -- 清理 PayPal 会话时,除了 cookie,也会清理相关 PayPal 标签页的 `localStorage` / `sessionStorage`。 -- `pm-redirects.stripe.com` 已纳入 PayPal 链路识别,步骤 8 会等待 Stripe 中转进入 PayPal 后再继续授权。 -- Hosted Checkout 增加完成页文案识别,用于辅助判断支付完成或 checkout 会话结束状态。 - ### 邮箱来源 -- 新增 `MoeMail` provider,接入邮箱生成、验证码轮询和主流程读取链路。 -- 新增 `YYDS Mail` provider,接入邮箱生成、验证码轮询和主流程读取链路。 -- 侧边栏新增对应 provider 配置项,并支持配置回填与状态恢复。 - -### Hosted Checkout / PayPal 填写 - -- Hosted Checkout 支持按模式保存和恢复 profile,自动运行恢复时会保留当前 `plusCheckoutMode`。 -- Hosted Checkout 增强验证码提取与过滤,支持 `SmsCode` 字段,并避免把示例码、说明文本或无效响应当成真实验证码。 -- Hosted Checkout 日区资料会规范都道府县、邮编、生日、信用卡有效期和密码格式。 -- PayPal 授权页会优先识别可授权状态,再处理登录态,减少已进入授权页却继续走登录分支的问题。 -- PayPal / Hosted Checkout 拒卡或 genericError 场景支持自动重建或换资料重试。 - -### 手机验证码与 WhatsApp 链路 - -- `requestAdditionalSms` 支持返回新的 activation,并立即刷新后台运行态,减少补码后沿用旧 activation 的错位问题。 -- 增强“无法向该号码发送短信 / 验证码”类中英文错误识别。 -- WhatsApp 识别逻辑区分“纯 WhatsApp 页面”和“短信 / WhatsApp 选择器文案”,避免混合文案误触发重开。 -- WA 自动重试默认次数调整为 `5` 次;添加手机号页提示短信切换到 WhatsApp 时,会刷新 add-phone 并重新取号。 +- 新增 `outlookEmail` 邮箱 provider,支持通过 `assast/outlookEmail` 的 API Key 和服务地址读取邮箱与邮件。 +- 新增 outlookEmail 密码配置,用于项目邮箱认领、CSRF 会话写操作、分组查询和标签写入。 +- 支持配置 outlookEmail 项目 Key、分组 ID、分组名、调用方前缀;项目留空时按分组或账号列表直接取用邮箱。 +- 支持为认领邮箱配置自定义邮箱后缀;留空时使用 outlookEmail 返回的原始邮箱域名。 +- 支持注册成功后给邮箱添加自定义标签,标签名留空时跳过。 +- 注册标签会在步骤 5 完成后写入;如果当前领取记录丢失,会按当前邮箱反查 outlookEmail 账号后再写入。 +- 支持 Plus 开通确认后给邮箱添加自定义标签,标签名留空时跳过。 +- 支持配置“跳过标签”,认领邮箱已有该标签时自动释放并重新认领,例如跳过已标记为“已注册”的邮箱。 -### Hotmail 管理器 +### 侧边栏 -- Hotmail 管理器新增从后台主动回读最新状态的同步逻辑。 -- 导入、保存、切换、校验、测试、删除后会立即刷新侧边栏数据。 -- 修复保存空 payload 时可能覆盖已有账号列表的问题。 +- 邮箱服务和邮箱生成器下拉框新增 `outlookEmail`。 +- 新增 outlookEmail 配置卡,包含服务地址、API Key、密码、项目、分组、邮箱后缀、注册标签、Plus 标签、跳过标签和调用方前缀。 +- outlookEmail 生成邮箱前会校验服务地址、API Key 和密码,避免在流程中间才暴露缺失配置。 -### 文档 +### 版本 -- README 新增 PPBoom 本地助手启动说明。 -- 新增 AdsPower PPBoom 架构设计文档,说明主扩展、helper、AdsPower / RoxyBrowser worker 的职责拆分。 +- 扩展版本号升级到 `0.2.1`,侧边栏显示为 `GuJumpgate V0.2.1`。 ## 使用提醒 -- 启用 PPBoom / PP爆破模式前,请先运行 `start-ppboom.bat` 或 `start-ppboom.command`。 -- 使用 AdsPower / RoxyBrowser 后端时,请提前确认窗口ID完整、正确 -- RoxyBrowser 当前仅支持 Chrome 内核窗口。 +- outlookEmail 的项目字段可留空;留空时扩展按分组或账号列表直接取邮箱,不调用项目 `claim-random`。 +- 填写项目 Key 时,服务端需要已配置对应项目池;扩展会调用项目 `claim-random` 和 `complete-success` 接口。 +- outlookEmail API 文档中的外部 API Key 主要覆盖读取接口;项目认领和标签写入属于会话写操作,因此需要配置密码。 +- 分组 ID / 分组名用于限定直接取邮箱的账号范围;填写项目 Key 时,随机认领仍以 outlookEmail 服务端项目池配置为准。 diff --git a/background.js b/background.js index 9e872d3..918828d 100644 --- a/background.js +++ b/background.js @@ -73,8 +73,10 @@ importScripts( 'moemail-utils.js', 'yydsmail-utils.js', 'outlook-email-plus-utils.js', + 'outlook-email-utils.js', 'background/freemail-provider.js', 'background/outlook-email-plus-provider.js', + 'background/outlook-email-provider.js', 'background/cloudmail-provider.js', 'background/moemail-provider.js', 'background/yydsmail-provider.js', @@ -395,6 +397,23 @@ const { normalizeOutlookEmailPlusVerificationCode, unwrapOutlookEmailPlusResponse, } = self.OutlookEmailPlusUtils; +const { + OUTLOOK_EMAIL_GENERATOR, + OUTLOOK_EMAIL_PROVIDER, + buildOutlookEmailApiHeaders, + joinOutlookEmailUrl, + normalizeOutlookEmailAccount, + normalizeOutlookEmailAddress, + normalizeOutlookEmailBaseUrl, + normalizeOutlookEmailCallerIdPrefix, + normalizeOutlookEmailDomain, + normalizeOutlookEmailMessages, + normalizeOutlookEmailProjectKey, + normalizeOutlookEmailTags, + normalizeOutlookEmailVerificationCode, + replaceOutlookEmailDomain, + unwrapOutlookEmailResponse, +} = self.OutlookEmailUtils; const { findIcloudAliasByEmail, getConfiguredIcloudHostPreference, @@ -1316,6 +1335,17 @@ const PERSISTED_SETTING_DEFAULTS = { outlookEmailPlusProjectKey: 'openai', outlookEmailPlusCallerIdPrefix: 'gujumpgate', outlookEmailPlusAliasMaxPerMailbox: OUTLOOK_ALIAS_DEFAULT_MAX_PER_ACCOUNT, + outlookEmailBaseUrl: '', + outlookEmailApiKey: '', + outlookEmailPassword: '', + outlookEmailProjectKey: '', + outlookEmailGroupId: '', + outlookEmailGroupName: '', + outlookEmailDomain: '', + outlookEmailRegisteredTagName: '', + outlookEmailPlusTagName: '', + outlookEmailSkipTagName: '', + outlookEmailCallerIdPrefix: 'gujumpgate', hotmailAccounts: [], hotmailAliasEnabled: false, outlookAliasMaxPerAccount: OUTLOOK_ALIAS_DEFAULT_MAX_PER_ACCOUNT, @@ -1665,6 +1695,7 @@ const DEFAULT_STATE = { currentPayPalAccountId: null, currentHotmailAccountId: null, currentOutlookEmailPlusClaim: null, + currentOutlookEmailClaim: null, currentMail2925AccountId: null, preferredIcloudHost: '', ipProxyApplied: false, @@ -2861,6 +2892,7 @@ function normalizeEmailGenerator(value = '') { if (normalized === FREEMAIL_GENERATOR) return FREEMAIL_GENERATOR; if (normalized === MOEMAIL_GENERATOR) return MOEMAIL_GENERATOR; if (normalized === YYDSMAIL_GENERATOR) return YYDSMAIL_GENERATOR; + if (normalized === OUTLOOK_EMAIL_GENERATOR) return OUTLOOK_EMAIL_GENERATOR; if (normalized === OUTLOOK_EMAIL_PLUS_GENERATOR) return OUTLOOK_EMAIL_PLUS_GENERATOR; return 'duck'; } @@ -3100,6 +3132,18 @@ async function markCurrentRegistrationAccountUsed(state = {}, options = {}) { }); updated = Boolean(outlookEmailPlusResult?.handled) || updated; + if (hasOutlookEmailRegistrationTagTarget(latestState)) { + const tagResult = await markCurrentOutlookEmailRegisteredTag(latestState, { + level: options.level || 'ok', + }); + const completion = !hasCurrentOutlookEmailClaim(latestState) || latestState.plusModeEnabled + ? { handled: false, reason: latestState.plusModeEnabled ? 'defer_until_plus_result' : 'missing_claim' } + : await completeCurrentOutlookEmailClaim(latestState, { + result: 'registration_success', + }); + updated = Boolean(tagResult?.handled) || Boolean(completion?.handled) || updated; + } + if (typeof markCurrentCustomEmailPoolEntryUsed === 'function') { const result = await markCurrentCustomEmailPoolEntryUsed(latestState, { logPrefix: `${reasonPrefix}:自定义邮箱池`, @@ -3368,6 +3412,7 @@ function normalizeMailProvider(value = '') { case FREEMAIL_PROVIDER: case MOEMAIL_PROVIDER: case YYDSMAIL_PROVIDER: + case OUTLOOK_EMAIL_PROVIDER: case OUTLOOK_EMAIL_PLUS_PROVIDER: case '163': case '163-vip': @@ -3732,6 +3777,41 @@ const { pollOutlookEmailPlusVerificationCode, releaseOutlookEmailPlusClaim, } = outlookEmailPlusProvider; +const outlookEmailProvider = self.MultiPageBackgroundOutlookEmailProvider.createOutlookEmailProvider({ + addLog, + broadcastDataUpdate, + buildOutlookEmailApiHeaders, + extractVerificationCodeFromMessage, + fetchImpl: typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getState, + joinOutlookEmailUrl, + normalizeOutlookEmailAccount, + normalizeOutlookEmailAddress, + normalizeOutlookEmailBaseUrl, + normalizeOutlookEmailCallerIdPrefix, + normalizeOutlookEmailDomain, + normalizeOutlookEmailMessages, + normalizeOutlookEmailProjectKey, + normalizeOutlookEmailTags, + normalizeOutlookEmailVerificationCode, + OUTLOOK_EMAIL_GENERATOR, + persistRegistrationEmailState, + pickVerificationMessageWithTimeFallback, + replaceOutlookEmailDomain, + setEmailState, + setState, + sleepWithStop, + throwIfStopped, + unwrapOutlookEmailResponse, +}); +const { + claimOutlookEmailAddress, + completeOutlookEmailClaim, + getOutlookEmailConfig, + markOutlookEmailTag, + pollOutlookEmailVerificationCode, + releaseOutlookEmailClaim, +} = outlookEmailProvider; function normalizeSub2ApiGroupNames(value = '') { const source = Array.isArray(value) @@ -4261,6 +4341,9 @@ function normalizePersistentSettingValue(key, value) { if (normalizedMailProvider === YYDSMAIL_PROVIDER) { return YYDSMAIL_PROVIDER; } + if (normalizedMailProvider === OUTLOOK_EMAIL_PROVIDER) { + return OUTLOOK_EMAIL_PROVIDER; + } if (normalizedMailProvider === ICLOUD_PROVIDER || normalizedMailProvider === ICLOUD_API_PROVIDER) { return normalizedMailProvider; } @@ -4399,6 +4482,22 @@ function normalizePersistentSettingValue(key, value) { value, PERSISTED_SETTING_DEFAULTS.outlookEmailPlusAliasMaxPerMailbox ); + case 'outlookEmailBaseUrl': + return normalizeOutlookEmailBaseUrl(value); + case 'outlookEmailApiKey': + case 'outlookEmailPassword': + case 'outlookEmailGroupId': + case 'outlookEmailGroupName': + case 'outlookEmailRegisteredTagName': + case 'outlookEmailPlusTagName': + case 'outlookEmailSkipTagName': + return String(value || '').trim(); + case 'outlookEmailProjectKey': + return normalizeOutlookEmailProjectKey(value); + case 'outlookEmailDomain': + return normalizeOutlookEmailDomain(value); + case 'outlookEmailCallerIdPrefix': + return normalizeOutlookEmailCallerIdPrefix(value) || PERSISTED_SETTING_DEFAULTS.outlookEmailCallerIdPrefix; case 'hotmailAccounts': return normalizeHotmailAccounts(value); case 'hotmailAliasEnabled': @@ -6064,6 +6163,77 @@ async function markCurrentOutlookEmailPlusAliasUsed(state = {}, options = {}) { } } +function isOutlookEmailProvider(stateOrProvider) { + const provider = typeof stateOrProvider === 'string' + ? stateOrProvider + : stateOrProvider?.mailProvider; + return provider === OUTLOOK_EMAIL_PROVIDER; +} + +function hasCurrentOutlookEmailClaim(state = {}) { + return isOutlookEmailProvider(state) + && Boolean(state.currentOutlookEmailClaim?.address || state.currentOutlookEmailClaim?.accountId); +} + +function hasOutlookEmailRegistrationTagTarget(state = {}) { + if (!isOutlookEmailProvider(state)) return false; + if (hasCurrentOutlookEmailClaim(state)) return true; + return Boolean(String(state?.email || '').trim()); +} + +async function markCurrentOutlookEmailConfiguredTag(state = {}, tagName = '', options = {}) { + if (!hasOutlookEmailRegistrationTagTarget(state) || !String(tagName || '').trim() || typeof markOutlookEmailTag !== 'function') { + return { handled: false, reason: 'missing_target_or_tag' }; + } + try { + const targetEmail = String( + state?.currentOutlookEmailClaim?.primaryEmail + || state?.currentOutlookEmailClaim?.address + || state?.email + || '' + ).trim(); + const label = String(options.label || '').trim() || '标签'; + await addLog(`outlookEmail:准备给 ${targetEmail || '当前账号'} 写入${label} ${String(tagName || '').trim()}`, options.level || 'ok'); + return await markOutlookEmailTag(state, tagName, { + level: options.level || 'ok', + }); + } catch (error) { + const label = String(options.label || '').trim() || '标签'; + await addLog(`outlookEmail:${label}写入失败:${error?.message || error}`, 'warn'); + return { handled: false, error }; + } +} + +async function markCurrentOutlookEmailRegisteredTag(state = {}, options = {}) { + return markCurrentOutlookEmailConfiguredTag(state, state?.outlookEmailRegisteredTagName, { + ...options, + label: '注册标签', + }); +} + +async function markCurrentOutlookEmailPlusTag(state = {}, options = {}) { + return markCurrentOutlookEmailConfiguredTag(state, state?.outlookEmailPlusTagName, { + ...options, + label: 'Plus 标签', + }); +} + +async function completeCurrentOutlookEmailClaim(state = {}, options = {}) { + if (!hasCurrentOutlookEmailClaim(state) || typeof completeOutlookEmailClaim !== 'function') { + return { handled: false, reason: 'missing_claim' }; + } + try { + const result = await completeOutlookEmailClaim(state, { + result: options.result || 'registration_success', + detail: options.detail || options.result || 'registration_success', + }); + return { handled: Boolean(result?.completed), result }; + } catch (error) { + await addLog(`outlookEmail:完成项目邮箱回调失败:${error?.message || error}`, 'warn'); + return { handled: false, error }; + } +} + async function releaseCurrentOutlookEmailPlusClaim(state = {}, options = {}) { if (!hasCurrentOutlookEmailPlusClaim(state) || typeof releaseOutlookEmailPlusClaim !== 'function') { return { handled: false, reason: 'missing_claim' }; @@ -12993,6 +13163,15 @@ async function reportCompletedStepSideEffectError(step, error) { async function runCompletedNodeSideEffects(nodeId, payload, completionState, lastNodeId) { await handleNodeData(nodeId, payload); + if (nodeId === 'fill-profile') { + const latestState = await getState(); + if (hasOutlookEmailRegistrationTagTarget(latestState)) { + await markCurrentRegistrationAccountUsed(latestState, { + logPrefix: '步骤 5 完成', + level: 'ok', + }); + } + } if (nodeId === lastNodeId) { await appendAndBroadcastAccountRunRecord('success', completionState); } @@ -13805,6 +13984,9 @@ function getEmailGeneratorLabel(generator) { if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email'; if (generator === CLOUD_MAIL_GENERATOR) return 'Cloud Mail'; if (generator === FREEMAIL_GENERATOR) return 'freemail'; + if (generator === MOEMAIL_GENERATOR) return 'MoeMail'; + if (generator === YYDSMAIL_GENERATOR) return 'YYDS Mail'; + if (generator === OUTLOOK_EMAIL_GENERATOR) return 'outlookEmail'; if (generator === OUTLOOK_EMAIL_PLUS_GENERATOR) return 'Outlook Email Plus'; return 'Duck 邮箱'; } @@ -13994,8 +14176,22 @@ async function fetchGeneratedEmail(state, options = {}) { yydsMailBaseUrl: options.yydsMailBaseUrl ?? currentState.yydsMailBaseUrl, yydsMailApiKey: options.yydsMailApiKey ?? currentState.yydsMailApiKey, yydsMailDomain: options.yydsMailDomain ?? currentState.yydsMailDomain, + outlookEmailBaseUrl: options.outlookEmailBaseUrl ?? currentState.outlookEmailBaseUrl, + outlookEmailApiKey: options.outlookEmailApiKey ?? currentState.outlookEmailApiKey, + outlookEmailPassword: options.outlookEmailPassword ?? currentState.outlookEmailPassword, + outlookEmailProjectKey: options.outlookEmailProjectKey ?? currentState.outlookEmailProjectKey, + outlookEmailGroupId: options.outlookEmailGroupId ?? currentState.outlookEmailGroupId, + outlookEmailGroupName: options.outlookEmailGroupName ?? currentState.outlookEmailGroupName, + outlookEmailDomain: options.outlookEmailDomain ?? currentState.outlookEmailDomain, + outlookEmailRegisteredTagName: options.outlookEmailRegisteredTagName ?? currentState.outlookEmailRegisteredTagName, + outlookEmailPlusTagName: options.outlookEmailPlusTagName ?? currentState.outlookEmailPlusTagName, + outlookEmailSkipTagName: options.outlookEmailSkipTagName ?? currentState.outlookEmailSkipTagName, + outlookEmailCallerIdPrefix: options.outlookEmailCallerIdPrefix ?? currentState.outlookEmailCallerIdPrefix, }; const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator); + if (generator === OUTLOOK_EMAIL_GENERATOR) { + return claimOutlookEmailAddress(mergedState, options); + } if (generator === OUTLOOK_EMAIL_PLUS_GENERATOR) { return claimOutlookEmailPlusAddress(currentState, options); } @@ -15683,6 +15879,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create FREEMAIL_PROVIDER, ICLOUD_API_PROVIDER, MOEMAIL_PROVIDER, + OUTLOOK_EMAIL_PROVIDER, YYDSMAIL_PROVIDER, OUTLOOK_EMAIL_PLUS_PROVIDER, completeNodeFromBackground, @@ -15708,6 +15905,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create pollFreemailVerificationCode, pollIcloudApiVerificationCode, pollMoemailVerificationCode, + pollOutlookEmailVerificationCode, pollYydsMailVerificationCode, pollOutlookEmailPlusVerificationCode, pollHotmailVerificationCode, @@ -15855,6 +16053,7 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({ CLOUD_MAIL_PROVIDER, FREEMAIL_PROVIDER, MOEMAIL_PROVIDER, + OUTLOOK_EMAIL_PROVIDER, YYDSMAIL_PROVIDER, resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep, reuseOrCreateTab, @@ -15916,6 +16115,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ CLOUD_MAIL_PROVIDER, FREEMAIL_PROVIDER, MOEMAIL_PROVIDER, + OUTLOOK_EMAIL_PROVIDER, YYDSMAIL_PROVIDER, completeNodeFromBackground, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, @@ -16147,6 +16347,22 @@ async function executeReloginBoundEmail(state = {}) { }); } +async function executePlusReturnConfirmWithOutlookEmailTag(state = {}) { + const result = await plusReturnConfirmExecutor.executePlusReturnConfirm(state); + try { + const latestState = await getState(); + if (hasCurrentOutlookEmailClaim(latestState)) { + await markCurrentOutlookEmailPlusTag(latestState, { level: 'ok' }); + await completeCurrentOutlookEmailClaim(latestState, { + result: 'plus_success', + }); + } + } catch (error) { + await addLog(`outlookEmail:Plus 标签写入失败:${error?.message || error}`, 'warn'); + } + return result; +} + const stepExecutorsByKey = { 'open-chatgpt': () => step1Executor.executeStep1(), 'submit-signup-email': (state) => step2Executor.executeStep2(state), @@ -16161,7 +16377,7 @@ const stepExecutorsByKey = { 'paypal-approve': (state) => normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY ? goPayApproveExecutor.executeGoPayApprove(state) : payPalApproveExecutor.executePayPalApprove(state), - 'plus-checkout-return': (state) => plusReturnConfirmExecutor.executePlusReturnConfirm(state), + 'plus-checkout-return': (state) => executePlusReturnConfirmWithOutlookEmailTag(state), 'sub2api-session-import': (state) => sub2ApiSessionImportExecutor.executeSub2ApiSessionImport(state), 'cpa-session-import': (state) => cpaSessionImportExecutor.executeCpaSessionImport(state), 'oauth-login': (state) => step7Executor.executeStep7(state), @@ -16568,6 +16784,9 @@ function getMailConfig(state) { if (provider === YYDSMAIL_PROVIDER) { return { provider: YYDSMAIL_PROVIDER, label: 'YYDS Mail' }; } + if (provider === OUTLOOK_EMAIL_PROVIDER) { + return { provider: OUTLOOK_EMAIL_PROVIDER, label: 'outlookEmail' }; + } if (provider === OUTLOOK_EMAIL_PLUS_PROVIDER) { return { provider: OUTLOOK_EMAIL_PLUS_PROVIDER, label: 'Outlook Email Plus' }; } diff --git a/background/logging-status.js b/background/logging-status.js index 6dd73c8..a0ca457 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -39,6 +39,7 @@ 'moemail': 'MoeMail', 'yydsmail': 'YYDS Mail', 'outlook-email-plus': 'Outlook Email Plus', + 'outlook-email': 'outlookEmail', 'plus-checkout': 'Plus Checkout', 'paypal-flow': 'PayPal 授权页', 'gopay-flow': 'GoPay 授权页', diff --git a/background/outlook-email-provider.js b/background/outlook-email-provider.js new file mode 100644 index 0000000..35f9fc4 --- /dev/null +++ b/background/outlook-email-provider.js @@ -0,0 +1,639 @@ +(function outlookEmailProviderModule(root, factory) { + root.MultiPageBackgroundOutlookEmailProvider = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createOutlookEmailProviderModule() { + function createOutlookEmailProvider(deps = {}) { + const { + addLog = async () => {}, + broadcastDataUpdate = null, + buildOutlookEmailApiHeaders, + extractVerificationCodeFromMessage = null, + fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getState = async () => ({}), + joinOutlookEmailUrl, + normalizeOutlookEmailAccount, + normalizeOutlookEmailAddress, + normalizeOutlookEmailBaseUrl, + normalizeOutlookEmailCallerIdPrefix, + normalizeOutlookEmailDomain, + normalizeOutlookEmailMessages, + normalizeOutlookEmailProjectKey, + normalizeOutlookEmailTags = null, + normalizeOutlookEmailVerificationCode, + persistRegistrationEmailState = null, + pickVerificationMessageWithTimeFallback = null, + replaceOutlookEmailDomain, + setEmailState = async () => {}, + setState = async () => {}, + sleepWithStop = async () => {}, + throwIfStopped = () => {}, + unwrapOutlookEmailResponse, + OUTLOOK_EMAIL_GENERATOR = 'outlook-email', + } = deps; + + const activeClaims = new Map(); + let sessionReady = false; + let csrfToken = ''; + + async function persistResolvedEmailState(state = null, email, options = {}) { + if (typeof persistRegistrationEmailState === 'function') { + await persistRegistrationEmailState(state, email, options); + return; + } + await setEmailState(email, options); + } + + function getOutlookEmailConfig(state = {}) { + return { + baseUrl: normalizeOutlookEmailBaseUrl(state.outlookEmailBaseUrl), + apiKey: String(state.outlookEmailApiKey || '').trim(), + password: String(state.outlookEmailPassword || ''), + projectKey: normalizeOutlookEmailProjectKey(state.outlookEmailProjectKey), + groupId: String(state.outlookEmailGroupId || '').trim(), + groupName: String(state.outlookEmailGroupName || '').trim(), + domain: normalizeOutlookEmailDomain(state.outlookEmailDomain), + registeredTagName: String(state.outlookEmailRegisteredTagName || '').trim(), + plusTagName: String(state.outlookEmailPlusTagName || '').trim(), + skipTagName: String(state.outlookEmailSkipTagName || '').trim(), + callerIdPrefix: normalizeOutlookEmailCallerIdPrefix(state.outlookEmailCallerIdPrefix) || 'gujumpgate', + }; + } + + function ensureOutlookEmailConfig(state, options = {}) { + const { requireApiKey = true, requirePassword = false } = options; + const config = getOutlookEmailConfig(state); + if (!config.baseUrl) throw new Error('outlookEmail 服务地址为空或格式无效。'); + if (requireApiKey && !config.apiKey) throw new Error('outlookEmail API Key 为空。'); + if (requirePassword && !config.password) throw new Error('outlookEmail 密码为空,无法使用分组、项目领取或标签写入。'); + return config; + } + + async function requestJson(config, path, options = {}) { + if (!fetchImpl) throw new Error('outlookEmail 当前运行环境不支持 fetch。'); + const { + method = 'GET', + payload, + searchParams = null, + timeoutMs = 20000, + auth = 'api', + } = options; + const rawPath = String(path || '').trim(); + const url = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(rawPath) + ? new URL(rawPath) + : new URL(joinOutlookEmailUrl(config.baseUrl, rawPath)); + if (searchParams && typeof searchParams === 'object') { + for (const [key, value] of Object.entries(searchParams)) { + if (value === undefined || value === null || value === '') continue; + url.searchParams.set(key, String(value)); + } + } + + const headers = auth === 'session' + ? { + ...(payload !== undefined ? { 'Content-Type': 'application/json' } : {}), + Accept: 'application/json', + ...(csrfToken ? { 'X-CSRFToken': csrfToken } : {}), + } + : buildOutlookEmailApiHeaders(config, { json: payload !== undefined }); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs); + let response; + try { + response = await fetchImpl(url.toString(), { + method, + headers, + body: payload !== undefined ? JSON.stringify(payload) : undefined, + credentials: auth === 'session' ? 'include' : 'omit', + redirect: 'follow', + signal: controller.signal, + }); + } catch (err) { + const message = err?.name === 'AbortError' + ? `outlookEmail 请求超时(>${Math.round(timeoutMs / 1000)} 秒)` + : `outlookEmail 请求失败:${err?.message || err}`; + throw new Error(message); + } finally { + clearTimeout(timeoutId); + } + + const text = await response.text(); + let parsed; + try { + parsed = text ? JSON.parse(text) : {}; + } catch { + parsed = text; + } + if (!response.ok) { + const parsedError = parsed && typeof parsed === 'object' ? (parsed.error || parsed.message || parsed.msg) : ''; + throw new Error(`outlookEmail 请求失败:${parsedError || text || `HTTP ${response.status}`}`); + } + return unwrapOutlookEmailResponse(parsed); + } + + async function ensureSession(config) { + if (sessionReady && csrfToken) return true; + const login = await requestJson(config, '/api/extension/login', { + method: 'POST', + payload: { password: config.password, next: '/' }, + auth: 'session', + timeoutMs: 15000, + }); + const launchUrl = String(login?.launch_url || '').trim(); + if (!launchUrl) throw new Error('outlookEmail 登录未返回 launch_url。'); + await requestJson(config, launchUrl, { auth: 'session', timeoutMs: 15000 }); + const csrf = await requestJson(config, '/api/csrf-token', { auth: 'session', timeoutMs: 15000 }); + csrfToken = String(csrf?.csrf_token || '').trim(); + sessionReady = true; + return true; + } + + function buildRandomIdentifier() { + if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID(); + return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; + } + + function normalizeIdentifierPart(value = '') { + return String(value || '') + .trim() + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, '-') + .replace(/-{2,}/g, '-') + .replace(/^[-._]+|[-._]+$/g, ''); + } + + function buildCallerId(config, state = {}, taskId = '') { + const explicit = normalizeIdentifierPart(state.currentOutlookEmailClaim?.callerId); + if (explicit) return explicit; + const suffix = normalizeIdentifierPart(state.runId || state.activeRunId || taskId) || normalizeIdentifierPart(buildRandomIdentifier()); + return `${config.callerIdPrefix}-${suffix}`; + } + + function resolveTaskId(state = {}) { + return normalizeIdentifierPart( + state.currentOutlookEmailClaim?.taskId + || state.taskId + || state.activeRunId + || state.runId + ) || buildRandomIdentifier(); + } + + function rememberClaim(claim = {}) { + for (const key of [claim.taskId, claim.accountId, claim.address].map((value) => String(value || '').trim().toLowerCase()).filter(Boolean)) { + activeClaims.set(key, claim); + } + } + + function getRememberedClaim(claim = {}) { + for (const key of [claim.taskId, claim.accountId, claim.address].map((value) => String(value || '').trim().toLowerCase()).filter(Boolean)) { + const remembered = activeClaims.get(key); + if (remembered) return remembered; + } + return null; + } + + async function resolveGroupId(config) { + if (config.groupId) return config.groupId; + if (!config.groupName) return ''; + await ensureSession(config); + const result = await requestJson(config, '/api/groups', { auth: 'session' }); + const groups = Array.isArray(result?.groups) ? result.groups : []; + const group = groups.find((item) => String(item?.name || '').trim() === config.groupName); + if (!group?.id) throw new Error(`outlookEmail 未找到邮箱分组:${config.groupName}`); + return String(group.id); + } + + async function findAccountByEmail(config, email) { + const normalizedEmail = normalizeOutlookEmailAddress(email); + if (!normalizedEmail) return null; + const groupId = await resolveGroupId(config).catch(() => ''); + const result = await requestJson(config, '/api/external/accounts', { + searchParams: { group_id: groupId || undefined }, + }); + const accounts = Array.isArray(result?.accounts) ? result.accounts : []; + return accounts + .map(normalizeOutlookEmailAccount) + .find((account) => account.address === normalizedEmail || account.primaryEmail === normalizedEmail) + || null; + } + + async function listExternalAccounts(config) { + const groupId = await resolveGroupId(config).catch(() => config.groupId || ''); + const result = await requestJson(config, '/api/external/accounts', { + searchParams: { group_id: groupId || undefined }, + }); + return (Array.isArray(result?.accounts) ? result.accounts : []) + .map(normalizeOutlookEmailAccount) + .filter((account) => account.accountId && (account.address || account.primaryEmail)); + } + + async function getAccountDetail(config, accountId) { + const normalizedId = String(accountId || '').trim(); + if (!normalizedId) return null; + await ensureSession(config); + const result = await requestJson(config, `/api/accounts/${encodeURIComponent(normalizedId)}`, { + auth: 'session', + }); + return normalizeOutlookEmailAccount(result?.account || result); + } + + function normalizeTagName(value = '') { + return String(value || '').trim().toLowerCase(); + } + + async function resolveAccountTags(config, account = {}) { + const directTags = typeof normalizeOutlookEmailTags === 'function' + ? normalizeOutlookEmailTags(account.tags || account.raw?.tags || []) + : (Array.isArray(account.tags) ? account.tags : []); + if (directTags.length) return directTags; + const detail = await getAccountDetail(config, account.accountId).catch(() => null); + return typeof normalizeOutlookEmailTags === 'function' + ? normalizeOutlookEmailTags(detail?.tags || detail?.raw?.tags || []) + : (Array.isArray(detail?.tags) ? detail.tags : []); + } + + async function accountHasSkipTag(config, account = {}) { + const skipTagName = normalizeTagName(config.skipTagName); + if (!skipTagName) return false; + const tags = await resolveAccountTags(config, account); + return tags.some((tag) => normalizeTagName(tag?.name || tag) === skipTagName); + } + + function resolveRegistrationAddress(account, config) { + const address = normalizeOutlookEmailAddress(account.address || account.primaryEmail); + return config.domain ? replaceOutlookEmailDomain(address, config.domain) : address; + } + + async function requestProjectClaim(config, taskId, callerId) { + return normalizeOutlookEmailAccount(await requestJson(config, `/api/projects/${encodeURIComponent(config.projectKey)}/claim-random`, { + method: 'POST', + auth: 'session', + payload: { + caller_id: callerId, + task_id: taskId, + lease_seconds: 600, + }, + })); + } + + async function releaseProjectClaim(config, claim = {}, options = {}) { + if (!config.projectKey || !claim?.claimToken) { + return { released: false, reason: 'not_project_claim' }; + } + await requestJson(config, `/api/projects/${encodeURIComponent(claim.projectKey || config.projectKey)}/release`, { + method: 'POST', + auth: 'session', + payload: { + account_id: claim.accountId, + claim_token: claim.claimToken, + caller_id: claim.callerId || '', + task_id: claim.taskId || '', + reason: options.reason || 'skip_tag_matched', + detail: options.detail || '', + }, + }); + await addLog(`outlookEmail:已释放项目邮箱 ${claim.address || claim.accountId}`, options.level || 'warn'); + return { released: true }; + } + + async function pickDirectAccountClaim(config, latestState = {}, options = {}) { + const accounts = await listExternalAccounts(config); + if (!accounts.length) { + throw new Error(config.groupId || config.groupName + ? 'outlookEmail 分组内没有可用邮箱账号。' + : 'outlookEmail 没有可用邮箱账号。'); + } + const shuffled = accounts + .map((account) => ({ account, sort: Math.random() })) + .sort((left, right) => left.sort - right.sort) + .map((entry) => entry.account); + let skippedByTag = 0; + for (const account of shuffled) { + throwIfStopped(); + if (config.skipTagName && await accountHasSkipTag(config, account)) { + skippedByTag += 1; + await addLog(`outlookEmail:邮箱 ${account.primaryEmail || account.address} 已有标签 ${config.skipTagName},已跳过。`, 'warn'); + continue; + } + const address = resolveRegistrationAddress(account, config); + if (!address) continue; + return { + accountId: account.accountId, + address, + primaryEmail: account.primaryEmail || account.address, + claimToken: '', + projectKey: '', + taskId: resolveTaskId(latestState), + callerId: buildCallerId(config, latestState, resolveTaskId(latestState)), + groupId: account.groupId || config.groupId || '', + groupName: account.groupName || config.groupName || '', + claimedAt: '', + leaseExpiresAt: '', + mode: 'direct', + }; + } + throw new Error(config.skipTagName + ? `outlookEmail 分组内 ${skippedByTag} 个邮箱均命中跳过标签 ${config.skipTagName},未找到可用邮箱。` + : 'outlookEmail 分组内未找到可用邮箱。'); + } + + async function claimOutlookEmailAddress(state, options = {}) { + throwIfStopped(); + const latestState = state || await getState(); + const config = ensureOutlookEmailConfig(latestState, { requirePassword: true }); + await ensureSession(config); + const taskId = resolveTaskId(latestState); + const callerId = buildCallerId(config, latestState, taskId); + const maxSkipAttempts = Math.max(1, Math.min(20, Math.floor(Number(options.maxSkipAttempts || latestState.outlookEmailSkipTagMaxAttempts) || 10))); + let skippedByTag = 0; + + if (!config.projectKey) { + const storedClaim = await pickDirectAccountClaim(config, latestState, options); + rememberClaim(storedClaim); + await setState({ currentOutlookEmailClaim: storedClaim }); + await persistResolvedEmailState(latestState, storedClaim.address, { + source: `generated:${OUTLOOK_EMAIL_GENERATOR}`, + preserveAccountIdentity: Boolean(options?.preserveAccountIdentity), + }); + await addLog(`outlookEmail:已从${config.groupId || config.groupName ? '指定分组' : '账号列表'}取用 ${storedClaim.primaryEmail || storedClaim.address},注册使用 ${storedClaim.address}`, 'ok'); + return storedClaim.address; + } + + for (let attempt = 1; attempt <= maxSkipAttempts; attempt += 1) { + throwIfStopped(); + const claim = await requestProjectClaim(config, taskId, callerId); + const address = resolveRegistrationAddress(claim, config); + if (!claim.accountId || !address || !claim.claimToken) { + throw new Error('outlookEmail 未返回有效的项目邮箱认领信息。'); + } + const storedClaim = { + accountId: claim.accountId, + address, + primaryEmail: claim.primaryEmail || claim.address, + claimToken: claim.claimToken, + projectKey: config.projectKey, + taskId, + callerId, + groupId: claim.groupId || config.groupId || '', + claimedAt: claim.claimedAt, + leaseExpiresAt: claim.leaseExpiresAt, + mode: 'project', + }; + + if (config.skipTagName && await accountHasSkipTag(config, { + ...claim, + ...storedClaim, + })) { + skippedByTag += 1; + await addLog(`outlookEmail:邮箱 ${claim.primaryEmail || claim.address || address} 已有标签 ${config.skipTagName},跳过并重新认领。`, 'warn'); + await releaseProjectClaim(config, storedClaim, { + reason: 'skip_tag_matched', + detail: `tag:${config.skipTagName}`, + level: 'warn', + }); + continue; + } + + rememberClaim(storedClaim); + await setState({ currentOutlookEmailClaim: storedClaim }); + await persistResolvedEmailState(latestState, address, { + source: `generated:${OUTLOOK_EMAIL_GENERATOR}`, + preserveAccountIdentity: Boolean(options?.preserveAccountIdentity), + }); + await addLog(`outlookEmail:已认领 ${claim.primaryEmail || claim.address},注册使用 ${address}`, 'ok'); + return address; + } + + throw new Error(`outlookEmail 连续 ${skippedByTag} 个邮箱命中跳过标签 ${config.skipTagName},未找到可用邮箱。`); + } + + function resolveLifecycleClaim(state = {}, options = {}) { + const stored = options.claim && typeof options.claim === 'object' + ? options.claim + : (state.currentOutlookEmailClaim || {}); + if (!stored || typeof stored !== 'object') return null; + return { + ...(getRememberedClaim(stored) || {}), + ...stored, + claimToken: stored.claimToken || getRememberedClaim(stored)?.claimToken || '', + }; + } + + async function clearStoredClaim() { + await setState({ currentOutlookEmailClaim: null }); + } + + async function completeOutlookEmailClaim(state, options = {}) { + const latestState = state || await getState(); + const config = ensureOutlookEmailConfig(latestState, { requirePassword: true }); + const claim = resolveLifecycleClaim(latestState, options); + if (!claim?.accountId) return { completed: false, reason: 'missing_claim' }; + if (!claim?.claimToken || !config.projectKey) { + await clearStoredClaim(); + return { completed: true, reason: 'direct_account_no_project' }; + } + await ensureSession(config); + await requestJson(config, `/api/projects/${encodeURIComponent(claim.projectKey || config.projectKey)}/complete-success`, { + method: 'POST', + auth: 'session', + payload: { + account_id: claim.accountId, + claim_token: claim.claimToken, + caller_id: claim.callerId || '', + task_id: claim.taskId || '', + detail: options.detail || options.result || 'success', + }, + }); + await clearStoredClaim(); + await addLog(`outlookEmail:已完成项目邮箱 ${claim.address || claim.accountId}`, 'ok'); + return { completed: true }; + } + + async function releaseOutlookEmailClaim(state, options = {}) { + const latestState = state || await getState(); + const config = ensureOutlookEmailConfig(latestState, { requirePassword: true }); + const claim = resolveLifecycleClaim(latestState, options); + if (!claim?.accountId || !claim?.claimToken) return { released: false, reason: 'missing_claim' }; + await ensureSession(config); + await releaseProjectClaim(config, claim, { + reason: options.reason || 'flow_abandoned', + detail: options.detail || '', + level: options.level || 'warn', + }); + await clearStoredClaim(); + return { released: true }; + } + + async function ensureTag(config, tagName) { + const name = String(tagName || '').trim(); + if (!name) return null; + await ensureSession(config); + const tagsResult = await requestJson(config, '/api/tags', { auth: 'session' }); + const tags = Array.isArray(tagsResult?.tags) ? tagsResult.tags : []; + const existing = tags.find((tag) => String(tag?.name || '').trim() === name); + if (existing?.id) return existing; + const created = await requestJson(config, '/api/tags', { + method: 'POST', + auth: 'session', + payload: { name, color: '#1a1a1a' }, + }); + return created?.tag || created; + } + + async function markOutlookEmailTag(state = {}, tagName = '', options = {}) { + const name = String(tagName || '').trim(); + if (!name) return { handled: false, reason: 'empty_tag' }; + const latestState = state || await getState(); + const config = ensureOutlookEmailConfig(latestState, { requirePassword: true }); + const claim = resolveLifecycleClaim(latestState, options); + const email = normalizeOutlookEmailAddress(claim?.primaryEmail || claim?.address || latestState.email); + const account = claim?.accountId ? claim : await findAccountByEmail(config, email); + if (!account?.accountId) { + await addLog(`outlookEmail:未找到可写入标签的邮箱账号 ${email || '(空)'}`, options.level || 'warn'); + return { handled: false, reason: 'missing_account' }; + } + const tag = await ensureTag(config, name); + if (!tag?.id) throw new Error(`outlookEmail 标签不可用:${name}`); + await requestJson(config, '/api/accounts/tags', { + method: 'POST', + auth: 'session', + payload: { + account_ids: [Number(account.accountId)], + tag_id: Number(tag.id), + action: 'add', + }, + }); + await addLog(`outlookEmail:已给 ${email || account.accountId} 打标签 ${name}`, options.level || 'ok'); + return { handled: true, accountId: account.accountId, tagId: tag.id, tagName: name }; + } + + function resolvePollTargetEmail(state = {}, pollPayload = {}) { + return normalizeOutlookEmailAddress( + pollPayload.targetEmail + || state.registrationEmailState?.current + || state.email + || state.currentOutlookEmailClaim?.address + || '' + ); + } + + function resolveSinceMinutes(pollPayload = {}) { + const configured = Math.floor(Number(pollPayload.sinceMinutes || pollPayload.since_minutes) || 0); + if (configured > 0) return configured; + const afterTimestamp = Number(pollPayload.filterAfterTimestamp) || 0; + if (afterTimestamp <= 0) return 0; + return Math.max(1, Math.ceil(Math.max(0, Date.now() - afterTimestamp) / 60000)); + } + + function extractCodeFromMessage(message = {}, pollPayload = {}) { + const excludeCodes = new Set((Array.isArray(pollPayload.excludeCodes) ? pollPayload.excludeCodes : []) + .map((value) => normalizeOutlookEmailVerificationCode(value)) + .filter(Boolean)); + const codeLength = Math.max(0, Math.floor(Number(pollPayload.codeLength) || 0)); + const codeRegex = String(pollPayload.codeRegex || '').trim(); + const text = [message.subject, message.bodyPreview, message.body].filter(Boolean).join('\n'); + let code = ''; + if (typeof extractVerificationCodeFromMessage === 'function') { + code = extractVerificationCodeFromMessage({ + subject: message.subject, + bodyPreview: [message.bodyPreview, message.body].filter(Boolean).join('\n'), + from: message.from, + }, { + codeLength, + codeRegex, + }) || ''; + } + if (!code) { + const pattern = codeRegex + ? new RegExp(codeRegex) + : new RegExp(`\\b\\d{${codeLength > 0 ? codeLength : '4,8'}}\\b`); + code = String(text.match(pattern)?.[1] || text.match(pattern)?.[0] || '').trim(); + } + if (!code || excludeCodes.has(code)) return null; + return { + code, + emailTimestamp: message.timestamp || Date.now(), + mailId: message.id || '', + }; + } + + function summarizeMessages(messages = []) { + return messages + .slice(0, 3) + .map((message) => `${message.subject || '无主题'} @ ${message.date || message.timestamp || '无时间'}`) + .join(';'); + } + + function extractCodeFromMessages(messages = [], pollPayload = {}) { + const filterAfterTimestamp = Number(pollPayload.filterAfterTimestamp) || 0; + const timeToleranceMs = Math.max(0, Number(pollPayload.timeToleranceMs) || 120000); + const minTimestamp = filterAfterTimestamp > 0 ? filterAfterTimestamp - timeToleranceMs : 0; + const sortedMessages = (Array.isArray(messages) ? messages : []) + .slice() + .sort((left, right) => (right.timestamp || 0) - (left.timestamp || 0)); + const candidates = sortedMessages.filter((message) => !minTimestamp || !message.timestamp || message.timestamp >= minTimestamp); + for (const message of candidates) { + const verification = extractCodeFromMessage(message, pollPayload); + if (verification?.code) { + return { + ...verification, + matchedMessageSummary: summarizeMessages([message]), + }; + } + } + return null; + } + + async function pollOutlookEmailVerificationCode(step, state, pollPayload = {}) { + const latestState = state || await getState(); + const config = ensureOutlookEmailConfig(latestState); + const targetEmail = resolvePollTargetEmail(latestState, pollPayload); + if (!targetEmail) throw new Error('outlookEmail 轮询前缺少目标邮箱地址,请先获取注册邮箱。'); + const maxAttempts = Math.max(1, Math.floor(Number(pollPayload.maxAttempts) || 5)); + const intervalMs = Math.max(0, Number(pollPayload.intervalMs) || 3000); + const sinceMinutes = resolveSinceMinutes(pollPayload); + let lastError = null; + + await addLog(`步骤 ${step}:正在轮询 outlookEmail 邮件(${targetEmail})...`, 'info'); + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + throwIfStopped(); + try { + const payload = await requestJson(config, '/api/external/emails', { + searchParams: { + email: targetEmail, + folder: 'all', + top: 10, + subject_contains: pollPayload.subjectContains || pollPayload.subject_contains || undefined, + from_contains: pollPayload.fromContains || pollPayload.from_contains || undefined, + keyword: pollPayload.keyword || undefined, + since_minutes: sinceMinutes > 0 ? sinceMinutes : undefined, + }, + }); + const messages = normalizeOutlookEmailMessages(payload); + const verification = extractCodeFromMessages(messages, pollPayload); + if (verification?.code) return { ok: true, ...verification }; + lastError = new Error(`步骤 ${step}:暂未在 outlookEmail 中找到匹配验证码(${attempt}/${maxAttempts})。`); + if (attempt === 1 && messages.length) { + await addLog(`步骤 ${step}:outlookEmail 已返回 ${messages.length} 封邮件,最近邮件:${summarizeMessages(messages)}`, 'info'); + } + await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info'); + } catch (err) { + lastError = err; + await addLog(`步骤 ${step}:outlookEmail 轮询失败:${err?.message || err}`, 'warn'); + } + if (attempt < maxAttempts) await sleepWithStop(intervalMs); + } + throw lastError || new Error(`步骤 ${step}:outlookEmail 轮询失败。`); + } + + return { + claimOutlookEmailAddress, + completeOutlookEmailClaim, + getOutlookEmailConfig, + markOutlookEmailTag, + pollOutlookEmailVerificationCode, + releaseOutlookEmailClaim, + requestOutlookEmailJson: requestJson, + }; + } + + return { createOutlookEmailProvider }; +}); diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index b507afb..cff77de 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -12,6 +12,7 @@ FREEMAIL_PROVIDER = 'freemail', ICLOUD_API_PROVIDER = 'icloud-api', MOEMAIL_PROVIDER = 'moemail', + OUTLOOK_EMAIL_PROVIDER = 'outlook-email', YYDSMAIL_PROVIDER = 'yydsmail', OUTLOOK_EMAIL_PLUS_PROVIDER = 'outlook-email-plus', completeNodeFromBackground, @@ -619,6 +620,7 @@ || mail.provider === CLOUD_MAIL_PROVIDER || mail.provider === FREEMAIL_PROVIDER || mail.provider === MOEMAIL_PROVIDER + || mail.provider === OUTLOOK_EMAIL_PROVIDER || mail.provider === YYDSMAIL_PROVIDER || mail.provider === OUTLOOK_EMAIL_PLUS_PROVIDER ) { diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index 7b5736f..5f3ed48 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -23,6 +23,7 @@ CLOUD_MAIL_PROVIDER = 'cloudmail', FREEMAIL_PROVIDER = 'freemail', MOEMAIL_PROVIDER = 'moemail', + OUTLOOK_EMAIL_PROVIDER = 'outlook-email', YYDSMAIL_PROVIDER = 'yydsmail', OUTLOOK_EMAIL_PLUS_PROVIDER = 'outlook-email-plus', resolveVerificationStep, @@ -128,6 +129,7 @@ || mail.provider === CLOUD_MAIL_PROVIDER || mail.provider === FREEMAIL_PROVIDER || mail.provider === MOEMAIL_PROVIDER + || mail.provider === OUTLOOK_EMAIL_PROVIDER || mail.provider === YYDSMAIL_PROVIDER || mail.provider === OUTLOOK_EMAIL_PLUS_PROVIDER ) { @@ -159,6 +161,7 @@ CLOUD_MAIL_PROVIDER, FREEMAIL_PROVIDER, MOEMAIL_PROVIDER, + OUTLOOK_EMAIL_PROVIDER, YYDSMAIL_PROVIDER, OUTLOOK_EMAIL_PLUS_PROVIDER, ].includes(mail.provider); diff --git a/background/verification-flow.js b/background/verification-flow.js index 435c36b..46daa4b 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -15,6 +15,7 @@ FREEMAIL_PROVIDER = 'freemail', ICLOUD_API_PROVIDER = 'icloud-api', MOEMAIL_PROVIDER = 'moemail', + OUTLOOK_EMAIL_PROVIDER = 'outlook-email', YYDSMAIL_PROVIDER = 'yydsmail', OUTLOOK_EMAIL_PLUS_PROVIDER = 'outlook-email-plus', completeNodeFromBackground, @@ -36,6 +37,7 @@ pollFreemailVerificationCode, pollIcloudApiVerificationCode, pollMoemailVerificationCode, + pollOutlookEmailVerificationCode, pollYydsMailVerificationCode, pollOutlookEmailPlusVerificationCode, pollHotmailVerificationCode, @@ -1009,6 +1011,13 @@ }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`); return pollMoemailVerificationCode(step, state, timedPoll.payload); } + if (mail.provider === OUTLOOK_EMAIL_PROVIDER) { + const timedPoll = await applyMailPollingTimeBudget(step, { + ...getVerificationPollPayload(step, state), + ...cleanPollOverrides, + }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`); + return pollOutlookEmailVerificationCode(step, state, timedPoll.payload); + } if (mail.provider === YYDSMAIL_PROVIDER) { const timedPoll = await applyMailPollingTimeBudget(step, { ...getVerificationPollPayload(step, state), diff --git a/docs/releases/v0.2.1.md b/docs/releases/v0.2.1.md new file mode 100644 index 0000000..2ae4ee0 --- /dev/null +++ b/docs/releases/v0.2.1.md @@ -0,0 +1,33 @@ +# GuJumpgate v0.2.1 + +发布日期:2026-06-10 + +## 本次更新 + +### 邮箱来源 + +- 新增 `outlookEmail` 邮箱 provider,支持通过 `assast/outlookEmail` 的 API Key 和服务地址读取邮箱与邮件。 +- 新增 outlookEmail 密码配置,用于项目邮箱认领、CSRF 会话写操作、分组查询和标签写入。 +- 支持配置 outlookEmail 项目 Key、分组 ID、分组名、调用方前缀;项目留空时按分组或账号列表直接取用邮箱。 +- 支持为认领邮箱配置自定义邮箱后缀;留空时使用 outlookEmail 返回的原始邮箱域名。 +- 支持注册成功后给邮箱添加自定义标签,标签名留空时跳过。 +- 注册标签会在步骤 5 完成后写入;如果当前领取记录丢失,会按当前邮箱反查 outlookEmail 账号后再写入。 +- 支持 Plus 开通确认后给邮箱添加自定义标签,标签名留空时跳过。 +- 支持配置“跳过标签”,认领邮箱已有该标签时自动释放并重新认领,例如跳过已标记为“已注册”的邮箱。 + +### 侧边栏 + +- 邮箱服务和邮箱生成器下拉框新增 `outlookEmail`。 +- 新增 outlookEmail 配置卡,包含服务地址、API Key、密码、项目、分组、邮箱后缀、注册标签、Plus 标签、跳过标签和调用方前缀。 +- outlookEmail 生成邮箱前会校验服务地址、API Key 和密码,避免在流程中间才暴露缺失配置。 + +### 版本 + +- 扩展版本号升级到 `0.2.1`,侧边栏显示为 `GuJumpgate V0.2.1`。 + +## 使用提醒 + +- outlookEmail 的项目字段可留空;留空时扩展按分组或账号列表直接取邮箱,不调用项目 `claim-random`。 +- 填写项目 Key 时,服务端需要已配置对应项目池;扩展会调用项目 `claim-random` 和 `complete-success` 接口。 +- outlookEmail API 文档中的外部 API Key 主要覆盖读取接口;项目认领和标签写入属于会话写操作,因此需要配置密码。 +- 分组 ID / 分组名用于限定直接取邮箱的账号范围;填写项目 Key 时,随机认领仍以 outlookEmail 服务端项目池配置为准。 diff --git a/mail-provider-utils.js b/mail-provider-utils.js index 2eca8da..63be47e 100644 --- a/mail-provider-utils.js +++ b/mail-provider-utils.js @@ -16,6 +16,7 @@ const FREEMAIL_PROVIDER = 'freemail'; const MOEMAIL_PROVIDER = 'moemail'; const YYDSMAIL_PROVIDER = 'yydsmail'; + const OUTLOOK_EMAIL_PROVIDER = 'outlook-email'; const OUTLOOK_EMAIL_PLUS_PROVIDER = 'outlook-email-plus'; 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'; @@ -37,6 +38,7 @@ case FREEMAIL_PROVIDER: case MOEMAIL_PROVIDER: case YYDSMAIL_PROVIDER: + case OUTLOOK_EMAIL_PROVIDER: case OUTLOOK_EMAIL_PLUS_PROVIDER: case '163': case '163-vip': @@ -144,6 +146,9 @@ if (provider === YYDSMAIL_PROVIDER) { return { provider: YYDSMAIL_PROVIDER, label: 'YYDS Mail' }; } + if (provider === OUTLOOK_EMAIL_PROVIDER) { + return { provider: OUTLOOK_EMAIL_PROVIDER, label: 'outlookEmail' }; + } if (provider === OUTLOOK_EMAIL_PLUS_PROVIDER) { return { provider: OUTLOOK_EMAIL_PLUS_PROVIDER, label: 'Outlook Email Plus' }; } @@ -195,6 +200,7 @@ ICLOUD_API_PROVIDER, ICLOUD_PROVIDER, MOEMAIL_PROVIDER, + OUTLOOK_EMAIL_PROVIDER, YYDSMAIL_PROVIDER, buildIcloudApiEndpoint, getIcloudForwardMailConfig, diff --git a/manifest.json b/manifest.json index 04f9284..358c220 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, "name": "GuJumpgate", - "version": "0.2.0", - "version_name": "GuJumpgate V0.2.0", + "version": "0.2.1", + "version_name": "GuJumpgate V0.2.1", "description": "用于自动执行多步骤 OAuth 注册流程", "permissions": [ "sidePanel", diff --git a/outlook-email-utils.js b/outlook-email-utils.js new file mode 100644 index 0000000..abe4a9d --- /dev/null +++ b/outlook-email-utils.js @@ -0,0 +1,243 @@ +(function outlookEmailUtilsModule(root, factory) { + if (typeof module !== 'undefined' && module.exports) { + module.exports = factory(); + return; + } + + root.OutlookEmailUtils = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createOutlookEmailUtils() { + const OUTLOOK_EMAIL_PROVIDER = 'outlook-email'; + const OUTLOOK_EMAIL_GENERATOR = 'outlook-email'; + + function firstNonEmptyString(values) { + for (const value of values) { + if (value === undefined || value === null) continue; + const normalized = String(value).trim(); + if (normalized) return normalized; + } + return ''; + } + + function normalizeOutlookEmailBaseUrl(rawValue = '') { + const value = String(rawValue || '').trim(); + if (!value) return ''; + const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`; + try { + const parsed = new URL(candidate); + if (!['http:', 'https:'].includes(parsed.protocol)) return ''; + parsed.hash = ''; + parsed.search = ''; + let pathname = String(parsed.pathname || '').replace(/\/+/g, '/'); + pathname = pathname.replace(/\/api(?:\/.*)?$/i, ''); + pathname = pathname === '/' ? '' : pathname.replace(/\/+$/g, ''); + return `${parsed.origin}${pathname}`; + } catch { + return ''; + } + } + + function joinOutlookEmailUrl(baseUrl, path) { + const normalizedBase = normalizeOutlookEmailBaseUrl(baseUrl); + const normalizedPath = String(path || '').trim(); + if (!normalizedBase || !normalizedPath) return normalizedBase || ''; + return `${normalizedBase}${normalizedPath.startsWith('/') ? '' : '/'}${normalizedPath}`; + } + + function buildOutlookEmailApiHeaders(config = {}, options = {}) { + const headers = {}; + const apiKey = firstNonEmptyString([config.apiKey, config.outlookEmailApiKey, options.apiKey]); + if (apiKey) headers['X-API-Key'] = apiKey; + if (options.json) headers['Content-Type'] = 'application/json'; + if (options.acceptJson !== false) headers.Accept = 'application/json'; + return headers; + } + + function normalizeOutlookEmailAddress(value = '') { + return String(value || '').trim().toLowerCase(); + } + + function parseOutlookEmailAddressParts(value = '') { + const normalized = normalizeOutlookEmailAddress(value); + const atIndex = normalized.lastIndexOf('@'); + if (atIndex <= 0 || atIndex >= normalized.length - 1) return null; + return { + local: normalized.slice(0, atIndex), + domain: normalized.slice(atIndex + 1), + }; + } + + function normalizeOutlookEmailDomain(value = '') { + return String(value || '') + .trim() + .toLowerCase() + .replace(/^@+/, '') + .replace(/[^\w.-]+/g, ''); + } + + function replaceOutlookEmailDomain(address = '', domain = '') { + const parts = parseOutlookEmailAddressParts(address); + const normalizedDomain = normalizeOutlookEmailDomain(domain); + if (!parts || !normalizedDomain) return normalizeOutlookEmailAddress(address); + return `${parts.local}@${normalizedDomain}`; + } + + function normalizeOutlookEmailProjectKey(value = '') { + return String(value || '').trim().toLowerCase(); + } + + function normalizeOutlookEmailCallerIdPrefix(value = '') { + return String(value || '') + .trim() + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, '-') + .replace(/-{2,}/g, '-') + .replace(/^[-._]+|[-._]+$/g, ''); + } + + function normalizeOutlookEmailTimestamp(value) { + if (value === undefined || value === null || value === '') return 0; + const numeric = Number(value); + if (Number.isFinite(numeric) && numeric > 0) { + return numeric < 1e12 ? Math.floor(numeric * 1000) : Math.floor(numeric); + } + const parsed = Date.parse(String(value).trim()); + return Number.isFinite(parsed) ? parsed : 0; + } + + function normalizeOutlookEmailAccount(value = {}) { + const source = value?.data && typeof value.data === 'object' && !Array.isArray(value.data) + ? value.data + : (value && typeof value === 'object' && !Array.isArray(value) ? value : {}); + return { + accountId: firstNonEmptyString([source.account_id, source.accountId, source.id]), + address: normalizeOutlookEmailAddress(firstNonEmptyString([source.email, source.address, source.normalized_email])), + primaryEmail: normalizeOutlookEmailAddress(firstNonEmptyString([source.primary_email, source.primaryEmail])), + groupId: firstNonEmptyString([source.group_id, source.groupId]), + groupName: firstNonEmptyString([source.group_name, source.groupName]), + provider: String(firstNonEmptyString([source.provider, source.account_type, source.accountType])).trim().toLowerCase(), + claimToken: firstNonEmptyString([source.claim_token, source.claimToken, source.token]), + claimedAt: firstNonEmptyString([source.claimed_at, source.claimedAt]), + leaseExpiresAt: firstNonEmptyString([source.lease_expires_at, source.leaseExpiresAt]), + tags: normalizeOutlookEmailTags(source.tags || source.tag_list || source.tagList), + raw: source, + }; + } + + function normalizeOutlookEmailTags(value = []) { + const source = Array.isArray(value) + ? value + : String(value || '').split(/[\r\n,,、;;]+/); + return source + .map((item) => { + if (item && typeof item === 'object' && !Array.isArray(item)) { + return { + id: firstNonEmptyString([item.id, item.tag_id, item.tagId]), + name: firstNonEmptyString([item.name, item.tag_name, item.tagName]), + color: firstNonEmptyString([item.color]), + raw: item, + }; + } + const name = String(item || '').trim(); + return name ? { id: '', name, color: '', raw: item } : null; + }) + .filter((item) => item && item.name); + } + + function normalizeOutlookEmailMessage(value = {}) { + const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {}; + const sender = source.from && typeof source.from === 'object' + ? firstNonEmptyString([source.from.emailAddress?.address, source.from.address, source.from.email, source.from.name]) + : firstNonEmptyString([source.from, source.sender, source.sender_email, source.senderEmail]); + return { + id: firstNonEmptyString([source.id, source.message_id, source.messageId, source.mail_id, source.mailId]), + subject: firstNonEmptyString([source.subject, source.title]), + from: sender, + to: firstNonEmptyString([source.to, source.recipient, source.recipient_email, source.recipientEmail]), + date: firstNonEmptyString([source.date, source.received_at, source.receivedAt, source.timestamp, source.created_at]), + timestamp: normalizeOutlookEmailTimestamp(firstNonEmptyString([ + source.received_at, + source.receivedAt, + source.date, + source.timestamp, + source.created_at, + ])), + bodyPreview: firstNonEmptyString([source.body_preview, source.bodyPreview, source.preview, source.snippet]), + body: firstNonEmptyString([source.body, source.text, source.html]), + folder: firstNonEmptyString([source.folder, source.mail_folder]), + raw: source, + }; + } + + function normalizeOutlookEmailMessages(value = {}) { + const source = value?.data && typeof value.data === 'object' && !Array.isArray(value.data) + ? value.data + : value; + const rawMessages = Array.isArray(source?.emails) + ? source.emails + : (Array.isArray(source?.messages) ? source.messages : (Array.isArray(source) ? source : [])); + return rawMessages.map(normalizeOutlookEmailMessage); + } + + function normalizeOutlookEmailVerificationCode(value = '') { + if (value && typeof value === 'object' && !Array.isArray(value)) { + const source = value?.data && typeof value.data === 'object' && !Array.isArray(value.data) + ? value.data + : value; + return { + code: normalizeOutlookEmailVerificationCode(firstNonEmptyString([ + source.code, + source.verification_code, + source.verificationCode, + ])), + emailTimestamp: normalizeOutlookEmailTimestamp(firstNonEmptyString([ + source.email_timestamp, + source.emailTimestamp, + source.received_at, + source.receivedAt, + source.timestamp, + ])), + mailId: firstNonEmptyString([source.message_id, source.messageId, source.mail_id, source.mailId, source.id]), + raw: source, + }; + } + return String(value || '').trim(); + } + + function buildOutlookEmailResponseError(payload = {}) { + const message = firstNonEmptyString([payload.message, payload.error, payload.msg, payload.detail]) + || 'outlookEmail business error'; + const error = new Error(message); + error.payload = payload; + return error; + } + + function unwrapOutlookEmailResponse(payload) { + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload; + if (payload.success === false || payload.ok === false) { + throw buildOutlookEmailResponseError(payload); + } + if (payload.success === true || payload.ok === true) { + return Object.prototype.hasOwnProperty.call(payload, 'data') ? payload.data : payload; + } + return payload; + } + + return { + OUTLOOK_EMAIL_GENERATOR, + OUTLOOK_EMAIL_PROVIDER, + buildOutlookEmailApiHeaders, + joinOutlookEmailUrl, + normalizeOutlookEmailAccount, + normalizeOutlookEmailAddress, + normalizeOutlookEmailBaseUrl, + normalizeOutlookEmailCallerIdPrefix, + normalizeOutlookEmailDomain, + normalizeOutlookEmailMessages, + normalizeOutlookEmailProjectKey, + normalizeOutlookEmailTags, + normalizeOutlookEmailTimestamp, + normalizeOutlookEmailVerificationCode, + replaceOutlookEmailDomain, + unwrapOutlookEmailResponse, + }; +}); diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 5fbaf46..4e6da85 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -4,7 +4,7 @@ - GuJumpgate V0.2.0 + GuJumpgate V0.2.1
+ aria-label="打开 GitHub Releases 页面" title="打开 GitHub Releases 页面">GuJumpgate V0.2.1
@@ -616,6 +616,7 @@ + @@ -646,6 +647,7 @@ + +