diff --git a/background.js b/background.js index b107b68..bd74bc8 100644 --- a/background.js +++ b/background.js @@ -63,7 +63,9 @@ importScripts( 'luckmail-utils.js', 'cloudflare-temp-email-utils.js', 'cloudmail-utils.js', + 'outlook-email-plus-utils.js', 'background/cloudmail-provider.js', + 'background/outlook-email-plus-provider.js', 'icloud-utils.js', 'mail-provider-utils.js', 'content/activation-utils.js' @@ -318,6 +320,25 @@ const { normalizeCloudMailDomains, normalizeCloudMailMailApiMessages, } = self.CloudMailUtils; +const { + DEFAULT_OUTLOOK_EMAIL_PLUS_BASE_URL, + buildOutlookEmailPlusAliasAddress, + buildOutlookEmailPlusHeaders, + buildOutlookEmailPlusPayPalAliasAddress, + deriveOutlookEmailPlusBaseAddress, + generateOutlookEmailPlusTag, + getOutlookEmailPlusPayPalAliasIndex, + isOutlookEmailPlusTaggedAlias, + joinOutlookEmailPlusUrl, + normalizeOutlookEmailPlusAddress, + normalizeOutlookEmailPlusBaseUrl, + normalizeOutlookEmailPlusCallerIdPrefix, + normalizeOutlookEmailPlusClaim, + normalizeOutlookEmailPlusProjectKey, + normalizeOutlookEmailPlusProvider, + normalizeOutlookEmailPlusVerificationCode, + unwrapOutlookEmailPlusResponse, +} = self.OutlookEmailPlusUtils; const { findIcloudAliasByEmail, getConfiguredIcloudHostPreference, @@ -462,6 +483,8 @@ const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email'; const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email'; const CLOUD_MAIL_PROVIDER = 'cloudmail'; const CLOUD_MAIL_GENERATOR = 'cloudmail'; +const OUTLOOK_EMAIL_PLUS_PROVIDER = 'outlook-email-plus'; +const OUTLOOK_EMAIL_PLUS_GENERATOR = 'outlook-email-plus'; const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool'; const HOTMAIL_MAILBOXES = ['INBOX', 'Junk']; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; @@ -563,6 +586,7 @@ const HOSTED_CHECKOUT_VERIFICATION_POPUP_DELAY_MIN_SECONDS = 0; const HOSTED_CHECKOUT_VERIFICATION_POPUP_DELAY_MAX_SECONDS = 60; const OUTLOOK_ALIAS_DEFAULT_MAX_PER_ACCOUNT = 5; const OUTLOOK_ALIAS_MAX_PER_ACCOUNT_LIMIT = 50; +const OUTLOOK_EMAIL_PLUS_ALIAS_MAX_PER_MAILBOX_LIMIT = 5; const OUTLOOK_SUBSCRIPTION_USED_KEYWORD = 'ChatGPT Plus Subscription'; const VERIFICATION_RESEND_COUNT_MIN = 0; const VERIFICATION_RESEND_COUNT_MAX = 20; @@ -1117,6 +1141,12 @@ const PERSISTED_SETTING_DEFAULTS = { cloudMailReceiveMailbox: '', cloudMailDomain: '', cloudMailDomains: [], + outlookEmailPlusBaseUrl: DEFAULT_OUTLOOK_EMAIL_PLUS_BASE_URL, + outlookEmailPlusApiKey: '', + outlookEmailPlusProvider: 'outlook', + outlookEmailPlusProjectKey: 'openai', + outlookEmailPlusCallerIdPrefix: 'gujumpgate', + outlookEmailPlusAliasMaxPerMailbox: OUTLOOK_ALIAS_DEFAULT_MAX_PER_ACCOUNT, hotmailAccounts: [], hotmailAliasEnabled: false, outlookAliasMaxPerAccount: OUTLOOK_ALIAS_DEFAULT_MAX_PER_ACCOUNT, @@ -1444,6 +1474,22 @@ function normalizeOutlookAliasMaxPerAccount(value, fallback = OUTLOOK_ALIAS_DEFA return Math.min(OUTLOOK_ALIAS_MAX_PER_ACCOUNT_LIMIT, Math.max(1, Math.floor(numeric))); } +function normalizeOutlookEmailPlusAliasMaxPerMailbox(value, fallback = OUTLOOK_ALIAS_DEFAULT_MAX_PER_ACCOUNT) { + const rawValue = String(value ?? '').trim(); + const fallbackNumber = Number(fallback); + const normalizedFallback = Number.isFinite(fallbackNumber) + ? Math.min(OUTLOOK_EMAIL_PLUS_ALIAS_MAX_PER_MAILBOX_LIMIT, Math.max(1, Math.floor(fallbackNumber))) + : OUTLOOK_ALIAS_DEFAULT_MAX_PER_ACCOUNT; + if (!rawValue) { + return normalizedFallback; + } + const numeric = Number(rawValue); + if (!Number.isFinite(numeric)) { + return normalizedFallback; + } + return Math.min(OUTLOOK_EMAIL_PLUS_ALIAS_MAX_PER_MAILBOX_LIMIT, Math.max(1, Math.floor(numeric))); +} + function normalizeVerificationResendCount(value, fallback) { const rawValue = String(value ?? '').trim(); if (!rawValue) { @@ -2321,6 +2367,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 === OUTLOOK_EMAIL_PLUS_GENERATOR) return OUTLOOK_EMAIL_PLUS_GENERATOR; return 'duck'; } @@ -2543,6 +2590,13 @@ async function markCurrentRegistrationAccountUsed(state = {}, options = {}) { const icloudResult = await finalizeIcloudAliasAfterSuccessfulFlow(latestState); updated = Boolean(icloudResult?.handled) || updated; + const outlookEmailPlusResult = await markCurrentOutlookEmailPlusAliasUsed(latestState, { + logPrefix: reasonPrefix, + level: options.level || 'warn', + result: 'success', + }); + updated = Boolean(outlookEmailPlusResult?.handled) || updated; + if (typeof markCurrentCustomEmailPoolEntryUsed === 'function') { const result = await markCurrentCustomEmailPoolEntryUsed(latestState, { logPrefix: `${reasonPrefix}:自定义邮箱池`, @@ -2622,6 +2676,7 @@ function normalizeMailProvider(value = '') { case LUCKMAIL_PROVIDER: case CLOUDFLARE_TEMP_EMAIL_PROVIDER: case CLOUD_MAIL_PROVIDER: + case OUTLOOK_EMAIL_PLUS_PROVIDER: case '163': case '163-vip': case '126': @@ -2859,6 +2914,44 @@ const { pollCloudMailVerificationCode, resolveCloudMailPollTargetEmail, } = cloudMailProvider; +const outlookEmailPlusProvider = self.MultiPageBackgroundOutlookEmailPlusProvider.createOutlookEmailPlusProvider({ + addLog, + broadcastDataUpdate, + buildOutlookEmailPlusAliasAddress, + buildOutlookEmailPlusHeaders, + buildOutlookEmailPlusPayPalAliasAddress, + deriveOutlookEmailPlusBaseAddress, + generateOutlookEmailPlusTag, + getOutlookEmailPlusPayPalAliasIndex, + isOutlookEmailPlusTaggedAlias, + joinOutlookEmailPlusUrl, + normalizeOutlookEmailPlusAddress, + normalizeOutlookEmailPlusBaseUrl, + normalizeOutlookEmailPlusCallerIdPrefix, + normalizeOutlookEmailPlusClaim, + normalizeOutlookEmailPlusProjectKey, + normalizeOutlookEmailPlusProvider, + normalizeOutlookEmailPlusVerificationCode, + normalizeHotmailAliasUsage, + OUTLOOK_EMAIL_PLUS_GENERATOR, + OUTLOOK_EMAIL_PLUS_PROVIDER, + getState, + persistRegistrationEmailState, + setEmailState, + setPersistentSettings, + setState, + sleepWithStop, + throwIfStopped, + unwrapOutlookEmailPlusResponse, +}); +const { + claimOutlookEmailPlusAddress, + completeOutlookEmailPlusClaim, + getOutlookEmailPlusConfig, + markOutlookEmailPlusAliasUsed, + pollOutlookEmailPlusVerificationCode, + releaseOutlookEmailPlusClaim, +} = outlookEmailPlusProvider; function normalizeSub2ApiGroupNames(value = '') { const source = Array.isArray(value) @@ -3240,6 +3333,9 @@ function normalizePersistentSettingValue(key, value) { if (normalizedMailProvider === CLOUD_MAIL_PROVIDER) { return CLOUD_MAIL_PROVIDER; } + if (normalizedMailProvider === OUTLOOK_EMAIL_PLUS_PROVIDER) { + return OUTLOOK_EMAIL_PLUS_PROVIDER; + } return HOTMAIL_PROVIDER; } case 'mail2925Mode': @@ -3330,6 +3426,21 @@ function normalizePersistentSettingValue(key, value) { return normalizeCloudMailDomain(value); case 'cloudMailDomains': return normalizeCloudMailDomains(value); + case 'outlookEmailPlusBaseUrl': + return normalizeOutlookEmailPlusBaseUrl(value); + case 'outlookEmailPlusApiKey': + return String(value || '').trim(); + case 'outlookEmailPlusProvider': + return normalizeOutlookEmailPlusProvider(value) || PERSISTED_SETTING_DEFAULTS.outlookEmailPlusProvider; + case 'outlookEmailPlusProjectKey': + return normalizeOutlookEmailPlusProjectKey(value) || PERSISTED_SETTING_DEFAULTS.outlookEmailPlusProjectKey; + case 'outlookEmailPlusCallerIdPrefix': + return normalizeOutlookEmailPlusCallerIdPrefix(value) || PERSISTED_SETTING_DEFAULTS.outlookEmailPlusCallerIdPrefix; + case 'outlookEmailPlusAliasMaxPerMailbox': + return normalizeOutlookEmailPlusAliasMaxPerMailbox( + value, + PERSISTED_SETTING_DEFAULTS.outlookEmailPlusAliasMaxPerMailbox + ); case 'hotmailAccounts': return normalizeHotmailAccounts(value); case 'hotmailAliasEnabled': @@ -3597,8 +3708,17 @@ async function initializeSessionStorageAccess() { } } +const STATE_LOG_REDACTED_VALUE = '[redacted]'; +const STATE_LOG_SECRET_KEY_PATTERN = /(api[_-]?key|password|secret|token)/i; + +function stringifyStateUpdatesForLog(updates) { + return JSON.stringify(updates, (key, value) => ( + STATE_LOG_SECRET_KEY_PATTERN.test(key) ? STATE_LOG_REDACTED_VALUE : value + )); +} + async function setState(updates) { - console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200)); + console.log(LOG_PREFIX, 'storage.set:', stringifyStateUpdatesForLog(updates).slice(0, 200)); if (Object.keys(updates || {}).length > 0) { const currentSessionState = await chrome.storage.session.get(null); const sessionUpdates = buildStatePatchWithRuntimeState({ @@ -4760,6 +4880,160 @@ function isLuckmailProvider(stateOrProvider) { return provider === LUCKMAIL_PROVIDER; } +function isOutlookEmailPlusProvider(stateOrProvider) { + const provider = typeof stateOrProvider === 'string' + ? stateOrProvider + : stateOrProvider?.mailProvider; + return provider === OUTLOOK_EMAIL_PLUS_PROVIDER; +} + +function hasCurrentOutlookEmailPlusClaim(state = {}) { + return isOutlookEmailPlusProvider(state) + && Boolean(state.currentOutlookEmailPlusClaim?.address || state.currentOutlookEmailPlusClaim?.accountId); +} + +function getOutlookEmailPlusClaimIdentity(claim = {}) { + return String(claim?.taskId || claim?.accountId || claim?.address || '') + .trim() + .toLowerCase(); +} + +function isSameOutlookEmailPlusClaim(left = {}, right = {}) { + const leftIdentity = getOutlookEmailPlusClaimIdentity(left); + const rightIdentity = getOutlookEmailPlusClaimIdentity(right); + return Boolean(leftIdentity && rightIdentity && leftIdentity === rightIdentity); +} + +async function completeCurrentOutlookEmailPlusClaim(state = {}, options = {}) { + if (!hasCurrentOutlookEmailPlusClaim(state) || typeof completeOutlookEmailPlusClaim !== 'function') { + return { handled: false, reason: 'missing_claim' }; + } + try { + const result = await completeOutlookEmailPlusClaim(state, { + result: options.result || 'success', + }); + if (result?.completed) { + const logPrefix = String(options.logPrefix || '').trim() || 'Outlook Email Plus'; + await addLog(`${logPrefix}:Outlook Email Plus 邮箱认领已完成。`, options.level || 'ok'); + return { handled: true, result }; + } + if (result?.reason === 'missing_claim_token') { + const refreshedState = await getState(); + if (isSameOutlookEmailPlusClaim(state.currentOutlookEmailPlusClaim, refreshedState.currentOutlookEmailPlusClaim)) { + await addLog('Outlook Email Plus:缺少认领令牌,无法通知服务端完成认领,可能是扩展后台已重启。', 'warn'); + } + } + return { handled: false, result }; + } catch (error) { + await addLog(`Outlook Email Plus:完成认领回调失败:${error?.message || error}`, 'warn'); + return { handled: false, error }; + } +} + +async function markCurrentOutlookEmailPlusAliasUsed(state = {}, options = {}) { + if (!hasCurrentOutlookEmailPlusClaim(state) || typeof markOutlookEmailPlusAliasUsed !== 'function') { + return { handled: false, reason: 'missing_claim' }; + } + const logPrefix = String(options.logPrefix || '').trim() || 'Outlook Email Plus'; + try { + const result = await markOutlookEmailPlusAliasUsed(state); + if (!result?.handled) { + return { handled: false, result }; + } + if (!result.alreadyUsed) { + await addLog( + `${logPrefix}:Outlook Email Plus 别名 ${result.registrationEmail || ''} 已标记为已用(${result.aliasIndex}/${result.aliasMax})。`, + options.level || 'warn' + ); + } + if (result.exhausted) { + const completion = await completeCurrentOutlookEmailPlusClaim(state, { + logPrefix, + level: options.level, + result: options.result || 'success', + }); + return { + handled: true, + result, + completion, + exhausted: true, + }; + } + if (!result.alreadyUsed) { + await addLog( + `${logPrefix}:当前 Outlook Email Plus 邮箱还可继续分配后续别名。`, + 'info' + ); + } + return { + handled: true, + result, + exhausted: false, + }; + } catch (error) { + await addLog(`Outlook Email Plus:标记别名已用失败:${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' }; + } + try { + const result = await releaseOutlookEmailPlusClaim(state, { + reason: options.reason || 'flow_abandoned', + }); + if (result?.released) { + return { handled: true, result }; + } + if (result?.reason === 'missing_claim_token') { + const refreshedState = await getState(); + if (isSameOutlookEmailPlusClaim(state.currentOutlookEmailPlusClaim, refreshedState.currentOutlookEmailPlusClaim)) { + await addLog('Outlook Email Plus:缺少认领令牌,无法通知服务端释放认领,可能是扩展后台已重启。', 'warn'); + } + } + return { handled: false, result }; + } catch (error) { + await addLog(`Outlook Email Plus:释放认领回调失败:${error?.message || error}`, 'warn'); + return { handled: false, error }; + } +} + +function getOutlookEmailPlusLifecycleAction(status = '') { + const normalizedStatus = String(status || '').trim().toLowerCase(); + if (normalizedStatus === 'success') { + return 'complete'; + } + if ( + normalizedStatus === 'failed' + || normalizedStatus === 'stopped' + || normalizedStatus.includes(':failed') + || normalizedStatus.includes(':stopped') + || normalizedStatus.endsWith('_failed') + || normalizedStatus.endsWith('_stopped') + ) { + return 'release'; + } + return ''; +} + +async function finalizeOutlookEmailPlusClaimForAccountRunRecord(status, state = {}, reason = '') { + const action = getOutlookEmailPlusLifecycleAction(status); + if (!action || !hasCurrentOutlookEmailPlusClaim(state)) { + return { handled: false }; + } + if (action === 'complete') { + return markCurrentOutlookEmailPlusAliasUsed(state, { + logPrefix: '流程完成', + result: 'success', + }); + } + return releaseCurrentOutlookEmailPlusClaim(state, { + reason: String(reason || status || 'flow_abandoned').trim() || 'flow_abandoned', + }); +} + function isCustomMailProvider(stateOrProvider) { const provider = typeof stateOrProvider === 'string' ? stateOrProvider @@ -9255,6 +9529,7 @@ function getSourceLabel(source) { 'luckmail-api': 'LuckMail(API 购邮)', 'cloudflare-temp-email': 'Cloudflare Temp Email', 'cloudmail': 'Cloud Mail', + 'outlook-email-plus': 'Outlook Email Plus', 'plus-checkout': 'Plus Checkout', 'paypal-flow': 'PayPal 授权页', 'gopay-flow': 'GoPay 授权页', @@ -11216,10 +11491,6 @@ async function failNodeFromBackground(nodeId, errorLike = '未知错误') { } async function appendManualAccountRunRecordIfNeeded(status, stateOverride = null, reason = '') { - if (!accountRunHistoryHelpers?.appendAccountRunRecord) { - return null; - } - const state = stateOverride || await getState(); return appendAndBroadcastAccountRunRecord(status, state, reason); } @@ -11850,6 +12121,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 === OUTLOOK_EMAIL_PLUS_GENERATOR) return 'Outlook Email Plus'; return 'Duck 邮箱'; } const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMail2925SessionManager({ @@ -12027,6 +12299,9 @@ async function fetchDuckEmail(options = {}) { async function fetchGeneratedEmail(state, options = {}) { const currentState = state || await getState(); const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator); + if (generator === OUTLOOK_EMAIL_PLUS_GENERATOR) { + return claimOutlookEmailPlusAddress(currentState, options); + } if (generator === CLOUD_MAIL_GENERATOR) { return fetchCloudMailAddress(currentState, options); } @@ -12106,13 +12381,13 @@ async function broadcastAccountRunHistoryUpdate() { } async function appendAndBroadcastAccountRunRecord(status, stateOverride = null, reason = '') { - if (!accountRunHistoryHelpers?.appendAccountRunRecord) { - return null; - } - const state = stateOverride || await getState(); const resolvedStatus = resolveAccountRunRecordStatusForStop(status, state); const resolvedReason = resolveAccountRunRecordReasonForStop(resolvedStatus, reason); + await finalizeOutlookEmailPlusClaimForAccountRunRecord(resolvedStatus, state, resolvedReason); + if (!accountRunHistoryHelpers?.appendAccountRunRecord) { + return null; + } const record = await accountRunHistoryHelpers.appendAccountRunRecord(resolvedStatus, state, resolvedReason); if (!record) { return null; @@ -13520,6 +13795,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create closeConflictingTabsForSource, CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUD_MAIL_PROVIDER, + OUTLOOK_EMAIL_PLUS_PROVIDER, completeNodeFromBackground, confirmCustomVerificationStepBypassRequest: (step) => chrome.runtime.sendMessage({ type: 'REQUEST_CUSTOM_VERIFICATION_BYPASS_CONFIRMATION', @@ -13540,6 +13816,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create MAIL_2925_VERIFICATION_MAX_ATTEMPTS, pollCloudflareTempEmailVerificationCode, pollCloudMailVerificationCode, + pollOutlookEmailPlusVerificationCode, pollHotmailVerificationCode, pollLuckmailVerificationCode, sendToContentScript, @@ -13671,6 +13948,7 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({ LUCKMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUD_MAIL_PROVIDER, + OUTLOOK_EMAIL_PLUS_PROVIDER, resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep, reuseOrCreateTab, sendToContentScript, @@ -13729,6 +14007,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ chrome, CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUD_MAIL_PROVIDER, + OUTLOOK_EMAIL_PLUS_PROVIDER, completeNodeFromBackground, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, ensureMail2925MailboxSession, @@ -14343,8 +14622,11 @@ function getMailConfig(state) { if (provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { return { provider: CLOUDFLARE_TEMP_EMAIL_PROVIDER, label: 'Cloudflare Temp Email' }; } - if (provider === 'cloudmail') { - return { provider: 'cloudmail', label: 'Cloud Mail' }; + if (provider === CLOUD_MAIL_PROVIDER) { + return { provider: CLOUD_MAIL_PROVIDER, label: 'Cloud Mail' }; + } + if (provider === OUTLOOK_EMAIL_PLUS_PROVIDER) { + return { provider: OUTLOOK_EMAIL_PLUS_PROVIDER, label: 'Outlook Email Plus' }; } if (provider === '163') { return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 邮箱' }; diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index 0bbb856..be8d71d 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -584,6 +584,13 @@ emailPrefix: prevState.emailPrefix, outlookAliasMaxPerAccount: prevState.outlookAliasMaxPerAccount, hotmailAliasUsage: prevState.hotmailAliasUsage, + outlookEmailPlusBaseUrl: prevState.outlookEmailPlusBaseUrl, + outlookEmailPlusApiKey: prevState.outlookEmailPlusApiKey, + outlookEmailPlusProvider: prevState.outlookEmailPlusProvider, + outlookEmailPlusProjectKey: prevState.outlookEmailPlusProjectKey, + outlookEmailPlusCallerIdPrefix: prevState.outlookEmailPlusCallerIdPrefix, + outlookEmailPlusAliasMaxPerMailbox: prevState.outlookEmailPlusAliasMaxPerMailbox, + currentOutlookEmailPlusClaim: prevState.currentOutlookEmailPlusClaim, inbucketHost: prevState.inbucketHost, inbucketMailbox: prevState.inbucketMailbox, cloudflareDomain: prevState.cloudflareDomain, diff --git a/background/logging-status.js b/background/logging-status.js index e0c41cc..80ddaea 100644 --- a/background/logging-status.js +++ b/background/logging-status.js @@ -35,6 +35,7 @@ 'luckmail-api': 'LuckMail(API 购邮)', 'cloudflare-temp-email': 'Cloudflare Temp Email', 'cloudmail': 'Cloud Mail', + 'outlook-email-plus': 'Outlook Email Plus', 'plus-checkout': 'Plus Checkout', 'paypal-flow': 'PayPal 授权页', 'gopay-flow': 'GoPay 授权页', diff --git a/background/outlook-email-plus-provider.js b/background/outlook-email-plus-provider.js new file mode 100644 index 0000000..f6d7dc2 --- /dev/null +++ b/background/outlook-email-plus-provider.js @@ -0,0 +1,882 @@ +(function outlookEmailPlusProviderModule(root, factory) { + root.MultiPageBackgroundOutlookEmailPlusProvider = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createOutlookEmailPlusProviderModule() { + function createOutlookEmailPlusProvider(deps = {}) { + const { + addLog = async () => {}, + buildOutlookEmailPlusAliasAddress, + buildOutlookEmailPlusHeaders, + buildOutlookEmailPlusPayPalAliasAddress, + deriveOutlookEmailPlusBaseAddress, + generateOutlookEmailPlusTag, + getOutlookEmailPlusPayPalAliasIndex, + isOutlookEmailPlusTaggedAlias, + joinOutlookEmailPlusUrl, + normalizeOutlookEmailPlusAddress, + normalizeOutlookEmailPlusBaseUrl, + normalizeOutlookEmailPlusCallerIdPrefix, + normalizeOutlookEmailPlusClaim, + normalizeOutlookEmailPlusProjectKey, + normalizeOutlookEmailPlusProvider, + normalizeOutlookEmailPlusVerificationCode, + OUTLOOK_EMAIL_PLUS_GENERATOR = 'outlook-email-plus', + OUTLOOK_EMAIL_PLUS_PROVIDER = 'outlook-email-plus', + broadcastDataUpdate = null, + fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getState = async () => ({}), + normalizeHotmailAliasUsage: normalizeHotmailAliasUsageDep = null, + persistRegistrationEmailState = null, + setEmailState = async () => {}, + setPersistentSettings = null, + setState = async () => {}, + sleepWithStop = async () => {}, + throwIfStopped = () => {}, + unwrapOutlookEmailPlusResponse, + } = deps; + const activeClaims = new Map(); + const DEFAULT_ALIAS_MAX_PER_MAILBOX = 5; + + async function persistResolvedEmailState(state = null, email, options = {}) { + if (typeof persistRegistrationEmailState === 'function') { + await persistRegistrationEmailState(state, email, options); + return; + } + await setEmailState(email, options); + } + + function getOutlookEmailPlusConfig(state = {}) { + return { + baseUrl: normalizeOutlookEmailPlusBaseUrl(state.outlookEmailPlusBaseUrl), + apiKey: String(state.outlookEmailPlusApiKey || '').trim(), + provider: normalizeOutlookEmailPlusProvider(state.outlookEmailPlusProvider) || 'outlook', + projectKey: normalizeOutlookEmailPlusProjectKey(state.outlookEmailPlusProjectKey) || 'openai', + callerIdPrefix: normalizeOutlookEmailPlusCallerIdPrefix(state.outlookEmailPlusCallerIdPrefix) || 'gujumpgate', + }; + } + + function ensureOutlookEmailPlusConfig(state, options = {}) { + const { requireApiKey = true } = options; + const config = getOutlookEmailPlusConfig(state); + if (!config.baseUrl) { + throw new Error('Outlook Email Plus 服务地址为空或格式无效。'); + } + if (requireApiKey && !config.apiKey) { + throw new Error('Outlook Email Plus API Key 为空。'); + } + if (!config.provider) { + throw new Error('Outlook Email Plus 邮箱提供商为空。'); + } + if (!config.projectKey) { + throw new Error('Outlook Email Plus Project Key 为空。'); + } + return config; + } + + async function requestOutlookEmailPlusJson(config, path, options = {}) { + if (!fetchImpl) { + throw new Error('Outlook Email Plus 当前运行环境不支持 fetch。'); + } + + const { + method = 'GET', + payload, + searchParams = null, + timeoutMs = 20000, + } = options; + const url = new URL(joinOutlookEmailPlusUrl(config.baseUrl, path)); + 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 controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs); + let response; + try { + response = await fetchImpl(url.toString(), { + method, + headers: buildOutlookEmailPlusHeaders(config, { + json: payload !== undefined, + }), + body: payload !== undefined ? JSON.stringify(payload) : undefined, + signal: controller.signal, + }); + } catch (err) { + const errorMessage = err?.name === 'AbortError' + ? `Outlook Email Plus 请求超时(>${Math.round(timeoutMs / 1000)} 秒)` + : `Outlook Email Plus 请求失败:${err?.message || err}`; + throw new Error(errorMessage); + } finally { + clearTimeout(timeoutId); + } + + const text = await response.text(); + let parsed; + try { + parsed = text ? JSON.parse(text) : {}; + } catch (_error) { + parsed = text; + } + + if (!response.ok) { + let payloadMessage = ''; + try { + payloadMessage = String(unwrapOutlookEmailPlusResponse(parsed)?.message || ''); + } catch (error) { + payloadMessage = String(error?.message || ''); + } + if (!payloadMessage && parsed && typeof parsed === 'object') { + payloadMessage = String(parsed.message || parsed.error || parsed.msg || ''); + } + throw new Error(`Outlook Email Plus 请求失败:${payloadMessage || text || `HTTP ${response.status}`}`); + } + + return unwrapOutlookEmailPlusResponse(parsed); + } + + 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 normalizeUsageKey(value = '') { + return String(value || '').trim().toLowerCase(); + } + + function normalizeAliasUsageEntry(value = {}) { + const email = String(value.email || '').trim(); + if (!email) return null; + return { + email, + used: Boolean(value.used), + lastCheckedAt: Math.max(0, Math.floor(Number(value.lastCheckedAt) || 0)), + reason: String(value.reason || '').trim(), + }; + } + + function normalizeOutlookEmailPlusAliasUsage(value = {}) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return {}; + } + const usage = {}; + for (const [rawKey, rawBucket] of Object.entries(value)) { + const usageKey = normalizeUsageKey(rawKey); + if (!usageKey || !rawBucket || typeof rawBucket !== 'object' || Array.isArray(rawBucket)) { + continue; + } + const rawAliases = rawBucket.aliases && typeof rawBucket.aliases === 'object' && !Array.isArray(rawBucket.aliases) + ? rawBucket.aliases + : {}; + const aliases = {}; + for (const [rawEmail, rawEntry] of Object.entries(rawAliases)) { + const entry = normalizeAliasUsageEntry({ + ...(rawEntry && typeof rawEntry === 'object' && !Array.isArray(rawEntry) ? rawEntry : {}), + email: rawEntry?.email || rawEmail, + }); + if (entry) aliases[normalizeOutlookEmailPlusAddress(entry.email)] = entry; + } + usage[usageKey] = { + aliases, + updatedAt: Math.max(0, Math.floor(Number(rawBucket.updatedAt) || 0)), + }; + } + return usage; + } + + function normalizeAliasUsage(value = {}) { + if (typeof normalizeHotmailAliasUsageDep === 'function') { + return normalizeHotmailAliasUsageDep(value); + } + return normalizeOutlookEmailPlusAliasUsage(value); + } + + function resolveClaimTaskId(state = {}, options = {}) { + return normalizeIdentifierPart( + options.taskId + || state.currentOutlookEmailPlusClaim?.taskId + || state.taskId + || state.activeRunId + || state.runId + ) || buildRandomIdentifier(); + } + + function buildCallerId(config, state = {}, options = {}, taskId = '') { + const prefix = normalizeOutlookEmailPlusCallerIdPrefix( + options.callerIdPrefix || config.callerIdPrefix || state.outlookEmailPlusCallerIdPrefix || 'gujumpgate' + ) || 'gujumpgate'; + const explicitCallerId = normalizeIdentifierPart(options.callerId || state.currentOutlookEmailPlusClaim?.callerId); + if (explicitCallerId) { + return explicitCallerId; + } + const suffix = normalizeIdentifierPart(options.runId || state.runId || taskId) || normalizeIdentifierPart(buildRandomIdentifier()); + return `${prefix}-${suffix}`; + } + + function getClaimKeys(claim = {}) { + return [claim.taskId, claim.accountId, claim.address] + .map((value) => String(value || '').trim().toLowerCase()) + .filter(Boolean); + } + + function rememberClaim(claim = {}, storedClaim = {}) { + const secretClaim = { + ...storedClaim, + claimToken: claim.claimToken || '', + }; + for (const key of getClaimKeys(storedClaim)) { + activeClaims.set(key, secretClaim); + } + } + + function forgetClaim(claim = {}) { + for (const key of getClaimKeys(claim)) { + activeClaims.delete(key); + } + } + + function getRememberedClaim(storedClaim = {}) { + for (const key of getClaimKeys(storedClaim)) { + const remembered = activeClaims.get(key); + if (remembered) return remembered; + } + return null; + } + + function buildOutlookEmailPlusAliasUsageKey(claim = {}) { + const explicitKey = normalizeUsageKey(claim.usageKey); + if (explicitKey) return explicitKey; + const accountId = normalizeIdentifierPart(claim.accountId); + if (accountId) return `outlook-email-plus:${accountId}`; + const baseAddress = normalizeOutlookEmailPlusAddress(claim.baseAddress) + || deriveOutlookEmailPlusBaseAddress(claim.address) + || normalizeOutlookEmailPlusAddress(claim.address); + return baseAddress ? `outlook-email-plus:${baseAddress}` : ''; + } + + function getAliasEntriesForClaimFromUsage(usage = {}, claim = {}) { + const usageKey = buildOutlookEmailPlusAliasUsageKey(claim); + if (!usageKey) return []; + return Object.values(usage[usageKey]?.aliases || {}); + } + + function getAliasEntryIndex(entry = {}, baseAddress = '') { + return getPayPalAliasIndex(entry.email, baseAddress) || Number.MAX_SAFE_INTEGER; + } + + function sortAliasEntriesByIndex(entries = [], baseAddress = '') { + return [...entries].sort((left, right) => { + const leftIndex = getAliasEntryIndex(left, baseAddress); + const rightIndex = getAliasEntryIndex(right, baseAddress); + if (leftIndex !== rightIndex) return leftIndex - rightIndex; + return String(left.email || '').localeCompare(String(right.email || '')); + }); + } + + function countUsedAliasEntries(entries = []) { + return entries.reduce((count, entry) => count + (entry?.used ? 1 : 0), 0); + } + + async function persistOutlookEmailPlusAliasUsage(nextUsage = {}) { + if (typeof setPersistentSettings === 'function') { + await setPersistentSettings({ hotmailAliasUsage: nextUsage }); + } + if (typeof setState === 'function') { + await setState({ hotmailAliasUsage: nextUsage }); + } + if (typeof broadcastDataUpdate === 'function') { + broadcastDataUpdate({ hotmailAliasUsage: nextUsage }); + } + } + + async function setOutlookEmailPlusAliasUsageEntry(state = {}, claim = {}, aliasEmail = '', updates = {}) { + const usageKey = buildOutlookEmailPlusAliasUsageKey(claim); + const emailKey = normalizeOutlookEmailPlusAddress(aliasEmail); + if (!usageKey || !emailKey) { + return { entry: null, usage: normalizeAliasUsage(state.hotmailAliasUsage) }; + } + const usage = normalizeAliasUsage(state.hotmailAliasUsage); + const bucket = usage[usageKey] || { aliases: {}, updatedAt: 0 }; + const aliases = { ...(bucket.aliases || {}) }; + const previous = aliases[emailKey] || {}; + const now = Date.now(); + const entry = { + email: String(aliasEmail || previous.email || emailKey).trim(), + used: updates.used === undefined ? Boolean(previous.used) : Boolean(updates.used), + lastCheckedAt: Math.max(0, Math.floor(Number(updates.lastCheckedAt) || now)), + reason: String(updates.reason || previous.reason || '').trim(), + }; + aliases[emailKey] = entry; + const nextUsage = { + ...usage, + [usageKey]: { + ...bucket, + aliases, + updatedAt: now, + }, + }; + await persistOutlookEmailPlusAliasUsage(nextUsage); + return { entry, usage: nextUsage }; + } + + async function ensureOutlookEmailPlusClaimAliasUsage(state = {}, storedClaim = {}) { + const usageKey = buildOutlookEmailPlusAliasUsageKey(storedClaim); + const baseAddress = normalizeOutlookEmailPlusAddress(storedClaim.baseAddress) + || deriveOutlookEmailPlusBaseAddress(storedClaim.address) + || normalizeOutlookEmailPlusAddress(storedClaim.address); + const aliasMax = getAliasMaxForState(state, storedClaim.aliasMax); + const aliasIndex = Math.min(aliasMax, Math.max( + getPayPalAliasIndex(storedClaim.registrationEmail, baseAddress) || 0, + Math.floor(Number(storedClaim.aliasIndex) || 0) + )); + if (!usageKey || !baseAddress || aliasIndex <= 0) { + return normalizeAliasUsage(state.hotmailAliasUsage); + } + + const usage = normalizeAliasUsage(state.hotmailAliasUsage); + const bucket = usage[usageKey] || { aliases: {}, updatedAt: 0 }; + const aliases = { ...(bucket.aliases || {}) }; + let changed = false; + const now = Date.now(); + for (let index = 1; index <= aliasIndex; index += 1) { + const aliasEmail = allocateRegistrationAlias(baseAddress, index); + const email = normalizeOutlookEmailPlusAddress(aliasEmail); + const shouldBeUsed = index < aliasIndex || Boolean(storedClaim.aliasUsed); + const existing = aliases[email]; + if (!existing) { + aliases[email] = { + email: aliasEmail, + used: shouldBeUsed, + lastCheckedAt: now, + reason: shouldBeUsed ? 'legacy_used' : 'allocated', + }; + changed = true; + } else if (shouldBeUsed && !existing.used) { + aliases[email] = { + ...existing, + used: true, + lastCheckedAt: now, + reason: existing.reason || 'legacy_used', + }; + changed = true; + } + } + if (!changed) return usage; + + const nextUsage = { + ...usage, + [usageKey]: { + ...bucket, + aliases, + updatedAt: now, + }, + }; + await persistOutlookEmailPlusAliasUsage(nextUsage); + return nextUsage; + } + + function buildStoredOutlookEmailPlusClaim(claim = {}, context = {}) { + return { + accountId: claim.accountId || '', + address: claim.address || '', + baseAddress: claim.baseAddress || '', + registrationEmail: claim.registrationEmail || claim.address || '', + isAliasClaim: Boolean(claim.isAliasClaim), + domain: claim.domain || '', + claimedAt: claim.claimedAt || '', + leaseExpiresAt: claim.leaseExpiresAt || '', + callerId: context.callerId || '', + taskId: context.taskId || '', + projectKey: context.projectKey || '', + provider: context.provider || '', + aliasIndex: Math.max(0, Math.floor(Number(claim.aliasIndex) || 0)), + aliasMax: normalizeAliasMax(claim.aliasMax || context.aliasMax), + aliasUsed: Boolean(claim.aliasUsed), + usageKey: buildOutlookEmailPlusAliasUsageKey({ ...claim, usageKey: claim.usageKey || context.usageKey }), + claimToken: claim.claimToken || context.claimToken || '', + }; + } + + function buildLifecyclePayload(claim = {}, options = {}) { + const payload = { + account_id: claim.accountId || undefined, + email: claim.address || undefined, + claim_token: claim.claimToken || undefined, + caller_id: claim.callerId || undefined, + task_id: claim.taskId || undefined, + project_key: claim.projectKey || undefined, + result: options.result || undefined, + reason: options.reason || undefined, + }; + for (const [key, value] of Object.entries(payload)) { + if (value === undefined || value === null || value === '') { + delete payload[key]; + } + } + return payload; + } + + async function clearStoredClaim(storedClaim = {}) { + forgetClaim(storedClaim); + if (typeof setState === 'function') { + await setState({ currentOutlookEmailPlusClaim: null }); + } + } + + function resolveLifecycleClaim(state = {}, options = {}) { + const storedClaim = options.claim && typeof options.claim === 'object' + ? options.claim + : (state.currentOutlookEmailPlusClaim || {}); + if (!storedClaim || typeof storedClaim !== 'object') { + return null; + } + const rememberedClaim = getRememberedClaim(storedClaim) || {}; + return { + ...rememberedClaim, + ...storedClaim, + claimToken: storedClaim.claimToken || rememberedClaim.claimToken || '', + }; + } + + function normalizeAliasMax(value, fallback = DEFAULT_ALIAS_MAX_PER_MAILBOX) { + const numeric = Math.floor(Number(value)); + if (Number.isFinite(numeric)) { + return Math.min(DEFAULT_ALIAS_MAX_PER_MAILBOX, Math.max(1, numeric)); + } + const fallbackNumber = Math.floor(Number(fallback)); + if (Number.isFinite(fallbackNumber)) { + return Math.min(DEFAULT_ALIAS_MAX_PER_MAILBOX, Math.max(1, fallbackNumber)); + } + return DEFAULT_ALIAS_MAX_PER_MAILBOX; + } + + function getAliasMaxForState(state = {}) { + return normalizeAliasMax(state.outlookEmailPlusAliasMaxPerMailbox, DEFAULT_ALIAS_MAX_PER_MAILBOX); + } + + function buildPayPalAlias(baseAddress = '', index = 1) { + if (typeof buildOutlookEmailPlusPayPalAliasAddress === 'function') { + return buildOutlookEmailPlusPayPalAliasAddress(baseAddress, index); + } + const numericIndex = Math.max(1, Math.floor(Number(index) || 1)); + return buildOutlookEmailPlusAliasAddress(baseAddress, `PayPal${numericIndex}`); + } + + function getPayPalAliasIndex(aliasAddress = '', baseAddress = '') { + if (typeof getOutlookEmailPlusPayPalAliasIndex === 'function') { + return getOutlookEmailPlusPayPalAliasIndex(aliasAddress, baseAddress); + } + return null; + } + + function allocateRegistrationAlias(baseAddress = '', index = 1) { + const registrationEmail = buildPayPalAlias(baseAddress, index); + if (!registrationEmail) { + throw new Error('Outlook Email Plus 无法基于认领邮箱生成注册别名。'); + } + return registrationEmail; + } + + function resolveClaimRegistration(claim = {}, context = {}) { + const claimedAddress = normalizeOutlookEmailPlusAddress(claim.address); + const baseAddress = deriveOutlookEmailPlusBaseAddress(claimedAddress) || claimedAddress; + const isAliasClaim = Boolean(isOutlookEmailPlusTaggedAlias(claimedAddress)); + const aliasMax = normalizeAliasMax(context.aliasMax); + const aliasIndex = Math.max(1, Math.floor(Number(context.aliasIndex) || 1)); + const registrationEmail = allocateRegistrationAlias(baseAddress, aliasIndex); + + return { + ...claim, + address: claimedAddress, + baseAddress, + registrationEmail, + isAliasClaim, + aliasIndex, + aliasMax, + aliasUsed: false, + }; + } + + function getReusableStoredClaim(state = {}) { + const storedClaim = state.currentOutlookEmailPlusClaim; + if (!storedClaim || typeof storedClaim !== 'object') { + return null; + } + const rememberedClaim = getRememberedClaim(storedClaim); + const address = normalizeOutlookEmailPlusAddress(storedClaim.address); + const baseAddress = normalizeOutlookEmailPlusAddress(storedClaim.baseAddress) + || deriveOutlookEmailPlusBaseAddress(address) + || address; + if (!address || !baseAddress) { + return null; + } + return { + ...storedClaim, + address, + baseAddress, + aliasIndex: Math.max(0, Math.floor(Number(storedClaim.aliasIndex) || 0)), + aliasMax: getAliasMaxForState(state, storedClaim.aliasMax), + aliasUsed: Boolean(storedClaim.aliasUsed), + usageKey: buildOutlookEmailPlusAliasUsageKey({ ...storedClaim, address, baseAddress }), + claimToken: storedClaim.claimToken || rememberedClaim?.claimToken || '', + }; + } + + function buildReusableRegistrationClaim(storedClaim = {}, context = {}) { + const state = context.state || {}; + const aliasMax = getAliasMaxForState(state, storedClaim.aliasMax); + const usage = normalizeAliasUsage(context.hotmailAliasUsage || state.hotmailAliasUsage); + const usageKey = buildOutlookEmailPlusAliasUsageKey(storedClaim); + const entries = getAliasEntriesForClaimFromUsage(usage, { ...storedClaim, usageKey }); + if (countUsedAliasEntries(entries) >= aliasMax) { + return { + ...storedClaim, + usageKey, + aliasMax, + exhausted: true, + }; + } + + const unusedEntry = sortAliasEntriesByIndex(entries, storedClaim.baseAddress) + .find((entry) => !entry.used); + if (unusedEntry) { + const aliasIndex = getPayPalAliasIndex(unusedEntry.email, storedClaim.baseAddress) + || Math.max(1, Math.floor(Number(storedClaim.aliasIndex) || 1)); + return { + ...storedClaim, + registrationEmail: unusedEntry.email, + aliasIndex, + aliasMax, + aliasUsed: false, + usageKey, + }; + } + + const knownAliases = new Set(entries.map((entry) => normalizeOutlookEmailPlusAddress(entry.email))); + for (let index = 1; index <= aliasMax; index += 1) { + const registrationEmail = allocateRegistrationAlias(storedClaim.baseAddress, index); + if (!knownAliases.has(normalizeOutlookEmailPlusAddress(registrationEmail))) { + return { + ...storedClaim, + aliasIndex: index, + aliasMax, + aliasUsed: false, + registrationEmail, + usageKey, + }; + } + } + + return { + ...storedClaim, + aliasMax, + usageKey, + exhausted: true, + }; + } + + async function reuseOutlookEmailPlusClaimAddress(state = {}, config = {}, options = {}) { + const storedClaim = getReusableStoredClaim(state); + if (!storedClaim) { + return { reused: false }; + } + const hotmailAliasUsage = await ensureOutlookEmailPlusClaimAliasUsage(state, storedClaim); + const claim = buildReusableRegistrationClaim(storedClaim, { + state, + hotmailAliasUsage, + }); + if (claim.exhausted) { + return { reused: false, exhausted: true, claim }; + } + await persistResolvedEmailState(state, claim.registrationEmail, { + source: `generated:${OUTLOOK_EMAIL_PLUS_GENERATOR}`, + preserveAccountIdentity: Boolean(options?.preserveAccountIdentity), + }); + const nextStoredClaim = buildStoredOutlookEmailPlusClaim(claim, { + callerId: storedClaim.callerId, + taskId: storedClaim.taskId, + projectKey: storedClaim.projectKey || config.projectKey, + provider: storedClaim.provider || config.provider, + usageKey: storedClaim.usageKey, + }); + if (typeof setState === 'function') { + await setState({ currentOutlookEmailPlusClaim: nextStoredClaim }); + } + await setOutlookEmailPlusAliasUsageEntry( + { ...state, hotmailAliasUsage }, + nextStoredClaim, + claim.registrationEmail, + { used: false, reason: 'allocated' } + ); + await addLog(`Outlook Email Plus:复用 ${claim.address},本次注册使用 ${claim.registrationEmail}`, 'ok'); + return { reused: true, address: claim.registrationEmail }; + } + + async function claimOutlookEmailPlusAddress(state, options = {}) { + throwIfStopped(); + let latestState = state || await getState(); + const config = ensureOutlookEmailPlusConfig(latestState); + const reuseResult = await reuseOutlookEmailPlusClaimAddress(latestState, config, options); + if (reuseResult.reused) { + return reuseResult.address; + } + if (reuseResult.exhausted) { + await completeOutlookEmailPlusClaim(latestState, { result: 'success' }); + latestState = await getState(); + } + const taskId = resolveClaimTaskId(latestState, options); + const callerId = buildCallerId(config, latestState, options, taskId); + const aliasMax = getAliasMaxForState(latestState); + const payload = { + provider: config.provider, + project_key: config.projectKey, + caller_id: callerId, + task_id: taskId, + }; + + const result = await requestOutlookEmailPlusJson(config, '/api/external/pool/claim-random', { + method: 'POST', + payload, + }); + const claim = resolveClaimRegistration(normalizeOutlookEmailPlusClaim(result), { + aliasTag: options.aliasTag, + aliasIndex: 1, + aliasMax, + callerId, + projectKey: config.projectKey, + taskId, + }); + if (!claim.address) { + throw new Error('Outlook Email Plus 未返回可用邮箱地址。'); + } + + await persistResolvedEmailState(latestState, claim.registrationEmail, { + source: `generated:${OUTLOOK_EMAIL_PLUS_GENERATOR}`, + preserveAccountIdentity: Boolean(options?.preserveAccountIdentity), + }); + const storedClaim = buildStoredOutlookEmailPlusClaim(claim, { + callerId, + taskId, + projectKey: config.projectKey, + provider: config.provider, + }); + rememberClaim(claim, storedClaim); + if (typeof setState === 'function') { + await setState({ currentOutlookEmailPlusClaim: storedClaim }); + } + await setOutlookEmailPlusAliasUsageEntry(latestState, storedClaim, claim.registrationEmail, { + used: false, + reason: 'allocated', + }); + await addLog(`Outlook Email Plus:已认领 ${claim.address},注册使用 ${claim.registrationEmail}`, 'ok'); + return claim.registrationEmail; + } + + async function markOutlookEmailPlusAliasUsed(state = {}) { + const latestState = state || await getState(); + const storedClaim = getReusableStoredClaim(latestState) || latestState.currentOutlookEmailPlusClaim; + if (!storedClaim || typeof storedClaim !== 'object') { + return { handled: false, reason: 'missing_claim' }; + } + const aliasMax = getAliasMaxForState(latestState, storedClaim.aliasMax); + const aliasIndex = Math.max( + getPayPalAliasIndex(storedClaim.registrationEmail, storedClaim.baseAddress) || 0, + Math.floor(Number(storedClaim.aliasIndex) || 0) + ); + const normalizedAliasIndex = Math.max(1, aliasIndex || 1); + const hotmailAliasUsage = await ensureOutlookEmailPlusClaimAliasUsage(latestState, storedClaim); + const usageKey = buildOutlookEmailPlusAliasUsageKey(storedClaim); + const usageBefore = normalizeAliasUsage(hotmailAliasUsage); + const registrationEmail = String(storedClaim.registrationEmail || storedClaim.address || '').trim(); + const registrationEmailKey = normalizeOutlookEmailPlusAddress(registrationEmail); + const existingEntry = usageBefore[usageKey]?.aliases?.[registrationEmailKey] || null; + const alreadyUsed = Boolean(existingEntry?.used || storedClaim.aliasUsed); + const usageResult = await setOutlookEmailPlusAliasUsageEntry( + { ...latestState, hotmailAliasUsage }, + { ...storedClaim, usageKey }, + registrationEmail, + { used: true, reason: 'flow_completed' } + ); + const usedAliasCount = countUsedAliasEntries( + getAliasEntriesForClaimFromUsage(usageResult.usage, { ...storedClaim, usageKey }) + ); + const nextStoredClaim = { + ...storedClaim, + aliasIndex: normalizedAliasIndex, + aliasMax, + aliasUsed: true, + usageKey, + }; + if (typeof setState === 'function') { + await setState({ currentOutlookEmailPlusClaim: nextStoredClaim }); + } + return { + handled: true, + alreadyUsed, + exhausted: usedAliasCount >= aliasMax, + aliasIndex: normalizedAliasIndex, + aliasMax, + registrationEmail, + }; + } + + function resolvePollTargetEmail(state = {}, pollPayload = {}) { + return normalizeOutlookEmailPlusAddress( + pollPayload.targetEmail + || state.registrationEmailState?.current + || state.email + || state.currentOutlookEmailPlusClaim?.registrationEmail + || state.currentOutlookEmailPlusClaim?.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; + } + const ageMs = Math.max(0, Date.now() - afterTimestamp); + return Math.max(1, Math.ceil(ageMs / 60000)); + } + + async function pollOutlookEmailPlusVerificationCode(step, state, pollPayload = {}) { + const latestState = state || await getState(); + const config = ensureOutlookEmailPlusConfig(latestState); + const targetEmail = resolvePollTargetEmail(latestState, pollPayload); + if (!targetEmail) { + throw new Error('Outlook Email Plus 轮询前缺少目标邮箱地址,请先获取注册邮箱。'); + } + + const maxAttempts = Math.max(1, Math.floor(Number(pollPayload.maxAttempts) || 5)); + const intervalMs = Math.max(0, Number(pollPayload.intervalMs) || 3000); + const excludeCodes = new Set( + (Array.isArray(pollPayload.excludeCodes) ? pollPayload.excludeCodes : []) + .map((value) => normalizeOutlookEmailPlusVerificationCode(value)) + .filter((value) => typeof value === 'string' && value) + ); + const sinceMinutes = resolveSinceMinutes(pollPayload); + const codeLength = Math.max(0, Math.floor(Number(pollPayload.codeLength) || 0)); + const codeRegex = String(pollPayload.codeRegex || '').trim(); + const codeSource = String(pollPayload.codeSource || '').trim(); + let lastError = null; + let sawNoCode = false; + + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + throwIfStopped(); + try { + const result = await requestOutlookEmailPlusJson(config, '/api/external/verification-code', { + method: 'GET', + searchParams: { + email: targetEmail, + since_minutes: sinceMinutes > 0 ? sinceMinutes : undefined, + code_length: codeLength > 0 ? codeLength : undefined, + code_regex: codeRegex || undefined, + code_source: codeSource || undefined, + }, + }); + const verification = normalizeOutlookEmailPlusVerificationCode(result); + if (verification.code) { + if (!excludeCodes.has(verification.code)) { + return { + ok: true, + code: verification.code, + emailTimestamp: verification.emailTimestamp || Date.now(), + mailId: verification.mailId || '', + }; + } + sawNoCode = true; + lastError = new Error(`步骤 ${step}:Outlook Email Plus 返回了已排除的旧验证码。`); + await addLog(`步骤 ${step}:Outlook Email Plus 命中过滤掉的旧验证码,继续轮询(${attempt}/${maxAttempts})。`, 'info'); + } else { + sawNoCode = true; + lastError = new Error(`步骤 ${step}:暂未在 Outlook Email Plus 中找到匹配验证码(${attempt}/${maxAttempts})。`); + await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info'); + } + } catch (err) { + lastError = err; + await addLog(`步骤 ${step}:Outlook Email Plus 轮询失败:${err?.message || err}`, 'warn'); + } + + if (attempt < maxAttempts) { + await sleepWithStop(intervalMs); + } + } + + if (sawNoCode) { + await completeOutlookEmailPlusClaim(latestState, { result: 'verification_timeout' }); + throw new Error(`步骤 ${step}:未在 Outlook Email Plus 中找到新的匹配验证码。`); + } + throw lastError || new Error(`步骤 ${step}:Outlook Email Plus 轮询失败。`); + } + + async function completeOutlookEmailPlusClaim(state, options = {}) { + const latestState = state || await getState(); + const config = ensureOutlookEmailPlusConfig(latestState); + const claim = resolveLifecycleClaim(latestState, options); + if (!claim?.address && !claim?.accountId) { + return { completed: false, reason: 'missing_claim' }; + } + if (!claim.claimToken) { + return { completed: false, reason: 'missing_claim_token' }; + } + await requestOutlookEmailPlusJson(config, '/api/external/pool/claim-complete', { + method: 'POST', + payload: buildLifecyclePayload(claim, { result: options.result || 'success' }), + }); + await clearStoredClaim(claim); + await addLog(`Outlook Email Plus:已完成认领 ${claim.address || claim.accountId}`, 'ok'); + return { completed: true }; + } + + async function releaseOutlookEmailPlusClaim(state, options = {}) { + const latestState = state || await getState(); + const config = ensureOutlookEmailPlusConfig(latestState); + const claim = resolveLifecycleClaim(latestState, options); + if (!claim?.address && !claim?.accountId) { + return { released: false, reason: 'missing_claim' }; + } + if (!claim.claimToken) { + return { released: false, reason: 'missing_claim_token' }; + } + await requestOutlookEmailPlusJson(config, '/api/external/pool/claim-release', { + method: 'POST', + payload: buildLifecyclePayload(claim, { reason: options.reason || 'flow_abandoned' }), + }); + await clearStoredClaim(claim); + await addLog(`Outlook Email Plus:已释放认领 ${claim.address || claim.accountId}`, 'warn'); + return { released: true }; + } + + return { + claimOutlookEmailPlusAddress, + completeOutlookEmailPlusClaim, + ensureOutlookEmailPlusConfig, + getOutlookEmailPlusConfig, + markOutlookEmailPlusAliasUsed, + pollOutlookEmailPlusVerificationCode, + releaseOutlookEmailPlusClaim, + requestOutlookEmailPlusJson, + }; + } + + return { + createOutlookEmailPlusProvider, + }; +}); diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 5ecd1e7..910c99b 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -9,6 +9,7 @@ chrome, CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUD_MAIL_PROVIDER = 'cloudmail', + OUTLOOK_EMAIL_PLUS_PROVIDER = 'outlook-email-plus', completeNodeFromBackground, confirmCustomVerificationStepBypass, ensureMail2925MailboxSession, @@ -603,6 +604,7 @@ || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER || mail.provider === CLOUD_MAIL_PROVIDER + || mail.provider === OUTLOOK_EMAIL_PLUS_PROVIDER ) { await addLog(`步骤 ${visibleStep}:正在通过 ${mail.label} 轮询验证码...`); } else { diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index 102cc24..ef4d8f0 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -20,6 +20,7 @@ LUCKMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUD_MAIL_PROVIDER = 'cloudmail', + OUTLOOK_EMAIL_PLUS_PROVIDER = 'outlook-email-plus', resolveVerificationStep, reuseOrCreateTab, sendToContentScript, @@ -120,6 +121,7 @@ || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER || mail.provider === CLOUD_MAIL_PROVIDER + || mail.provider === OUTLOOK_EMAIL_PLUS_PROVIDER ) { await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); } else if (mail.provider === '2925') { @@ -146,6 +148,7 @@ LUCKMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUD_MAIL_PROVIDER, + OUTLOOK_EMAIL_PLUS_PROVIDER, ].includes(mail.provider); const signupProfile = buildSignupProfileForVerificationStep(); diff --git a/background/verification-flow.js b/background/verification-flow.js index 8b07c40..9ae3009 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -12,6 +12,7 @@ closeConflictingTabsForSource, CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUD_MAIL_PROVIDER = 'cloudmail', + OUTLOOK_EMAIL_PLUS_PROVIDER = 'outlook-email-plus', completeNodeFromBackground, confirmCustomVerificationStepBypassRequest, getNodeIdByStepForState, @@ -28,6 +29,7 @@ MAIL_2925_VERIFICATION_MAX_ATTEMPTS, pollCloudflareTempEmailVerificationCode, pollCloudMailVerificationCode, + pollOutlookEmailPlusVerificationCode, pollHotmailVerificationCode, pollLuckmailVerificationCode, sendToContentScript, @@ -985,6 +987,13 @@ }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`); return pollCloudMailVerificationCode(step, state, timedPoll.payload); } + if (mail.provider === OUTLOOK_EMAIL_PLUS_PROVIDER) { + const timedPoll = await applyMailPollingTimeBudget(step, { + ...getVerificationPollPayload(step, state), + ...cleanPollOverrides, + }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`); + return pollOutlookEmailPlusVerificationCode(step, state, timedPoll.payload); + } if (Number(pollOverrides.resendIntervalMs) > 0) { return pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides); diff --git a/outlook-email-plus-utils.js b/outlook-email-plus-utils.js new file mode 100644 index 0000000..d134770 --- /dev/null +++ b/outlook-email-plus-utils.js @@ -0,0 +1,301 @@ +(function outlookEmailPlusUtilsModule(root, factory) { + if (typeof module !== 'undefined' && module.exports) { + module.exports = factory(); + return; + } + + root.OutlookEmailPlusUtils = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createOutlookEmailPlusUtils() { + const DEFAULT_OUTLOOK_EMAIL_PLUS_BASE_URL = ''; + + 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 normalizeOutlookEmailPlusBaseUrl(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); + parsed.hash = ''; + parsed.search = ''; + let pathname = String(parsed.pathname || '').replace(/\/+/g, '/'); + pathname = pathname.replace(/\/api\/external(?:\/.*)?$/i, ''); + pathname = pathname === '/' ? '' : pathname.replace(/\/+$/g, ''); + return `${parsed.origin}${pathname}`; + } catch (_error) { + return ''; + } + } + + function buildOutlookEmailPlusHeaders(config = {}, options = {}) { + const headers = {}; + const apiKey = firstNonEmptyString([ + config.apiKey, + config.outlookEmailPlusApiKey, + 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 joinOutlookEmailPlusUrl(baseUrl, path) { + const normalizedBase = normalizeOutlookEmailPlusBaseUrl(baseUrl); + const normalizedPath = String(path || '').trim(); + if (!normalizedBase || !normalizedPath) return normalizedBase || ''; + return `${normalizedBase}${normalizedPath.startsWith('/') ? '' : '/'}${normalizedPath}`; + } + + function normalizeOutlookEmailPlusAddress(value) { + return String(value || '').trim().toLowerCase(); + } + + function parseOutlookEmailPlusAddressParts(value = '') { + const normalized = normalizeOutlookEmailPlusAddress(value); + const atIndex = normalized.lastIndexOf('@'); + if (atIndex <= 0 || atIndex >= normalized.length - 1) { + return null; + } + + const local = normalized.slice(0, atIndex); + const domain = normalized.slice(atIndex + 1); + const plusIndex = local.indexOf('+'); + const baseLocal = plusIndex >= 0 ? local.slice(0, plusIndex) : local; + const tag = plusIndex >= 0 ? local.slice(plusIndex + 1) : ''; + return { + local, + domain, + baseLocal, + tag, + isTaggedAlias: plusIndex >= 0 && Boolean(baseLocal) && Boolean(tag), + }; + } + + function isOutlookEmailPlusTaggedAlias(value = '') { + return Boolean(parseOutlookEmailPlusAddressParts(value)?.isTaggedAlias); + } + + function deriveOutlookEmailPlusBaseAddress(value = '') { + const parts = parseOutlookEmailPlusAddressParts(value); + if (!parts?.baseLocal || !parts.domain) { + return ''; + } + return `${parts.baseLocal}@${parts.domain}`; + } + + function sanitizeOutlookEmailPlusTag(value = '') { + return String(value || '') + .trim() + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, '-') + .replace(/-{2,}/g, '-') + .replace(/^[._-]+|[._-]+$/g, ''); + } + + function generateOutlookEmailPlusTag(...values) { + const normalized = sanitizeOutlookEmailPlusTag(values.filter((value) => value !== undefined && value !== null).join('-')); + return normalized.slice(0, 64); + } + + function buildOutlookEmailPlusAliasAddress(baseAddress = '', tag = '') { + const parts = parseOutlookEmailPlusAddressParts(baseAddress); + const sanitizedTag = sanitizeOutlookEmailPlusTag(tag); + if (!parts?.baseLocal || !parts.domain || !sanitizedTag) { + return ''; + } + return `${parts.baseLocal}+${sanitizedTag}@${parts.domain}`; + } + + function buildOutlookEmailPlusPayPalAliasAddress(baseAddress = '', index = 1) { + const parts = parseOutlookEmailPlusAddressParts(baseAddress); + const numericIndex = Math.max(1, Math.floor(Number(index) || 1)); + if (!parts?.baseLocal || !parts.domain) { + return ''; + } + return `${parts.baseLocal}+PayPal${numericIndex}@${parts.domain}`; + } + + function getOutlookEmailPlusPayPalAliasIndex(aliasAddress = '', baseAddress = '') { + const aliasParts = parseOutlookEmailPlusAddressParts(aliasAddress); + const baseParts = parseOutlookEmailPlusAddressParts(baseAddress); + if (!aliasParts || !baseParts || aliasParts.domain !== baseParts.domain) { + return null; + } + const prefix = `${baseParts.baseLocal}+paypal`; + if (!aliasParts.local.startsWith(prefix)) { + return null; + } + const numeric = Number(aliasParts.local.slice(prefix.length)); + return Number.isInteger(numeric) && numeric > 0 ? numeric : null; + } + + function normalizeOutlookEmailPlusCallerIdPrefix(value = '') { + return String(value || '') + .trim() + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, '-') + .replace(/-{2,}/g, '-') + .replace(/^[-._]+|[-._]+$/g, ''); + } + + function normalizeOutlookEmailPlusProjectKey(value = '') { + return String(value || '').trim().toLowerCase(); + } + + function normalizeOutlookEmailPlusProvider(value = '') { + return String(value || '').trim().toLowerCase(); + } + + function normalizeOutlookEmailPlusVerificationCode(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: normalizeOutlookEmailPlusVerificationCode(firstNonEmptyString([ + source.code, + source.verification_code, + source.verificationCode, + ])), + emailTimestamp: normalizeOutlookEmailPlusTimestamp(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 normalizeOutlookEmailPlusTimestamp(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 normalizeOutlookEmailPlusClaim(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: normalizeOutlookEmailPlusAddress(firstNonEmptyString([ + source.email, + source.address, + ])), + domain: String(firstNonEmptyString([ + source.email_domain, + source.emailDomain, + source.domain, + ])).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, + ]), + raw: source, + }; + } + + function unwrapOutlookEmailPlusResponse(payload) { + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { + return payload; + } + + if (payload.success === false || payload.ok === false) { + throw buildOutlookEmailPlusResponseError(payload); + } + + if (payload.success === true || payload.ok === true) { + return Object.prototype.hasOwnProperty.call(payload, 'data') ? payload.data : payload; + } + + return payload; + } + + function buildOutlookEmailPlusResponseError(payload = {}) { + const code = firstNonEmptyString([ + payload.code, + payload.error_code, + payload.errorCode, + ]); + const message = firstNonEmptyString([ + payload.message, + payload.error, + payload.msg, + payload.data?.message, + payload.data?.error, + code && `code=${code}`, + 'Outlook Email Plus 请求失败', + ]); + const error = new Error(code && !message.includes(code) ? `${message} (${code})` : message); + if (code) { + error.code = code; + } + return error; + } + + return { + DEFAULT_OUTLOOK_EMAIL_PLUS_BASE_URL, + buildOutlookEmailPlusAliasAddress, + buildOutlookEmailPlusHeaders, + buildOutlookEmailPlusPayPalAliasAddress, + deriveOutlookEmailPlusBaseAddress, + generateOutlookEmailPlusTag, + getOutlookEmailPlusPayPalAliasIndex, + isOutlookEmailPlusTaggedAlias, + joinOutlookEmailPlusUrl, + normalizeOutlookEmailPlusAddress, + normalizeOutlookEmailPlusBaseUrl, + normalizeOutlookEmailPlusCallerIdPrefix, + normalizeOutlookEmailPlusClaim, + normalizeOutlookEmailPlusProjectKey, + normalizeOutlookEmailPlusProvider, + normalizeOutlookEmailPlusVerificationCode, + parseOutlookEmailPlusAddressParts, + sanitizeOutlookEmailPlusTag, + unwrapOutlookEmailPlusResponse, + }; +}); diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 59fb73f..af915e4 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -553,6 +553,7 @@ + @@ -579,6 +580,7 @@ +
+