From 2c84205c4110f56e72cc24979c0082875ce27ac0 Mon Sep 17 00:00:00 2001 From: zjm54321 Date: Sun, 24 May 2026 01:26:39 +0800 Subject: [PATCH 1/9] Add Outlook Email Plus provider core Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- background/outlook-email-plus-provider.js | 640 ++++++++++++++++++++++ outlook-email-plus-utils.js | 301 ++++++++++ tests/outlook-email-plus-provider.test.js | 293 ++++++++++ tests/outlook-email-plus-utils.test.js | 143 +++++ 4 files changed, 1377 insertions(+) create mode 100644 background/outlook-email-plus-provider.js create mode 100644 outlook-email-plus-utils.js create mode 100644 tests/outlook-email-plus-provider.test.js create mode 100644 tests/outlook-email-plus-utils.test.js diff --git a/background/outlook-email-plus-provider.js b/background/outlook-email-plus-provider.js new file mode 100644 index 0000000..be17efb --- /dev/null +++ b/background/outlook-email-plus-provider.js @@ -0,0 +1,640 @@ +(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', + fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null, + getState = async () => ({}), + persistRegistrationEmailState = null, + setEmailState = async () => {}, + 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 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 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), + }; + } + + 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 { + ...storedClaim, + ...rememberedClaim, + }; + } + + function normalizeAliasMax(value, fallback = DEFAULT_ALIAS_MAX_PER_MAILBOX) { + const numeric = Math.floor(Number(value)); + if (Number.isFinite(numeric) && numeric >= 1) { + return Math.min(50, numeric); + } + const fallbackNumber = Math.floor(Number(fallback)); + if (Number.isFinite(fallbackNumber) && fallbackNumber >= 1) { + return Math.min(50, fallbackNumber); + } + return DEFAULT_ALIAS_MAX_PER_MAILBOX; + } + + function getAliasMaxForState(state = {}, fallback = DEFAULT_ALIAS_MAX_PER_MAILBOX) { + return normalizeAliasMax( + state.outlookEmailPlusAliasMaxPerMailbox ?? state.outlookAliasMaxPerAccount, + fallback + ); + } + + 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); + if (!rememberedClaim?.claimToken) { + return null; + } + 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: normalizeAliasMax(storedClaim.aliasMax, getAliasMaxForState(state)), + aliasUsed: Boolean(storedClaim.aliasUsed), + }; + } + + function buildReusableRegistrationClaim(storedClaim = {}, context = {}) { + const aliasMax = normalizeAliasMax(storedClaim.aliasMax, getAliasMaxForState(context.state || {}, storedClaim.aliasMax)); + if (!storedClaim.aliasUsed && storedClaim.registrationEmail) { + return { + ...storedClaim, + aliasMax, + }; + } + const nextAliasIndex = Math.max(0, Math.floor(Number(storedClaim.aliasIndex) || 0)) + 1; + if (nextAliasIndex > aliasMax) { + return { + ...storedClaim, + aliasMax, + exhausted: true, + }; + } + const registrationEmail = allocateRegistrationAlias(storedClaim.baseAddress, nextAliasIndex); + return { + ...storedClaim, + aliasIndex: nextAliasIndex, + aliasMax, + aliasUsed: false, + registrationEmail, + }; + } + + async function reuseOutlookEmailPlusClaimAddress(state = {}, config = {}, options = {}) { + const storedClaim = getReusableStoredClaim(state); + if (!storedClaim) { + return { reused: false }; + } + const claim = buildReusableRegistrationClaim(storedClaim, { + state, + }); + 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, + }); + if (typeof setState === 'function') { + await setState({ currentOutlookEmailPlusClaim: nextStoredClaim }); + } + 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 addLog(`Outlook Email Plus:已认领 ${claim.address},注册使用 ${claim.registrationEmail}`, 'ok'); + return claim.registrationEmail; + } + + async function markOutlookEmailPlusAliasUsed(state = {}) { + const latestState = state || await getState(); + const storedClaim = latestState.currentOutlookEmailPlusClaim; + if (!storedClaim || typeof storedClaim !== 'object') { + return { handled: false, reason: 'missing_claim' }; + } + const aliasMax = normalizeAliasMax(storedClaim.aliasMax, getAliasMaxForState(latestState)); + const aliasIndex = Math.max( + getPayPalAliasIndex(storedClaim.registrationEmail, storedClaim.baseAddress) || 0, + Math.floor(Number(storedClaim.aliasIndex) || 0) + ); + const normalizedAliasIndex = Math.max(1, aliasIndex || 1); + const alreadyUsed = Boolean(storedClaim.aliasUsed); + const nextStoredClaim = { + ...storedClaim, + aliasIndex: normalizedAliasIndex, + aliasMax, + aliasUsed: true, + }; + if (!alreadyUsed && typeof setState === 'function') { + await setState({ currentOutlookEmailPlusClaim: nextStoredClaim }); + } + return { + handled: true, + alreadyUsed, + exhausted: normalizedAliasIndex >= aliasMax, + aliasIndex: normalizedAliasIndex, + aliasMax, + registrationEmail: storedClaim.registrationEmail || storedClaim.address || '', + }; + } + + 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) { + 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/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/tests/outlook-email-plus-provider.test.js b/tests/outlook-email-plus-provider.test.js new file mode 100644 index 0000000..bb94ba9 --- /dev/null +++ b/tests/outlook-email-plus-provider.test.js @@ -0,0 +1,293 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); + +const outlookEmailPlusUtils = require('../outlook-email-plus-utils.js'); +require('../background/outlook-email-plus-provider.js'); + +const { createOutlookEmailPlusProvider } = globalThis.MultiPageBackgroundOutlookEmailPlusProvider; + +function createJsonResponse(payload, status = 200) { + return { + ok: status >= 200 && status < 300, + status, + text: async () => JSON.stringify(payload), + }; +} + +function createProviderHarness(routeHandler, initialState = {}) { + const requests = []; + const logs = []; + const persistedEmails = []; + const state = { + outlookEmailPlusBaseUrl: 'https://outlook-plus.test/api/external/health', + outlookEmailPlusApiKey: 'secret-key', + outlookEmailPlusProvider: 'outlook', + outlookEmailPlusProjectKey: 'openai', + outlookEmailPlusCallerIdPrefix: 'gujumpgate', + ...initialState, + }; + const provider = createOutlookEmailPlusProvider({ + ...outlookEmailPlusUtils, + addLog: async (message, level) => logs.push({ message, level }), + fetchImpl: async (url, options = {}) => { + const request = { + url, + options, + body: options.body ? JSON.parse(options.body) : null, + }; + requests.push(request); + return routeHandler(request, state, requests); + }, + getState: async () => state, + persistRegistrationEmailState: async (_state, email, options) => { + persistedEmails.push({ email, options }); + state.email = email; + state.registrationEmailState = { + ...(state.registrationEmailState || {}), + current: email, + }; + }, + setState: async (patch) => Object.assign(state, patch), + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + OUTLOOK_EMAIL_PLUS_GENERATOR: 'outlook-email-plus', + OUTLOOK_EMAIL_PLUS_PROVIDER: 'outlook-email-plus', + }); + return { logs, persistedEmails, provider, requests, state }; +} + +function claimPayload(accountId, email, claimToken) { + return { + success: true, + data: { + account_id: accountId, + email, + email_domain: email.split('@')[1], + claim_token: claimToken, + claimed_at: '2026-05-23T00:00:00Z', + lease_expires_at: '2026-05-23T00:10:00Z', + }, + }; +} + +test('base claims generate PayPal aliases and store only sanitized claim state', async () => { + const harness = createProviderHarness((request) => { + const url = new URL(request.url); + if (url.pathname === '/api/external/pool/claim-random') { + return createJsonResponse(claimPayload('acct-1', 'User@Example.com', 'claim-token-1')); + } + if (url.pathname === '/api/external/pool/claim-complete') { + return createJsonResponse({ success: true, data: { ok: true } }); + } + throw new Error(`Unexpected request: ${url.pathname}`); + }, { outlookEmailPlusAliasMaxPerMailbox: 3 }); + + const address = await harness.provider.claimOutlookEmailPlusAddress(harness.state, { taskId: 'Task 42' }); + + assert.equal(address, 'user+PayPal1@example.com'); + assert.equal(harness.persistedEmails[0].email, 'user+PayPal1@example.com'); + const claimRequest = harness.requests[0]; + assert.equal(claimRequest.body.provider, 'outlook'); + assert.equal(claimRequest.body.project_key, 'openai'); + assert.match(claimRequest.body.caller_id, /^gujumpgate-/); + assert.equal(claimRequest.body.task_id, 'task-42'); + assert.equal(claimRequest.options.headers['X-API-Key'], 'secret-key'); + assert.equal(harness.state.currentOutlookEmailPlusClaim.claimToken, undefined); + assert.equal(harness.state.currentOutlookEmailPlusClaim.raw, undefined); + assert.equal(harness.state.currentOutlookEmailPlusClaim.taskId, claimRequest.body.task_id); + assert.equal(harness.state.currentOutlookEmailPlusClaim.address, 'user@example.com'); + assert.equal(harness.state.currentOutlookEmailPlusClaim.baseAddress, 'user@example.com'); + assert.equal(harness.state.currentOutlookEmailPlusClaim.registrationEmail, 'user+PayPal1@example.com'); + assert.equal(harness.state.currentOutlookEmailPlusClaim.isAliasClaim, false); + assert.equal(harness.state.currentOutlookEmailPlusClaim.aliasIndex, 1); + assert.equal(harness.state.currentOutlookEmailPlusClaim.aliasMax, 3); + assert.equal(harness.state.currentOutlookEmailPlusClaim.aliasUsed, false); + + const result = await harness.provider.completeOutlookEmailPlusClaim(harness.state); + + assert.deepEqual(result, { completed: true }); + const completeRequest = harness.requests[1]; + assert.equal(new URL(completeRequest.url).pathname, '/api/external/pool/claim-complete'); + assert.equal(completeRequest.body.account_id, 'acct-1'); + assert.equal(completeRequest.body.email, 'user@example.com'); + assert.equal(completeRequest.body.claim_token, 'claim-token-1'); + assert.equal(completeRequest.body.caller_id, claimRequest.body.caller_id); + assert.equal(completeRequest.body.task_id, claimRequest.body.task_id); + assert.equal(completeRequest.body.result, 'success'); + assert.equal(harness.state.currentOutlookEmailPlusClaim, null); +}); + +test('claimed aliases use the base mailbox for numbered registration aliases without nesting', async () => { + const harness = createProviderHarness((request) => { + const url = new URL(request.url); + if (url.pathname === '/api/external/pool/claim-random') { + return createJsonResponse(claimPayload('acct-alias', 'User+Claimed@Example.com', 'claim-token-alias')); + } + throw new Error(`Unexpected request: ${url.pathname}`); + }, { outlookEmailPlusAliasMaxPerMailbox: 2 }); + + const address = await harness.provider.claimOutlookEmailPlusAddress(harness.state, { taskId: 'alias-case' }); + + assert.equal(address, 'user+PayPal1@example.com'); + assert.equal(harness.persistedEmails[0].email, 'user+PayPal1@example.com'); + assert.equal(harness.state.currentOutlookEmailPlusClaim.address, 'user+claimed@example.com'); + assert.equal(harness.state.currentOutlookEmailPlusClaim.baseAddress, 'user@example.com'); + assert.equal(harness.state.currentOutlookEmailPlusClaim.registrationEmail, 'user+PayPal1@example.com'); + assert.equal(harness.state.currentOutlookEmailPlusClaim.isAliasClaim, true); + assert.equal(harness.state.currentOutlookEmailPlusClaim.aliasIndex, 1); + assert.equal(harness.state.currentOutlookEmailPlusClaim.aliasMax, 2); +}); + +test('claim reuse advances aliases up to the configured mailbox limit before claiming a new mailbox', async () => { + const claims = [ + claimPayload('acct-1', 'first@example.com', 'token-1'), + claimPayload('acct-2', 'second@example.com', 'token-2'), + ]; + const harness = createProviderHarness((request) => { + const url = new URL(request.url); + if (url.pathname === '/api/external/pool/claim-random') { + return createJsonResponse(claims.shift()); + } + if (url.pathname === '/api/external/pool/claim-complete') { + return createJsonResponse({ success: true, data: { ok: true } }); + } + throw new Error(`Unexpected request: ${url.pathname}`); + }, { outlookEmailPlusAliasMaxPerMailbox: 2 }); + + const first = await harness.provider.claimOutlookEmailPlusAddress(harness.state, { taskId: 'batch' }); + const repeated = await harness.provider.claimOutlookEmailPlusAddress(harness.state, { taskId: 'batch' }); + assert.equal(first, 'first+PayPal1@example.com'); + assert.equal(repeated, 'first+PayPal1@example.com'); + assert.equal(harness.requests.filter((request) => new URL(request.url).pathname === '/api/external/pool/claim-random').length, 1); + + const usedFirst = await harness.provider.markOutlookEmailPlusAliasUsed(harness.state); + assert.equal(usedFirst.exhausted, false); + assert.equal(usedFirst.aliasIndex, 1); + + const second = await harness.provider.claimOutlookEmailPlusAddress(harness.state, { taskId: 'batch' }); + assert.equal(second, 'first+PayPal2@example.com'); + assert.equal(harness.requests.filter((request) => new URL(request.url).pathname === '/api/external/pool/claim-random').length, 1); + + const usedSecond = await harness.provider.markOutlookEmailPlusAliasUsed(harness.state); + assert.equal(usedSecond.exhausted, true); + assert.equal(usedSecond.aliasIndex, 2); + + const third = await harness.provider.claimOutlookEmailPlusAddress(harness.state, { taskId: 'batch-2' }); + assert.equal(third, 'second+PayPal1@example.com'); + const completeRequest = harness.requests.find((request) => new URL(request.url).pathname === '/api/external/pool/claim-complete'); + assert.equal(completeRequest.body.account_id, 'acct-1'); + assert.equal(completeRequest.body.email, 'first@example.com'); + assert.equal(completeRequest.body.claim_token, 'token-1'); + assert.equal(harness.requests.filter((request) => new URL(request.url).pathname === '/api/external/pool/claim-random').length, 2); + assert.equal(harness.state.currentOutlookEmailPlusClaim.address, 'second@example.com'); + assert.equal(harness.state.currentOutlookEmailPlusClaim.registrationEmail, 'second+PayPal1@example.com'); + assert.equal(harness.state.currentOutlookEmailPlusClaim.aliasIndex, 1); + assert.equal(harness.state.currentOutlookEmailPlusClaim.aliasUsed, false); +}); + +test('alias max defaults to 5 and clamps configured values to 50', async () => { + const defaultHarness = createProviderHarness((request) => { + const url = new URL(request.url); + if (url.pathname === '/api/external/pool/claim-random') { + return createJsonResponse(claimPayload('acct-default', 'default@example.com', 'token-default')); + } + throw new Error(`Unexpected request: ${url.pathname}`); + }); + + await defaultHarness.provider.claimOutlookEmailPlusAddress(defaultHarness.state, { taskId: 'default-max' }); + assert.equal(defaultHarness.state.currentOutlookEmailPlusClaim.aliasMax, 5); + + const clampedHarness = createProviderHarness((request) => { + const url = new URL(request.url); + if (url.pathname === '/api/external/pool/claim-random') { + return createJsonResponse(claimPayload('acct-clamped', 'clamped@example.com', 'token-clamped')); + } + throw new Error(`Unexpected request: ${url.pathname}`); + }, { outlookEmailPlusAliasMaxPerMailbox: 99 }); + + await clampedHarness.provider.claimOutlookEmailPlusAddress(clampedHarness.state, { taskId: 'clamped-max' }); + assert.equal(clampedHarness.state.currentOutlookEmailPlusClaim.aliasMax, 50); +}); + +test('release uses remembered claim token and original claimed address', async () => { + const harness = createProviderHarness((request) => { + const url = new URL(request.url); + if (url.pathname === '/api/external/pool/claim-random') { + return createJsonResponse(claimPayload('acct-2', 'release@example.com', 'claim-token-2')); + } + if (url.pathname === '/api/external/pool/claim-release') { + return createJsonResponse({ success: true, data: { ok: true } }); + } + throw new Error(`Unexpected request: ${url.pathname}`); + }); + + await harness.provider.claimOutlookEmailPlusAddress(harness.state, { taskId: 'manual-task' }); + const result = await harness.provider.releaseOutlookEmailPlusClaim(harness.state, { reason: 'flow_failed' }); + + assert.deepEqual(result, { released: true }); + const releaseRequest = harness.requests[1]; + assert.equal(new URL(releaseRequest.url).pathname, '/api/external/pool/claim-release'); + assert.equal(releaseRequest.body.account_id, 'acct-2'); + assert.equal(releaseRequest.body.email, 'release@example.com'); + assert.equal(releaseRequest.body.claim_token, 'claim-token-2'); + assert.equal(releaseRequest.body.task_id, 'manual-task'); + assert.equal(releaseRequest.body.reason, 'flow_failed'); + assert.equal(harness.state.currentOutlookEmailPlusClaim, null); +}); + +test('verification polling avoids over-restrictive sender and subject filters', async () => { + const harness = createProviderHarness((request) => { + const url = new URL(request.url); + assert.equal(url.pathname, '/api/external/verification-code'); + assert.equal(url.searchParams.get('email'), 'target@example.com'); + assert.equal(url.searchParams.get('since_minutes'), '7'); + assert.equal(url.searchParams.get('code_length'), '6'); + assert.equal(url.searchParams.has('from_contains'), false); + assert.equal(url.searchParams.has('subject_contains'), false); + return createJsonResponse({ + success: true, + data: { + verification_code: '123456', + message_id: 'msg-1', + received_at: '2026-05-23T00:00:00Z', + }, + }); + }); + + const result = await harness.provider.pollOutlookEmailPlusVerificationCode(8, harness.state, { + targetEmail: 'target@example.com', + senderFilters: ['verify'], + subjectFilters: ['verify'], + sinceMinutes: 7, + codeLength: 6, + }); + + assert.equal(result.ok, true); + assert.equal(result.code, '123456'); + assert.equal(result.mailId, 'msg-1'); +}); + +test('verification polling prefers current registration alias over claimed base address', async () => { + const harness = createProviderHarness((request) => { + const url = new URL(request.url); + assert.equal(url.pathname, '/api/external/verification-code'); + assert.equal(url.searchParams.get('email'), 'user+paypal1@example.com'); + return createJsonResponse({ + success: true, + data: { + verification_code: '789012', + }, + }); + }); + harness.state.email = 'user@example.com'; + harness.state.registrationEmailState = { current: 'user+PayPal1@example.com' }; + harness.state.currentOutlookEmailPlusClaim = { + address: 'user@example.com', + registrationEmail: 'user+PayPal2@example.com', + }; + + const result = await harness.provider.pollOutlookEmailPlusVerificationCode(4, harness.state, {}); + + assert.equal(result.ok, true); + assert.equal(result.code, '789012'); +}); diff --git a/tests/outlook-email-plus-utils.test.js b/tests/outlook-email-plus-utils.test.js new file mode 100644 index 0000000..bd4d543 --- /dev/null +++ b/tests/outlook-email-plus-utils.test.js @@ -0,0 +1,143 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); + +const outlookEmailPlusUtils = require('../outlook-email-plus-utils.js'); + +test('Outlook Email Plus normalizes base URLs and strips external API paths', () => { + assert.equal(outlookEmailPlusUtils.normalizeOutlookEmailPlusBaseUrl(''), ''); + assert.equal( + outlookEmailPlusUtils.normalizeOutlookEmailPlusBaseUrl('mail.example.com/api/external/verification-code?x=1'), + 'https://mail.example.com' + ); + assert.equal( + outlookEmailPlusUtils.normalizeOutlookEmailPlusBaseUrl('http://mail.example.com/root/api/external/pool/claim-random'), + 'http://mail.example.com/root' + ); + assert.equal( + outlookEmailPlusUtils.joinOutlookEmailPlusUrl('mail.example.com/api/external/pool/claim-random', '/api/external/verification-code'), + 'https://mail.example.com/api/external/verification-code' + ); +}); + +test('Outlook Email Plus builds API headers for JSON requests', () => { + assert.deepEqual( + outlookEmailPlusUtils.buildOutlookEmailPlusHeaders({ outlookEmailPlusApiKey: 'secret-key' }, { json: true }), + { + 'X-API-Key': 'secret-key', + 'Content-Type': 'application/json', + Accept: 'application/json', + } + ); +}); + +test('Outlook Email Plus unwraps success payloads and raw payloads', () => { + assert.deepEqual( + outlookEmailPlusUtils.unwrapOutlookEmailPlusResponse({ success: true, data: { email: 'demo@example.com' } }), + { email: 'demo@example.com' } + ); + assert.deepEqual( + outlookEmailPlusUtils.unwrapOutlookEmailPlusResponse({ ok: true, data: { code: '123456' } }), + { code: '123456' } + ); + assert.deepEqual( + outlookEmailPlusUtils.unwrapOutlookEmailPlusResponse({ email: 'raw@example.com' }), + { email: 'raw@example.com' } + ); +}); + +test('Outlook Email Plus unwrap throws explicit business errors with message and code', () => { + assert.throws( + () => outlookEmailPlusUtils.unwrapOutlookEmailPlusResponse({ success: false, message: 'busy', code: 'E_BUSY' }), + /busy.*E_BUSY/ + ); + assert.throws( + () => outlookEmailPlusUtils.unwrapOutlookEmailPlusResponse({ ok: false, error: 'denied' }), + /denied/ + ); +}); + +test('Outlook Email Plus normalizes claim payload fields to camelCase', () => { + assert.deepEqual( + outlookEmailPlusUtils.normalizeOutlookEmailPlusClaim({ + account_id: 'acct_1', + email: 'User+tag@Example.com', + email_domain: 'Example.com', + claim_token: 'claim_123', + claimed_at: '2026-05-23T10:00:00.000Z', + lease_expires_at: '2026-05-23T10:30:00.000Z', + }), + { + accountId: 'acct_1', + address: 'user+tag@example.com', + domain: 'example.com', + claimToken: 'claim_123', + claimedAt: '2026-05-23T10:00:00.000Z', + leaseExpiresAt: '2026-05-23T10:30:00.000Z', + raw: { + account_id: 'acct_1', + email: 'User+tag@Example.com', + email_domain: 'Example.com', + claim_token: 'claim_123', + claimed_at: '2026-05-23T10:00:00.000Z', + lease_expires_at: '2026-05-23T10:30:00.000Z', + }, + } + ); +}); + +test('Outlook Email Plus parses aliases, derives base addresses, and builds sanitized aliases', () => { + assert.deepEqual( + outlookEmailPlusUtils.parseOutlookEmailPlusAddressParts('User+Claimed@Example.com'), + { + local: 'user+claimed', + domain: 'example.com', + baseLocal: 'user', + tag: 'claimed', + isTaggedAlias: true, + } + ); + assert.equal(outlookEmailPlusUtils.isOutlookEmailPlusTaggedAlias('user+claimed@example.com'), true); + assert.equal(outlookEmailPlusUtils.isOutlookEmailPlusTaggedAlias('user@example.com'), false); + assert.equal(outlookEmailPlusUtils.deriveOutlookEmailPlusBaseAddress('User+Claimed@Example.com'), 'user@example.com'); + assert.equal(outlookEmailPlusUtils.deriveOutlookEmailPlusBaseAddress('User@Example.com'), 'user@example.com'); + assert.equal(outlookEmailPlusUtils.sanitizeOutlookEmailPlusTag(' Run ID:42 / Demo '), 'run-id-42-demo'); + assert.equal( + outlookEmailPlusUtils.generateOutlookEmailPlusTag('Manual Task', 'OpenAI', 'Run_01'), + 'manual-task-openai-run_01' + ); + assert.equal( + outlookEmailPlusUtils.buildOutlookEmailPlusAliasAddress('User@Example.com', ' Run ID:42 / Demo '), + 'user+run-id-42-demo@example.com' + ); + assert.equal( + outlookEmailPlusUtils.buildOutlookEmailPlusPayPalAliasAddress('User@Example.com', 2), + 'user+PayPal2@example.com' + ); + assert.equal( + outlookEmailPlusUtils.getOutlookEmailPlusPayPalAliasIndex('USER+PayPal2@Example.com', 'user@example.com'), + 2 + ); + assert.equal( + outlookEmailPlusUtils.getOutlookEmailPlusPayPalAliasIndex('user+other@example.com', 'user@example.com'), + null + ); +}); + +test('Outlook Email Plus normalizes nested verification payloads', () => { + const result = outlookEmailPlusUtils.normalizeOutlookEmailPlusVerificationCode({ + data: { + verification_code: '654321', + received_at: '2026-05-23T10:02:03.000Z', + mail_id: 'mail_42', + }, + }); + + assert.equal(result.code, '654321'); + assert.equal(result.emailTimestamp, Date.parse('2026-05-23T10:02:03.000Z')); + assert.equal(result.mailId, 'mail_42'); + assert.deepEqual(result.raw, { + verification_code: '654321', + received_at: '2026-05-23T10:02:03.000Z', + mail_id: 'mail_42', + }); +}); From 18a3b21956d2b11b4737d4b1c4b4128ecb8525f2 Mon Sep 17 00:00:00 2001 From: zjm54321 Date: Sun, 24 May 2026 01:27:04 +0800 Subject: [PATCH 2/9] Wire Outlook Email Plus mail flow Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- background.js | 273 ++++++++++++++++++++++++++++-- background/auto-run-controller.js | 7 + 2 files changed, 270 insertions(+), 10 deletions(-) diff --git a/background.js b/background.js index b107b68..763b550 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 = '流程已被用户停止。'; @@ -1117,6 +1140,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, @@ -2321,6 +2350,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 +2573,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 +2659,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 +2897,41 @@ const { pollCloudMailVerificationCode, resolveCloudMailPollTargetEmail, } = cloudMailProvider; +const outlookEmailPlusProvider = self.MultiPageBackgroundOutlookEmailPlusProvider.createOutlookEmailPlusProvider({ + addLog, + buildOutlookEmailPlusAliasAddress, + buildOutlookEmailPlusHeaders, + buildOutlookEmailPlusPayPalAliasAddress, + deriveOutlookEmailPlusBaseAddress, + generateOutlookEmailPlusTag, + getOutlookEmailPlusPayPalAliasIndex, + isOutlookEmailPlusTaggedAlias, + joinOutlookEmailPlusUrl, + normalizeOutlookEmailPlusAddress, + normalizeOutlookEmailPlusBaseUrl, + normalizeOutlookEmailPlusCallerIdPrefix, + normalizeOutlookEmailPlusClaim, + normalizeOutlookEmailPlusProjectKey, + normalizeOutlookEmailPlusProvider, + normalizeOutlookEmailPlusVerificationCode, + OUTLOOK_EMAIL_PLUS_GENERATOR, + OUTLOOK_EMAIL_PLUS_PROVIDER, + getState, + persistRegistrationEmailState, + setEmailState, + setState, + sleepWithStop, + throwIfStopped, + unwrapOutlookEmailPlusResponse, +}); +const { + claimOutlookEmailPlusAddress, + completeOutlookEmailPlusClaim, + getOutlookEmailPlusConfig, + markOutlookEmailPlusAliasUsed, + pollOutlookEmailPlusVerificationCode, + releaseOutlookEmailPlusClaim, +} = outlookEmailPlusProvider; function normalizeSub2ApiGroupNames(value = '') { const source = Array.isArray(value) @@ -3240,6 +3313,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 +3406,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 normalizeOutlookAliasMaxPerAccount( + value, + PERSISTED_SETTING_DEFAULTS.outlookEmailPlusAliasMaxPerMailbox + ); case 'hotmailAccounts': return normalizeHotmailAccounts(value); case 'hotmailAliasEnabled': @@ -4760,6 +4851,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 +9500,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 +11462,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 +12092,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 +12270,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 +12352,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 +13766,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 +13787,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create MAIL_2925_VERIFICATION_MAX_ATTEMPTS, pollCloudflareTempEmailVerificationCode, pollCloudMailVerificationCode, + pollOutlookEmailPlusVerificationCode, pollHotmailVerificationCode, pollLuckmailVerificationCode, sendToContentScript, @@ -13671,6 +13919,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 +13978,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 +14593,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, From ff51a7e42798cc8f57a95cc572e090612cee7425 Mon Sep 17 00:00:00 2001 From: zjm54321 Date: Sun, 24 May 2026 01:27:31 +0800 Subject: [PATCH 3/9] Route Outlook Email Plus verification Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- background/logging-status.js | 1 + background/steps/fetch-login-code.js | 2 ++ background/steps/fetch-signup-code.js | 3 +++ background/verification-flow.js | 9 +++++++++ 4 files changed, 15 insertions(+) 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/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); From a0e380e1b5652d11ca58a1b8134d2489626b7281 Mon Sep 17 00:00:00 2001 From: zjm54321 Date: Sun, 24 May 2026 01:27:58 +0800 Subject: [PATCH 4/9] Add Outlook Email Plus settings UI Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- sidepanel/sidepanel.html | 47 ++++++++++ sidepanel/sidepanel.js | 184 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 230 insertions(+), 1 deletion(-) diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 59fb73f..6098831 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -553,6 +553,7 @@ + @@ -579,6 +580,7 @@ + +