From 1ed128e1186360f7faf5c8fed3c76698f012eb02 Mon Sep 17 00:00:00 2001 From: InkCrow Date: Tue, 12 May 2026 16:41:09 +0800 Subject: [PATCH 1/6] fix(auth): preserve phone login identity across step8 retries --- background.js | 22 ++++--- background/steps/fetch-login-code.js | 18 ++++-- background/steps/oauth-login.js | 2 - background/verification-flow.js | 7 +++ tests/background-step6-retry-limit.test.js | 2 +- tests/background-step7-recovery.test.js | 24 ++++++-- tests/verification-flow-polling.test.js | 71 ++++++++++++++++++++++ 7 files changed, 127 insertions(+), 19 deletions(-) diff --git a/background.js b/background.js index b65b5d1..c5a6ccd 100644 --- a/background.js +++ b/background.js @@ -2993,14 +2993,22 @@ function isPhoneActivationForNumber(activation, phoneNumber) { async function setEmailStateSilently(email, options = {}) { const currentState = await getState(); - const updates = buildRegistrationEmailStateUpdates(currentState, { - currentEmail: email, - preservePrevious: Boolean(options?.preservePrevious), - source: options?.source || '', - }); + const preserveAccountIdentity = Boolean(options?.preserveAccountIdentity); + const updates = preserveAccountIdentity + ? buildFlowRegistrationEmailStateUpdates(currentState, { + currentEmail: email, + preservePrevious: Boolean(options?.preservePrevious), + preserveAccountIdentity: true, + source: options?.source || '', + }) + : buildRegistrationEmailStateUpdates(currentState, { + currentEmail: email, + preservePrevious: Boolean(options?.preservePrevious), + source: options?.source || '', + }); const normalizedEmail = updates.email; - if (normalizedEmail) { + if (!preserveAccountIdentity && normalizedEmail) { updates.accountIdentifierType = 'email'; updates.accountIdentifier = normalizedEmail; updates.phoneNumber = ''; @@ -3009,7 +3017,7 @@ async function setEmailStateSilently(email, options = {}) { updates.signupPhoneCompletedActivation = null; updates.signupPhoneVerificationRequestedAt = null; updates.signupPhoneVerificationPurpose = ''; - } else if (String(currentState?.accountIdentifierType || '').trim().toLowerCase() === 'email') { + } else if (!preserveAccountIdentity && String(currentState?.accountIdentifierType || '').trim().toLowerCase() === 'email') { updates.accountIdentifierType = null; updates.accountIdentifier = ''; } diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index edfcecd..2388709 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -31,6 +31,7 @@ reuseOrCreateTab, sendToContentScriptResilient, buildRegistrationEmailStateUpdates = null, + persistRegistrationEmailState = null, phoneVerificationHelpers = null, setState, shouldUseCustomRegistrationEmail, @@ -181,10 +182,17 @@ } const displayedEmail = normalizeStep8VerificationTargetEmail(result?.displayedEmail || resolvedEmail); - await setState({ - email: resolvedEmail, - step8VerificationTargetEmail: displayedEmail, - }); + if (typeof persistRegistrationEmailState === 'function') { + await persistRegistrationEmailState(latestState, resolvedEmail, { + source: 'step8_add_email', + preserveAccountIdentity: true, + }); + } else { + await setState({ + email: resolvedEmail, + step8VerificationTargetEmail: displayedEmail, + }); + } return { state: { @@ -535,6 +543,8 @@ } }, targetEmail: fixedTargetEmail, + maxResendRequests: mail.provider === '2925' ? 1 : undefined, + initialPollMaxAttempts: mail.provider === '2925' ? 5 : undefined, resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER ? 15000 : ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925') diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index ba39ca8..e330125 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -71,9 +71,7 @@ } function shouldPreferStep7PhoneSignupIdentity(state = {}) { - const frozenSignupMethod = normalizeStep7IdentifierType(state?.resolvedSignupMethod); return canUseConfiguredPhoneSignup(state) - && frozenSignupMethod !== 'email' && hasStep7PhoneSignupIdentity(state); } diff --git a/background/verification-flow.js b/background/verification-flow.js index 25fcb3d..32f6192 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -874,6 +874,7 @@ onResendRequestedAt, maxRounds: _ignoredMaxRounds, maxResendRequests: _ignoredMaxResendRequests, + initialPollMaxAttempts: _ignoredInitialPollMaxAttempts, ...cleanPollOverrides } = pollOverrides; const basePayload = { @@ -927,6 +928,7 @@ onResendRequestedAt, maxRounds: _ignoredMaxRounds, maxResendRequests: _ignoredMaxResendRequests, + initialPollMaxAttempts: _ignoredInitialPollMaxAttempts, ...cleanPollOverrides } = pollOverrides; @@ -982,6 +984,7 @@ let filterAfterTimestamp = cleanPollOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp; const maxResendRequests = resolveMaxResendRequests(pollOverrides); const maxRounds = maxResendRequests + 1; + const initialPollMaxAttempts = Math.max(0, Math.floor(Number(pollOverrides.initialPollMaxAttempts) || 0)); let usedResendRequests = 0; for (let round = 1; round <= maxRounds; round++) { @@ -1002,6 +1005,9 @@ filterAfterTimestamp, excludeCodes: [...rejectedCodes], }); + if (round === 1 && initialPollMaxAttempts > 0) { + payload.maxAttempts = initialPollMaxAttempts; + } try { const timedPoll = await applyMailPollingTimeBudget( @@ -1307,6 +1313,7 @@ disableTimeBudgetCap: Boolean(options.disableTimeBudgetCap), getRemainingTimeMs: options.getRemainingTimeMs, maxResendRequests: remainingAutomaticResendCount, + initialPollMaxAttempts: options.initialPollMaxAttempts, resendIntervalMs, lastResendAt, onResendRequestedAt: updateFilterAfterTimestampForVerificationStep, diff --git a/tests/background-step6-retry-limit.test.js b/tests/background-step6-retry-limit.test.js index 6528ba6..39df900 100644 --- a/tests/background-step6-retry-limit.test.js +++ b/tests/background-step6-retry-limit.test.js @@ -592,7 +592,7 @@ test('step 7 keeps phone login after step 8 stores an unbound email for phone si const phoneSignupState = { phoneVerificationEnabled: true, signupMethod: 'phone', - resolvedSignupMethod: 'phone', + resolvedSignupMethod: 'email', email: 'bound.step8@example.com', accountIdentifierType: 'email', accountIdentifier: 'bound.step8@example.com', diff --git a/tests/background-step7-recovery.test.js b/tests/background-step7-recovery.test.js index bfd3fe2..674bafd 100644 --- a/tests/background-step7-recovery.test.js +++ b/tests/background-step7-recovery.test.js @@ -266,6 +266,7 @@ test('step 8 submits add-email before polling the email verification code', asyn resolvedStates: [], setStates: [], mailStates: [], + persistCalls: [], }; const executor = api.createStep8Executor({ @@ -300,6 +301,9 @@ test('step 8 submits add-email before polling the email verification code', asyn isTabAlive: async () => true, isVerificationMailPollingError: () => false, LUCKMAIL_PROVIDER: 'luckmail-api', + persistRegistrationEmailState: async (state, email, options) => { + calls.persistCalls.push({ state, email, options }); + }, resolveSignupEmailForFlow: async (state, options = {}) => { calls.resolvedStates.push(state); calls.resolveOptions = options; @@ -338,14 +342,14 @@ test('step 8 submits add-email before polling the email verification code', asyn assert.equal(calls.contentMessages.length, 1); assert.equal(calls.resolvedStates.length, 1); assert.equal(calls.resolveOptions.preserveAccountIdentity, true); + assert.equal(calls.persistCalls.length, 1); + assert.equal(calls.persistCalls[0].email, 'new.user@example.com'); + assert.equal(calls.persistCalls[0].options.preserveAccountIdentity, true); + assert.equal(calls.persistCalls[0].options.source, 'step8_add_email'); assert.equal(calls.mailStates[0].email, 'new.user@example.com'); assert.equal(calls.resolvedVerification.state.email, 'new.user@example.com'); assert.equal(calls.resolvedVerification.options.targetEmail, 'new.user@example.com'); assert.deepStrictEqual(calls.setStates, [ - { - email: 'new.user@example.com', - step8VerificationTargetEmail: 'new.user@example.com', - }, { step8VerificationTargetEmail: 'new.user@example.com', }, @@ -407,6 +411,14 @@ test('step 8 reruns step 7 with preserved phone login identity after add-email v isTabAlive: async () => true, isVerificationMailPollingError: () => false, LUCKMAIL_PROVIDER: 'luckmail-api', + persistRegistrationEmailState: async (_state, email, options) => { + assert.equal(email, 'new.user@example.com'); + assert.equal(options.preserveAccountIdentity, true); + runtimeState = { + ...runtimeState, + email, + }; + }, resolveSignupEmailForFlow: async (_state, options = {}) => { assert.equal(options.preserveAccountIdentity, true); runtimeState = { @@ -944,7 +956,7 @@ test('step 8 completes when polling fails but recovery probe shows oauth consent ]); }); -test('step 8 uses a fixed 10-minute lookback window and disables resend interval for 2925 mailbox polling', async () => { +test('step 8 uses a fixed 10-minute lookback window and delays 2925 resend until after one full poll', async () => { let capturedOptions = null; let ensureCalls = 0; let ensureOptions = null; @@ -1036,6 +1048,8 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval ]); assert.equal(capturedOptions.filterAfterTimestamp, 300000); assert.equal(capturedOptions.resendIntervalMs, 0); + assert.equal(capturedOptions.maxResendRequests, 1); + assert.equal(capturedOptions.initialPollMaxAttempts, 5); assert.equal(capturedOptions.targetEmail, ''); assert.equal(capturedOptions.beforeSubmit, undefined); assert.equal(typeof capturedOptions.getRemainingTimeMs, 'function'); diff --git a/tests/verification-flow-polling.test.js b/tests/verification-flow-polling.test.js index 14eedb8..d28aa41 100644 --- a/tests/verification-flow-polling.test.js +++ b/tests/verification-flow-polling.test.js @@ -835,6 +835,77 @@ test('verification flow keeps 2925 mailbox polling at 15 refresh attempts even w assert.ok(pollCall.options.timeoutMs >= 250000); }); +test('verification flow delays 2925 login resend until after the first full mailbox poll fails', async () => { + const events = []; + const pollMaxAttempts = []; + let pollCalls = 0; + + const helpers = api.createVerificationFlowHelpers({ + addLog: async () => {}, + chrome: { tabs: { update: async () => {} } }, + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + completeStepFromBackground: async () => {}, + confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }), + getHotmailVerificationPollConfig: () => ({}), + getHotmailVerificationRequestTimestamp: () => 0, + getState: async () => ({}), + getTabId: async () => 1, + HOTMAIL_PROVIDER: 'hotmail-api', + isStopError: () => false, + LUCKMAIL_PROVIDER: 'luckmail-api', + MAIL_2925_VERIFICATION_INTERVAL_MS: 15000, + MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15, + pollCloudflareTempEmailVerificationCode: async () => ({}), + pollHotmailVerificationCode: async () => ({}), + pollLuckmailVerificationCode: async () => ({}), + sendToContentScript: async (_source, message) => { + if (message.type === 'RESEND_VERIFICATION_CODE') { + events.push('resend'); + return { resent: true }; + } + if (message.type === 'FILL_CODE') { + events.push('fill'); + } + return {}; + }, + sendToContentScriptResilient: async () => ({}), + sendToMailContentScriptResilient: async (_mail, message) => { + events.push('poll'); + pollMaxAttempts.push(message.payload.maxAttempts); + pollCalls += 1; + return pollCalls === 1 + ? { error: '步骤 8:邮箱轮询结束,但未获取到验证码。' } + : { code: '654321', emailTimestamp: 123 }; + }, + setState: async () => {}, + setStepStatus: async () => {}, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + VERIFICATION_POLL_MAX_ROUNDS: 5, + }); + + await helpers.resolveVerificationStep( + 8, + { + email: 'user@example.com', + mailProvider: '2925', + lastLoginCode: null, + }, + { provider: '2925', label: '2925 邮箱' }, + { + maxResendRequests: 1, + initialPollMaxAttempts: 5, + requestFreshCodeFirst: false, + filterAfterTimestamp: 123, + resendIntervalMs: 0, + } + ); + + assert.deepStrictEqual(events.slice(0, 3), ['poll', 'resend', 'poll']); + assert.deepStrictEqual(pollMaxAttempts.slice(0, 2), [5, 15]); + assert.equal(events.filter((event) => event === 'resend').length, 1); +}); + test('verification flow keeps Hotmail request timestamp filtering on the first poll', async () => { const pollPayloads = []; From 0b74e600c0419cc5010fdd431961b0c12bfac51c Mon Sep 17 00:00:00 2001 From: InkCrow Date: Tue, 12 May 2026 19:46:30 +0800 Subject: [PATCH 2/6] fix(auth): preserve phone identity when step payload carries bound email --- background.js | 35 ++++++++++- background/message-router.js | 42 ++++++++++++- tests/auto-run-step6-restart.test.js | 59 +++++++++++++++++++ ...ckground-message-router-step2-skip.test.js | 44 +++++++++++--- 4 files changed, 168 insertions(+), 12 deletions(-) diff --git a/background.js b/background.js index c5a6ccd..f802da9 100644 --- a/background.js +++ b/background.js @@ -8924,6 +8924,34 @@ async function handleStepData(step, payload) { return messageRouter.handleStepData(step, payload); } + function shouldPreservePhoneIdentityForStepEmailPayload(state = {}, stepPayload = {}) { + if (String(stepPayload.accountIdentifierType || '').trim().toLowerCase() === 'email') { + return false; + } + return Boolean( + String(state.signupPhoneNumber || '').trim() + || (String(state.accountIdentifierType || '').trim().toLowerCase() === 'phone' + && String(state.accountIdentifier || '').trim()) + || state.signupPhoneActivation + || state.signupPhoneCompletedActivation + ); + } + + async function persistStepEmailPayload(email, stepPayload = {}, source = 'step_identity') { + if (!email) { + return; + } + const currentState = await getState(); + if (shouldPreservePhoneIdentityForStepEmailPayload(currentState, stepPayload)) { + await persistRegistrationEmailState(currentState, email, { + source, + preserveAccountIdentity: true, + }); + return; + } + await setEmailState(email); + } + switch (step) { case 1: { const updates = {}; @@ -8949,8 +8977,8 @@ async function handleStepData(step, payload) { break; } case 2: - if (payload.email) await setEmailState(payload.email); - if (payload.accountIdentifierType || payload.accountIdentifier || payload.signupPhoneNumber || payload.signupPhoneActivation) { + await persistStepEmailPayload(payload.email, payload, 'step2_identity'); + if (!payload.email && (payload.accountIdentifierType || payload.accountIdentifier || payload.signupPhoneNumber || payload.signupPhoneActivation)) { await setState({ accountIdentifierType: payload.accountIdentifierType || null, accountIdentifier: String(payload.accountIdentifier || '').trim(), @@ -8969,7 +8997,7 @@ async function handleStepData(step, payload) { } break; case 3: - if (payload.email) await setEmailState(payload.email); + await persistStepEmailPayload(payload.email, payload, 'step3_identity'); if (payload.signupVerificationRequestedAt) { await setState({ signupVerificationRequestedAt: payload.signupVerificationRequestedAt }); } @@ -11724,6 +11752,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter setContributionMode, setEmailState, setEmailStateSilently, + persistRegistrationEmailState, setFreeReusablePhoneActivation, setSignupPhoneState, setSignupPhoneStateSilently, diff --git a/background/message-router.js b/background/message-router.js index f3127a0..1285de4 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -104,6 +104,7 @@ setContributionMode, setEmailState, setEmailStateSilently, + persistRegistrationEmailState, setFreeReusablePhoneActivation, setSignupPhoneState, setSignupPhoneStateSilently, @@ -210,6 +211,42 @@ : ''; } + function hasPhoneSignupIdentity(state = {}) { + const identifierType = String(state?.accountIdentifierType || '').trim().toLowerCase(); + return Boolean( + String(state?.signupPhoneNumber || '').trim() + || (identifierType === 'phone' && String(state?.accountIdentifier || '').trim()) + || state?.signupPhoneActivation + || state?.signupPhoneCompletedActivation + ); + } + + function shouldPreservePhoneIdentityForEmailPayload(payload = {}, state = {}) { + const identifierType = String(payload?.accountIdentifierType || '').trim().toLowerCase(); + if (identifierType === 'email') { + return false; + } + return hasPhoneSignupIdentity(state); + } + + async function persistEmailIdentityFromStepPayload(email, payload = {}, source = 'step_payload') { + if (!email) { + return; + } + const state = await getState(); + const preserveAccountIdentity = shouldPreservePhoneIdentityForEmailPayload(payload, state); + if (preserveAccountIdentity && typeof persistRegistrationEmailState === 'function') { + await persistRegistrationEmailState(state, email, { + source, + preserveAccountIdentity: true, + }); + return; + } + await setEmailState(email, preserveAccountIdentity + ? { source, preserveAccountIdentity: true } + : { source }); + } + function normalizeAutomationWindowId(value) { if (value === null || value === undefined || value === '') { return null; @@ -262,7 +299,10 @@ const email = resolveEmailIdentityPayload(payload); if (identifierType === 'email' || email) { if (email) { - await setEmailState(email); + await persistEmailIdentityFromStepPayload(email, payload, 'step_identity'); + } + if (email) { + return; } const updates = { phoneNumber: '', diff --git a/tests/auto-run-step6-restart.test.js b/tests/auto-run-step6-restart.test.js index 70ea3f8..a729bc8 100644 --- a/tests/auto-run-step6-restart.test.js +++ b/tests/auto-run-step6-restart.test.js @@ -83,6 +83,65 @@ const defaultStepDefinitions = { 10: { key: 'platform-verify' }, }; +const PHONE_IDENTITY_STATE_KEYS = [ + 'phoneNumber', + 'signupPhoneNumber', + 'signupPhoneActivation', + 'signupPhoneCompletedActivation', + 'signupPhoneVerificationRequestedAt', + 'signupPhoneVerificationPurpose', + 'accountIdentifierType', + 'accountIdentifier', +]; + +function createDownstreamResetHarness(stepKey = '') { + return new Function(` +function getStepExecutionKeyForState() { + return ${JSON.stringify(stepKey)}; +} +${extractFunction('getDownstreamStateResets')} +return { getDownstreamStateResets }; +`)(); +} + +test('downstream restarts after account creation preserve phone signup identity fields', () => { + const numericResetHarness = createDownstreamResetHarness(''); + const stepKeyResetHarnesses = [ + createDownstreamResetHarness('oauth-login'), + createDownstreamResetHarness('fetch-login-code'), + createDownstreamResetHarness('confirm-oauth'), + ]; + const phoneState = { + accountIdentifierType: 'phone', + accountIdentifier: '+447780579093', + signupPhoneNumber: '+447780579093', + signupPhoneActivation: { activationId: 'active', phoneNumber: '+447780579093' }, + signupPhoneCompletedActivation: { activationId: 'done', phoneNumber: '+447780579093' }, + }; + + for (const step of [5, 6, 7, 8, 9]) { + const resets = numericResetHarness.getDownstreamStateResets(step, phoneState); + for (const key of PHONE_IDENTITY_STATE_KEYS) { + assert.equal( + Object.prototype.hasOwnProperty.call(resets, key), + false, + `step ${step} reset must not clear ${key}` + ); + } + } + + for (const harness of stepKeyResetHarnesses) { + const resets = harness.getDownstreamStateResets(10, phoneState); + for (const key of PHONE_IDENTITY_STATE_KEYS) { + assert.equal( + Object.prototype.hasOwnProperty.call(resets, key), + false, + `${key} must not be cleared by step-key reset` + ); + } + } +}); + function createHarness(options = {}) { const { startStep = 7, diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js index acf3638..eb12289 100644 --- a/tests/background-message-router-step2-skip.test.js +++ b/tests/background-message-router-step2-skip.test.js @@ -14,6 +14,7 @@ function createRouter(overrides = {}) { broadcasts: [], balanceRefreshes: [], emailStates: [], + persistedRegistrationEmails: [], signupPhoneStates: [], signupPhoneSilentStates: [], finalizePayloads: [], @@ -123,6 +124,9 @@ function createRouter(overrides = {}) { events.emailStates.push(email); }, setEmailStateSilently: async () => {}, + persistRegistrationEmailState: async (state, email, options) => { + events.persistedRegistrationEmails.push({ state, email, options }); + }, setSignupPhoneState: async (phoneNumber) => { events.signupPhoneStates.push(phoneNumber); }, @@ -206,14 +210,38 @@ test('message router clears stale signup phone runtime when step 2 resolves emai }); assert.deepStrictEqual(events.emailStates, ['user@example.com']); - assert.deepStrictEqual(events.signupPhoneSilentStates, [null]); - assert.ok(events.stateUpdates.some((updates) => ( - updates.accountIdentifierType === 'email' - && updates.accountIdentifier === 'user@example.com' - && updates.signupPhoneNumber === '' - && updates.signupPhoneActivation === null - && updates.signupPhoneCompletedActivation === null - ))); + assert.deepStrictEqual(events.signupPhoneSilentStates, []); + assert.deepStrictEqual(events.stateUpdates, []); +}); + +test('message router preserves phone signup identity when step payload only reports registration email', async () => { + const { router, events } = createRouter({ + state: { + stepStatuses: { 3: 'pending' }, + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + signupPhoneActivation: { activationId: 'active', phoneNumber: '+66959916439' }, + }, + }); + + await router.handleStepData(3, { + email: 'bound@example.com', + signupVerificationRequestedAt: 123456, + }); + + assert.deepStrictEqual(events.emailStates, []); + assert.equal(events.persistedRegistrationEmails.length, 1); + assert.equal(events.persistedRegistrationEmails[0].email, 'bound@example.com'); + assert.deepStrictEqual(events.persistedRegistrationEmails[0].options, { + source: 'step_identity', + preserveAccountIdentity: true, + }); + assert.equal(events.persistedRegistrationEmails[0].state.signupPhoneNumber, '+66959916439'); + assert.equal(events.persistedRegistrationEmails[0].state.accountIdentifierType, 'phone'); + assert.equal(events.signupPhoneSilentStates.length, 0); + assert.ok(!events.stateUpdates.some((updates) => updates.signupPhoneNumber === '')); + assert.ok(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === 123456)); }); test('message router does not overwrite a completed step 3 when step 2 is replayed', async () => { From 67c111ed43196b8d8f4c51fd02698231e8cd72b2 Mon Sep 17 00:00:00 2001 From: InkCrow Date: Tue, 12 May 2026 20:39:20 +0800 Subject: [PATCH 3/6] fix(auth): preserve phone identity across add-email retries --- background.js | 10 ++ background/message-router.js | 6 ++ background/steps/fetch-login-code.js | 9 +- background/steps/oauth-login.js | 7 ++ ...ckground-message-router-step2-skip.test.js | 25 +++++ tests/background-step6-retry-limit.test.js | 28 ++++- tests/background-step7-recovery.test.js | 100 ++++++++++++++++++ 7 files changed, 183 insertions(+), 2 deletions(-) diff --git a/background.js b/background.js index f802da9..c899566 100644 --- a/background.js +++ b/background.js @@ -9014,6 +9014,15 @@ async function handleStepData(step, payload) { } break; case 7: + if (payload.accountIdentifierType || payload.accountIdentifier || payload.signupPhoneNumber || payload.signupPhoneActivation || payload.signupPhoneCompletedActivation) { + await setState({ + accountIdentifierType: payload.accountIdentifierType || null, + accountIdentifier: String(payload.accountIdentifier || '').trim(), + signupPhoneNumber: String(payload.signupPhoneNumber || '').trim(), + signupPhoneActivation: payload.signupPhoneActivation || null, + signupPhoneCompletedActivation: payload.signupPhoneCompletedActivation || null, + }); + } if (payload.loginVerificationRequestedAt) { await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt }); } @@ -11509,6 +11518,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ LUCKMAIL_PROVIDER, resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep, resolveSignupEmailForFlow, + persistRegistrationEmailState, phoneVerificationHelpers, rerunStep7ForStep8Recovery: (...args) => rerunStep7ForStep8Recovery(...args), resolveSignupMethod, diff --git a/background/message-router.js b/background/message-router.js index 1285de4..b7d5737 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -574,6 +574,12 @@ } } break; + case 7: + await syncStepAccountIdentityFromPayload(payload); + if (payload.loginVerificationRequestedAt) { + await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt }); + } + break; case 8: await setState({ ...(payload.phoneVerification || payload.loginPhoneVerification ? { diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 2388709..3ad3fb3 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -182,21 +182,28 @@ } const displayedEmail = normalizeStep8VerificationTargetEmail(result?.displayedEmail || resolvedEmail); + let persistedState = latestState; if (typeof persistRegistrationEmailState === 'function') { await persistRegistrationEmailState(latestState, resolvedEmail, { source: 'step8_add_email', preserveAccountIdentity: true, }); + persistedState = typeof getState === 'function' ? await getState() : latestState; } else { await setState({ email: resolvedEmail, step8VerificationTargetEmail: displayedEmail, }); + persistedState = { + ...latestState, + email: resolvedEmail, + step8VerificationTargetEmail: displayedEmail, + }; } return { state: { - ...latestState, + ...persistedState, email: resolvedEmail, step8VerificationTargetEmail: displayedEmail, }, diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index e330125..bd5fd18 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -263,6 +263,13 @@ const completionPayload = { loginVerificationRequestedAt: result.loginVerificationRequestedAt || null, }; + if (currentIdentifierType === 'phone') { + completionPayload.accountIdentifierType = 'phone'; + completionPayload.accountIdentifier = currentPhoneNumber; + completionPayload.signupPhoneNumber = currentPhoneNumber; + completionPayload.signupPhoneCompletedActivation = currentState?.signupPhoneCompletedActivation || null; + completionPayload.signupPhoneActivation = currentState?.signupPhoneActivation || null; + } if (Object.prototype.hasOwnProperty.call(result || {}, 'skipLoginVerificationStep')) { completionPayload.skipLoginVerificationStep = Boolean(result.skipLoginVerificationStep); } diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js index eb12289..927b122 100644 --- a/tests/background-message-router-step2-skip.test.js +++ b/tests/background-message-router-step2-skip.test.js @@ -244,6 +244,31 @@ test('message router preserves phone signup identity when step payload only repo assert.ok(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === 123456)); }); +test('message router persists phone signup identity from step 7 completion payload', async () => { + const completedActivation = { + activationId: 'signup-done', + phoneNumber: '+5511917097811', + }; + const { router, events } = createRouter({ + state: { stepStatuses: { 7: 'completed', 8: 'pending' } }, + }); + + await router.handleStepData(7, { + loginVerificationRequestedAt: 123456, + accountIdentifierType: 'phone', + accountIdentifier: '+5511917097811', + signupPhoneNumber: '+5511917097811', + signupPhoneActivation: null, + signupPhoneCompletedActivation: completedActivation, + }); + + assert.deepStrictEqual(events.signupPhoneSilentStates, ['+5511917097811']); + assert.ok(events.stateUpdates.some((updates) => updates.signupPhoneActivation === null)); + assert.ok(events.stateUpdates.some((updates) => updates.signupPhoneCompletedActivation === completedActivation)); + assert.ok(events.stateUpdates.some((updates) => updates.loginVerificationRequestedAt === 123456)); + assert.deepStrictEqual(events.emailStates, []); +}); + test('message router does not overwrite a completed step 3 when step 2 is replayed', async () => { const { router, events } = createRouter({ state: { stepStatuses: { 3: 'completed' } }, diff --git a/tests/background-step6-retry-limit.test.js b/tests/background-step6-retry-limit.test.js index 39df900..de5a68e 100644 --- a/tests/background-step6-retry-limit.test.js +++ b/tests/background-step6-retry-limit.test.js @@ -463,12 +463,15 @@ test('step 7 forwards phone login identity payload when account identifier is ph const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope); const events = { + completions: [], payloads: [], }; const executor = api.createStep7Executor({ addLog: async () => {}, - completeStepFromBackground: async () => {}, + completeStepFromBackground: async (step, payload) => { + events.completions.push({ step, payload }); + }, getErrorMessage: (error) => error?.message || String(error || ''), getLoginAuthStateLabel: (state) => state || 'unknown', getState: async () => ({ @@ -524,6 +527,24 @@ test('step 7 forwards phone login identity payload when account identifier is ph visibleStep: 7, }, ]); + assert.deepStrictEqual(events.completions, [ + { + step: 7, + payload: { + loginVerificationRequestedAt: 123456, + accountIdentifierType: 'phone', + accountIdentifier: '66959916439', + signupPhoneNumber: '66959916439', + signupPhoneCompletedActivation: { + activationId: 'signup-done', + phoneNumber: '66959916439', + countryId: 52, + countryLabel: 'Thailand', + }, + signupPhoneActivation: null, + }, + }, + ]); }); test('step 7 keeps Plus email login even when phone sms runtime exists', async () => { @@ -737,6 +758,11 @@ test('step 7 can start from a manually filled signup phone without completed ste step: 7, payload: { loginVerificationRequestedAt: 987654, + accountIdentifierType: 'phone', + accountIdentifier: '+447780579093', + signupPhoneNumber: '+447780579093', + signupPhoneCompletedActivation: null, + signupPhoneActivation: null, }, }, ]); diff --git a/tests/background-step7-recovery.test.js b/tests/background-step7-recovery.test.js index 674bafd..e32d7e3 100644 --- a/tests/background-step7-recovery.test.js +++ b/tests/background-step7-recovery.test.js @@ -472,6 +472,106 @@ test('step 8 reruns step 7 with preserved phone login identity after add-email v assert.equal(calls.rerunStates[0].signupPhoneNumber, '+447780579093'); }); +test('step 8 add-email rereads persisted phone identity before rerunning step 7', async () => { + const calls = { + resolveCalls: 0, + rerunStates: [], + }; + let runtimeState = { + visibleStep: 8, + email: '', + password: 'secret', + oauthUrl: 'https://oauth.example/latest', + accountIdentifierType: 'email', + accountIdentifier: 'stale@example.com', + signupMethod: 'phone', + resolvedSignupMethod: 'phone', + phoneVerificationEnabled: true, + signupPhoneNumber: '', + }; + + const executor = api.createStep8Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + confirmCustomVerificationStepBypass: async () => {}, + ensureStep8VerificationPageReady: async () => ({ state: 'add_email_page', url: 'https://auth.openai.com/add-email' }), + getOAuthFlowRemainingMs: async () => 8000, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000), + getMailConfig: () => ({ + provider: 'qq', + label: 'QQ 邮箱', + source: 'mail-qq', + url: 'https://mail.qq.com', + navigateOnReuse: false, + }), + getState: async () => ({ ...runtimeState }), + getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2), + HOTMAIL_PROVIDER: 'hotmail-api', + isTabAlive: async () => true, + isVerificationMailPollingError: () => false, + LUCKMAIL_PROVIDER: 'luckmail-api', + persistRegistrationEmailState: async (_state, email, options) => { + assert.equal(email, 'new.user@example.com'); + assert.equal(options.preserveAccountIdentity, true); + runtimeState = { + ...runtimeState, + email, + accountIdentifierType: 'phone', + accountIdentifier: '+447780579093', + signupPhoneNumber: '+447780579093', + signupPhoneCompletedActivation: { + activationId: 'signup-done', + phoneNumber: '+447780579093', + }, + }; + }, + resolveSignupEmailForFlow: async (_state, options = {}) => { + assert.equal(options.preserveAccountIdentity, true); + return 'new.user@example.com'; + }, + resolveVerificationStep: async (_step, state) => { + calls.resolveCalls += 1; + assert.equal(state.accountIdentifierType, 'phone'); + assert.equal(state.signupPhoneNumber, '+447780579093'); + throw new Error('STEP8_RESTART_STEP7::step 8 verification page fell into login timeout retry state'); + }, + rerunStep7ForStep8Recovery: async () => { + calls.rerunStates.push({ ...runtimeState }); + }, + reuseOrCreateTab: async () => {}, + sendToContentScriptResilient: async () => ({ + submitted: true, + displayedEmail: 'new.user@example.com', + url: 'https://auth.openai.com/email-verification', + }), + setState: async (payload) => { + runtimeState = { + ...runtimeState, + ...payload, + }; + }, + shouldUseCustomRegistrationEmail: () => false, + STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000, + STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 2, + throwIfStopped: () => {}, + }); + + await assert.rejects( + () => executor.executeStep8({ ...runtimeState }), + /STEP8_RESTART_STEP7::/ + ); + + assert.equal(calls.resolveCalls, 2); + assert.equal(calls.rerunStates.length, 1); + assert.equal(calls.rerunStates[0].accountIdentifierType, 'phone'); + assert.equal(calls.rerunStates[0].signupPhoneNumber, '+447780579093'); +}); + test('step 8 email_in_use recovery preserves the previous registration baseline', async () => { const calls = { contentCalls: 0, From 6bd743cbd3014a14320c5db6b72d2555e3cdbd66 Mon Sep 17 00:00:00 2001 From: InkCrow Date: Tue, 12 May 2026 21:08:02 +0800 Subject: [PATCH 4/6] fix(mail): harden 2925 verification polling --- background/verification-flow.js | 4 +- content/mail-2925.js | 79 +++++++++++++++++++------ tests/mail-2925-content.test.js | 24 ++++++++ tests/verification-flow-polling.test.js | 74 +++++++++++++++++++++++ 4 files changed, 161 insertions(+), 20 deletions(-) diff --git a/background/verification-flow.js b/background/verification-flow.js index 32f6192..d3c395f 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -1313,7 +1313,9 @@ disableTimeBudgetCap: Boolean(options.disableTimeBudgetCap), getRemainingTimeMs: options.getRemainingTimeMs, maxResendRequests: remainingAutomaticResendCount, - initialPollMaxAttempts: options.initialPollMaxAttempts, + initialPollMaxAttempts: mail.provider === '2925' && rejectedCodes.size > 0 + ? undefined + : options.initialPollMaxAttempts, resendIntervalMs, lastResendAt, onResendRequestedAt: updateFilterAfterTimestampForVerificationStep, diff --git a/content/mail-2925.js b/content/mail-2925.js index 13dfe4f..9f683ec 100644 --- a/content/mail-2925.js +++ b/content/mail-2925.js @@ -269,6 +269,48 @@ function findMailItems() { return []; } +function isLikelyMail2925MailboxPage() { + const pageText = getPageTextSample(3000); + if (!pageText) { + return /#\/mailList/i.test(location.hash || location.href || ''); + } + + if (findRefreshButton() || findInboxLink()) { + return true; + } + + return /#\/mailList/i.test(location.hash || location.href || '') + && /收件箱|刷新|删除|写信|邮件|mail/i.test(pageText) + && !(/欢迎使用邮箱|登录|login/i.test(pageText) && /密码|password/i.test(pageText)); +} + +async function waitForMailboxReady(timeoutMs = 12000) { + const startedAt = Date.now(); + while (Date.now() - startedAt <= timeoutMs) { + if (typeof throwIfMail2925LimitReached === 'function') { + throwIfMail2925LimitReached(); + } + const items = findMailItems(); + if (items.length > 0) { + return { ready: true, items, empty: false }; + } + if (getMail2925DisplayedMailboxEmail() || isLikelyMail2925MailboxPage()) { + return { ready: true, items: [], empty: true }; + } + await sleep(500); + } + + const items = findMailItems(); + if (items.length > 0) { + return { ready: true, items, empty: false }; + } + return { + ready: Boolean(getMail2925DisplayedMailboxEmail() || isLikelyMail2925MailboxPage()), + items, + empty: items.length === 0, + }; +} + function findActionBySelectors(selectors = []) { for (const selector of selectors) { const candidates = document.querySelectorAll(selector); @@ -518,7 +560,7 @@ function detectMail2925ViewState() { } const mailboxEmail = getMail2925DisplayedMailboxEmail(); - if (findMailItems().length > 0 || mailboxEmail) { + if (findMailItems().length > 0 || mailboxEmail || isLikelyMail2925MailboxPage()) { return { view: 'mailbox', limitMessage: '', mailboxEmail }; } @@ -924,7 +966,8 @@ async function sleepRandom(minMs, maxMs = minMs) { } async function returnToInbox() { - if (findMailItems().length > 0) { + const currentMailbox = await waitForMailboxReady(1500); + if (currentMailbox.ready) { return true; } @@ -936,7 +979,8 @@ async function returnToInbox() { simulateClick(inboxLink); for (let attempt = 0; attempt < 20; attempt += 1) { await sleep(250); - if (findMailItems().length > 0) { + const mailbox = await waitForMailboxReady(250); + if (mailbox.ready) { return true; } } @@ -1196,27 +1240,20 @@ async function handlePollEmail(step, payload) { let initialItems = []; let initialLoadUsedRefresh = false; - for (let i = 0; i < 20; i += 1) { - initialItems = findMailItems(); - if (initialItems.length > 0) { - break; - } - await sleep(500); - } - - if (initialItems.length === 0) { + const initialMailbox = await waitForMailboxReady(10000); + initialItems = initialMailbox.items; + if (!initialMailbox.ready || initialItems.length === 0) { initialLoadUsedRefresh = true; await returnToInbox(); await refreshInbox(); - await sleep(2000); + const refreshedMailbox = await waitForMailboxReady(12000); if (typeof throwIfMail2925LimitReached === 'function') { throwIfMail2925LimitReached(); } - initialItems = findMailItems(); - } - - if (initialItems.length === 0) { - throw new Error('2925 邮箱列表未加载完成,请确认当前已打开收件箱。'); + if (!refreshedMailbox.ready) { + throw new Error('2925 邮箱列表未加载完成,请确认当前已打开收件箱。'); + } + initialItems = refreshedMailbox.items; } log(`步骤 ${step}:邮件列表已加载,共 ${initialItems.length} 封邮件`); @@ -1233,7 +1270,11 @@ async function handlePollEmail(step, payload) { await sleepRandom(900, 1500); } - const items = findMailItems(); + const mailbox = await waitForMailboxReady(3000); + if (!mailbox.ready) { + throw new Error('2925 邮箱列表未加载完成,请确认当前已打开收件箱。'); + } + const items = mailbox.items; if (items.length > 0) { for (let index = 0; index < items.length; index += 1) { const item = items[index]; diff --git a/tests/mail-2925-content.test.js b/tests/mail-2925-content.test.js index 54ebb47..6f670d9 100644 --- a/tests/mail-2925-content.test.js +++ b/tests/mail-2925-content.test.js @@ -179,6 +179,10 @@ function extractVerificationCode(text) { async function sleep() {} async function sleepRandom() {} +async function waitForMailboxReady() { + const items = findMailItems(); + return { ready: true, items, empty: items.length === 0 }; +} async function returnToInbox() { clickOrder.push('inbox'); @@ -277,6 +281,10 @@ function extractVerificationCode(text) { async function sleep() {} async function sleepRandom() {} +async function waitForMailboxReady() { + const items = findMailItems(); + return { ready: true, items, empty: items.length === 0 }; +} async function returnToInbox() { return true; } @@ -370,6 +378,10 @@ function extractVerificationCode(text) { async function sleep() {} async function sleepRandom() {} +async function waitForMailboxReady() { + const items = findMailItems(); + return { ready: true, items, empty: items.length === 0 }; +} async function returnToInbox() { return true; } @@ -459,6 +471,10 @@ function extractVerificationCode(text) { async function sleep() {} async function sleepRandom() {} +async function waitForMailboxReady() { + const items = findMailItems(); + return { ready: true, items, empty: items.length === 0 }; +} async function returnToInbox() { return true; } @@ -633,6 +649,10 @@ function simulateClick(node) { async function sleep() {} async function sleepRandom() {} +async function waitForMailboxReady() { + const items = findMailItems(); + return { ready: items.length > 0, items, empty: items.length === 0 }; +} ${bundle} @@ -706,6 +726,10 @@ function simulateClick(node) { async function sleep() {} async function sleepRandom() {} +async function waitForMailboxReady() { + const items = findMailItems(); + return { ready: items.length > 0, items, empty: items.length === 0 }; +} const console = { warn() {} }; const MAIL2925_PREFIX = '[MultiPage:mail-2925]'; diff --git a/tests/verification-flow-polling.test.js b/tests/verification-flow-polling.test.js index d28aa41..447c9db 100644 --- a/tests/verification-flow-polling.test.js +++ b/tests/verification-flow-polling.test.js @@ -906,6 +906,80 @@ test('verification flow delays 2925 login resend until after the first full mail assert.equal(events.filter((event) => event === 'resend').length, 1); }); +test('verification flow uses full 2925 polling window after a rejected login code', async () => { + const pollMaxAttempts = []; + const submittedCodes = []; + let pollCalls = 0; + + const helpers = api.createVerificationFlowHelpers({ + addLog: async () => {}, + chrome: { tabs: { update: async () => {} } }, + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + completeStepFromBackground: async () => {}, + confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }), + getHotmailVerificationPollConfig: () => ({}), + getHotmailVerificationRequestTimestamp: () => 0, + getState: async () => ({}), + getTabId: async () => 1, + HOTMAIL_PROVIDER: 'hotmail-api', + isStopError: () => false, + LUCKMAIL_PROVIDER: 'luckmail-api', + MAIL_2925_VERIFICATION_INTERVAL_MS: 15000, + MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15, + pollCloudflareTempEmailVerificationCode: async () => ({}), + pollHotmailVerificationCode: async () => ({}), + pollLuckmailVerificationCode: async () => ({}), + sendToContentScript: async (_source, message) => { + if (message.type === 'RESEND_VERIFICATION_CODE') { + return { resent: true }; + } + if (message.type === 'FILL_CODE') { + submittedCodes.push(message.payload.code); + return message.payload.code === '111111' + ? { invalidCode: true, errorText: 'Incorrect code' } + : { success: true }; + } + return {}; + }, + sendToContentScriptResilient: async () => ({}), + sendToMailContentScriptResilient: async (_mail, message) => { + if (message.type !== 'POLL_EMAIL') { + return {}; + } + pollCalls += 1; + pollMaxAttempts.push(message.payload.maxAttempts); + return pollCalls === 1 + ? { code: '111111', emailTimestamp: 1 } + : { code: '222222', emailTimestamp: 2 }; + }, + setState: async () => {}, + setStepStatus: async () => {}, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + VERIFICATION_POLL_MAX_ROUNDS: 5, + }); + + await helpers.resolveVerificationStep( + 8, + { + email: 'user@example.com', + mailProvider: '2925', + lastLoginCode: null, + }, + { provider: '2925', label: '2925 邮箱' }, + { + maxResendRequests: 0, + initialPollMaxAttempts: 5, + requestFreshCodeFirst: false, + filterAfterTimestamp: 123, + resendIntervalMs: 0, + } + ); + + assert.deepStrictEqual(pollMaxAttempts, [5, 15]); + assert.deepStrictEqual(submittedCodes, ['111111', '222222']); +}); + test('verification flow keeps Hotmail request timestamp filtering on the first poll', async () => { const pollPayloads = []; From 24a0b8a45d35d74acb0f36baaf23452ef90a2735 Mon Sep 17 00:00:00 2001 From: InkCrow Date: Wed, 13 May 2026 02:19:00 +0800 Subject: [PATCH 5/6] fix(mail): resend 2925 code after quick initial poll --- background/steps/fetch-login-code.js | 2 +- tests/background-step7-recovery.test.js | 4 ++-- tests/verification-flow-polling.test.js | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 3ad3fb3..a812cf9 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -551,7 +551,7 @@ }, targetEmail: fixedTargetEmail, maxResendRequests: mail.provider === '2925' ? 1 : undefined, - initialPollMaxAttempts: mail.provider === '2925' ? 5 : undefined, + initialPollMaxAttempts: mail.provider === '2925' ? 2 : undefined, resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER ? 15000 : ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925') diff --git a/tests/background-step7-recovery.test.js b/tests/background-step7-recovery.test.js index e32d7e3..9bc9e71 100644 --- a/tests/background-step7-recovery.test.js +++ b/tests/background-step7-recovery.test.js @@ -1056,7 +1056,7 @@ test('step 8 completes when polling fails but recovery probe shows oauth consent ]); }); -test('step 8 uses a fixed 10-minute lookback window and delays 2925 resend until after one full poll', async () => { +test('step 8 uses a fixed 10-minute lookback window and delays 2925 resend until after two quick polls', async () => { let capturedOptions = null; let ensureCalls = 0; let ensureOptions = null; @@ -1149,7 +1149,7 @@ test('step 8 uses a fixed 10-minute lookback window and delays 2925 resend until assert.equal(capturedOptions.filterAfterTimestamp, 300000); assert.equal(capturedOptions.resendIntervalMs, 0); assert.equal(capturedOptions.maxResendRequests, 1); - assert.equal(capturedOptions.initialPollMaxAttempts, 5); + assert.equal(capturedOptions.initialPollMaxAttempts, 2); assert.equal(capturedOptions.targetEmail, ''); assert.equal(capturedOptions.beforeSubmit, undefined); assert.equal(typeof capturedOptions.getRemainingTimeMs, 'function'); diff --git a/tests/verification-flow-polling.test.js b/tests/verification-flow-polling.test.js index 447c9db..f3a4a9f 100644 --- a/tests/verification-flow-polling.test.js +++ b/tests/verification-flow-polling.test.js @@ -835,7 +835,7 @@ test('verification flow keeps 2925 mailbox polling at 15 refresh attempts even w assert.ok(pollCall.options.timeoutMs >= 250000); }); -test('verification flow delays 2925 login resend until after the first full mailbox poll fails', async () => { +test('verification flow delays 2925 login resend until after the first quick mailbox poll fails', async () => { const events = []; const pollMaxAttempts = []; let pollCalls = 0; @@ -894,7 +894,7 @@ test('verification flow delays 2925 login resend until after the first full mail { provider: '2925', label: '2925 邮箱' }, { maxResendRequests: 1, - initialPollMaxAttempts: 5, + initialPollMaxAttempts: 2, requestFreshCodeFirst: false, filterAfterTimestamp: 123, resendIntervalMs: 0, @@ -902,7 +902,7 @@ test('verification flow delays 2925 login resend until after the first full mail ); assert.deepStrictEqual(events.slice(0, 3), ['poll', 'resend', 'poll']); - assert.deepStrictEqual(pollMaxAttempts.slice(0, 2), [5, 15]); + assert.deepStrictEqual(pollMaxAttempts.slice(0, 2), [2, 15]); assert.equal(events.filter((event) => event === 'resend').length, 1); }); @@ -969,14 +969,14 @@ test('verification flow uses full 2925 polling window after a rejected login cod { provider: '2925', label: '2925 邮箱' }, { maxResendRequests: 0, - initialPollMaxAttempts: 5, + initialPollMaxAttempts: 2, requestFreshCodeFirst: false, filterAfterTimestamp: 123, resendIntervalMs: 0, } ); - assert.deepStrictEqual(pollMaxAttempts, [5, 15]); + assert.deepStrictEqual(pollMaxAttempts, [2, 15]); assert.deepStrictEqual(submittedCodes, ['111111', '222222']); }); From 6118d02f563e4380d1e345283cdd3c6ef6403d9e Mon Sep 17 00:00:00 2001 From: InkCrow Date: Wed, 13 May 2026 02:28:02 +0800 Subject: [PATCH 6/6] fix(mail): split 2925 initial polling around resend --- background/steps/fetch-login-code.js | 5 +++-- background/verification-flow.js | 16 +++++++++++++++- tests/background-step7-recovery.test.js | 7 ++++--- tests/verification-flow-polling.test.js | 18 ++++++++++-------- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index a812cf9..46f3370 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -550,8 +550,9 @@ } }, targetEmail: fixedTargetEmail, - maxResendRequests: mail.provider === '2925' ? 1 : undefined, - initialPollMaxAttempts: mail.provider === '2925' ? 2 : undefined, + maxResendRequests: mail.provider === '2925' ? 2 : undefined, + initialPollMaxAttempts: mail.provider === '2925' ? 5 : undefined, + pollAttemptPlan: mail.provider === '2925' ? [2, 3, 15] : undefined, resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER ? 15000 : ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925') diff --git a/background/verification-flow.js b/background/verification-flow.js index d3c395f..c6a0ea6 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -875,6 +875,7 @@ maxRounds: _ignoredMaxRounds, maxResendRequests: _ignoredMaxResendRequests, initialPollMaxAttempts: _ignoredInitialPollMaxAttempts, + pollAttemptPlan: _ignoredPollAttemptPlan, ...cleanPollOverrides } = pollOverrides; const basePayload = { @@ -929,6 +930,7 @@ maxRounds: _ignoredMaxRounds, maxResendRequests: _ignoredMaxResendRequests, initialPollMaxAttempts: _ignoredInitialPollMaxAttempts, + pollAttemptPlan: _ignoredPollAttemptPlan, ...cleanPollOverrides } = pollOverrides; @@ -985,6 +987,12 @@ const maxResendRequests = resolveMaxResendRequests(pollOverrides); const maxRounds = maxResendRequests + 1; const initialPollMaxAttempts = Math.max(0, Math.floor(Number(pollOverrides.initialPollMaxAttempts) || 0)); + const configuredPollAttemptPlan = Array.isArray(pollOverrides.pollAttemptPlan) + ? pollOverrides.pollAttemptPlan + .map((value) => Math.floor(Number(value) || 0)) + .filter((value) => value > 0) + : []; + const pollAttemptPlan = rejectedCodes.size > 0 ? [] : configuredPollAttemptPlan; let usedResendRequests = 0; for (let round = 1; round <= maxRounds; round++) { @@ -1005,7 +1013,10 @@ filterAfterTimestamp, excludeCodes: [...rejectedCodes], }); - if (round === 1 && initialPollMaxAttempts > 0) { + const plannedPollMaxAttempts = pollAttemptPlan[round - 1] || 0; + if (plannedPollMaxAttempts > 0) { + payload.maxAttempts = plannedPollMaxAttempts; + } else if (round === 1 && initialPollMaxAttempts > 0) { payload.maxAttempts = initialPollMaxAttempts; } @@ -1316,6 +1327,9 @@ initialPollMaxAttempts: mail.provider === '2925' && rejectedCodes.size > 0 ? undefined : options.initialPollMaxAttempts, + pollAttemptPlan: mail.provider === '2925' && rejectedCodes.size > 0 + ? undefined + : options.pollAttemptPlan, resendIntervalMs, lastResendAt, onResendRequestedAt: updateFilterAfterTimestampForVerificationStep, diff --git a/tests/background-step7-recovery.test.js b/tests/background-step7-recovery.test.js index 9bc9e71..7ab4d5e 100644 --- a/tests/background-step7-recovery.test.js +++ b/tests/background-step7-recovery.test.js @@ -1056,7 +1056,7 @@ test('step 8 completes when polling fails but recovery probe shows oauth consent ]); }); -test('step 8 uses a fixed 10-minute lookback window and delays 2925 resend until after two quick polls', async () => { +test('step 8 uses a fixed 10-minute lookback window and plans 2925 polling as 2/3/15', async () => { let capturedOptions = null; let ensureCalls = 0; let ensureOptions = null; @@ -1148,8 +1148,9 @@ test('step 8 uses a fixed 10-minute lookback window and delays 2925 resend until ]); assert.equal(capturedOptions.filterAfterTimestamp, 300000); assert.equal(capturedOptions.resendIntervalMs, 0); - assert.equal(capturedOptions.maxResendRequests, 1); - assert.equal(capturedOptions.initialPollMaxAttempts, 2); + assert.equal(capturedOptions.maxResendRequests, 2); + assert.equal(capturedOptions.initialPollMaxAttempts, 5); + assert.deepStrictEqual(capturedOptions.pollAttemptPlan, [2, 3, 15]); assert.equal(capturedOptions.targetEmail, ''); assert.equal(capturedOptions.beforeSubmit, undefined); assert.equal(typeof capturedOptions.getRemainingTimeMs, 'function'); diff --git a/tests/verification-flow-polling.test.js b/tests/verification-flow-polling.test.js index f3a4a9f..eb43fcd 100644 --- a/tests/verification-flow-polling.test.js +++ b/tests/verification-flow-polling.test.js @@ -835,7 +835,7 @@ test('verification flow keeps 2925 mailbox polling at 15 refresh attempts even w assert.ok(pollCall.options.timeoutMs >= 250000); }); -test('verification flow delays 2925 login resend until after the first quick mailbox poll fails', async () => { +test('verification flow can run a 2/3/15 2925 resend polling plan', async () => { const events = []; const pollMaxAttempts = []; let pollCalls = 0; @@ -873,7 +873,7 @@ test('verification flow delays 2925 login resend until after the first quick mai events.push('poll'); pollMaxAttempts.push(message.payload.maxAttempts); pollCalls += 1; - return pollCalls === 1 + return pollCalls <= 2 ? { error: '步骤 8:邮箱轮询结束,但未获取到验证码。' } : { code: '654321', emailTimestamp: 123 }; }, @@ -893,17 +893,18 @@ test('verification flow delays 2925 login resend until after the first quick mai }, { provider: '2925', label: '2925 邮箱' }, { - maxResendRequests: 1, - initialPollMaxAttempts: 2, + maxResendRequests: 2, + initialPollMaxAttempts: 5, + pollAttemptPlan: [2, 3, 15], requestFreshCodeFirst: false, filterAfterTimestamp: 123, resendIntervalMs: 0, } ); - assert.deepStrictEqual(events.slice(0, 3), ['poll', 'resend', 'poll']); - assert.deepStrictEqual(pollMaxAttempts.slice(0, 2), [2, 15]); - assert.equal(events.filter((event) => event === 'resend').length, 1); + assert.deepStrictEqual(events.slice(0, 5), ['poll', 'resend', 'poll', 'resend', 'poll']); + assert.deepStrictEqual(pollMaxAttempts.slice(0, 3), [2, 3, 15]); + assert.equal(events.filter((event) => event === 'resend').length, 2); }); test('verification flow uses full 2925 polling window after a rejected login code', async () => { @@ -969,7 +970,8 @@ test('verification flow uses full 2925 polling window after a rejected login cod { provider: '2925', label: '2925 邮箱' }, { maxResendRequests: 0, - initialPollMaxAttempts: 2, + initialPollMaxAttempts: 5, + pollAttemptPlan: [2, 3, 15], requestFreshCodeFirst: false, filterAfterTimestamp: 123, resendIntervalMs: 0,