diff --git a/README.md b/README.md index d4adf4d..a083c13 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ - 自动显示当前使用中的密码,便于后续保存 - 自动获取注册验证码与登录验证码 - 支持 `Hotmail`:继续使用 `邮箱 + 客户端 ID + 刷新令牌(refresh token)`,并可在远程服务与本地助手两种模式间切换 +- 支持 `2925`:新增多账号池、自动登录登出、Step 4 / Step 8 命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号 - 支持 `QQ Mail`、`163 Mail`、`Inbucket mailbox` - 支持从 DuckDuckGo Email Protection 自动生成新的 `@duck.com` 地址 - 支持基于 Cloudflare 自定义域名自动生成随机邮箱前缀 @@ -138,6 +139,17 @@ 4. 通过后再执行步骤或 `Auto` 5. 当前项目中,`Mail = Hotmail` 时会直接使用账号池里的邮箱作为注册邮箱,不再走 `Duck / Cloudflare` 自动生成 +### 方案 D:`2925 账号池` + +1. `Mail` 选择 `2925` +2. 在 `2925 账号池` 中添加 `邮箱 / 密码` +3. 在“2925 基邮箱”右侧打开 `号池` 开关后,基邮箱输入框会切成下拉框,只能从 2925 账号池中选择邮箱;关闭开关则回退到原来的手填基邮箱路线 +4. 可先点 `使用此账号` 让当前别名基邮箱切到这条 2925 账号,再点 `登录` 手动验证网页邮箱登录态 +5. 只有在 `号池` 开关开启时,自动流程执行到 Step 4 / Step 8 前才会自动检查 2925 登录态;如果未登录,会直接使用当前账号自动登录 +6. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间、把当前 2925 账号禁用 24 小时、自动切换到下一个可用账号并完成登录,然后直接结束当前尝试 +7. 如果你同时开启了 `Auto` 的自动重试,当前尝试结束后会按现有逻辑自动进入下一次尝试,不需要再手动介入 +8. `Mail = 2925` 仍然走 Gmail / 2925 共用的别名邮箱链路;实际注册邮箱会基于当前 2925 账号邮箱生成,例如 `name@2925.com -> name123456@2925.com` + ## 侧边栏配置说明 ### `CPA` @@ -166,6 +178,7 @@ Step 1 和 Step 10 都依赖这个地址。 支持五种验证码来源: - `Hotmail` +- `2925` - `163 Mail` - `163 VIP Mail` - `QQ Mail` @@ -174,6 +187,7 @@ Step 1 和 Step 10 都依赖这个地址。 说明: - `Hotmail` 通过侧边栏里的 Hotmail 账号池选择账号,可切换为远程服务模式或本地助手模式 +- `2925` 通过侧边栏里的 2925 账号池选择账号,并在 Step 4 / Step 8 前自动校验网页邮箱登录态 - `QQ`、`163`、`163 VIP` 用于直接轮询网页邮箱 - `Inbucket` 通过你在侧边栏里配置的 host 访问 `mailbox` 页面:`https:///m//` @@ -207,6 +221,24 @@ Step 1 和 Step 10 都依赖这个地址。 - 校验通过后,可点击 `测试收信` - Auto 模式每轮会自动选用一个可用账号 +### `2925 账号池` + +仅当 `Mail = 2925` 时使用。 + +每条账号支持保存: + +- `email` +- `password` + +使用方式: + +- 添加一个或多个 `2925.com` 账号 +- 点击 `使用此账号` 可以切换当前别名基邮箱来源 +- 点击 `登录` 可以让扩展直接打开 / 复用 2925 邮箱标签页,并使用当前账号自动登录 +- 如果账号因为“子邮箱已达上限邮箱”被临时禁用,列表里会展示上限记录时间和恢复时间 +- 命中冷却的账号可手动 `清冷却`,也可以等待 24 小时后自动恢复可用 +- Auto 模式在生成 2925 别名邮箱前,会自动分配当前可用账号;若 Step 4 / Step 8 命中上限,会自动切到下一个账号 + #### 本地 helper 启动命令 Windows: diff --git a/background.js b/background.js index b479a39..dc916ef 100644 --- a/background.js +++ b/background.js @@ -2,8 +2,10 @@ importScripts( 'managed-alias-utils.js', + 'mail2925-utils.js', 'background/account-run-history.js', 'background/contribution-oauth.js', + 'background/mail-2925-session.js', 'background/panel-bridge.js', 'background/generated-email-helpers.js', 'background/signup-flow-helpers.js', @@ -57,6 +59,16 @@ const { pickVerificationMessageWithTimeFallback, shouldClearHotmailCurrentSelection, } = self.HotmailUtils; +const { + MAIL2925_LIMIT_COOLDOWN_MS, + findMail2925Account, + getMail2925AccountStatus, + normalizeMail2925Account, + normalizeMail2925Accounts, + parseMail2925ImportText, + pickMail2925AccountForRun, + upsertMail2925AccountInList, +} = self.Mail2925Utils; const { fetchMicrosoftMailboxMessages, } = self.MultiPageMicrosoftEmail; @@ -248,6 +260,7 @@ const PERSISTED_SETTING_DEFAULTS = { verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT, mailProvider: '163', mail2925Mode: DEFAULT_MAIL_2925_MODE, + mail2925UseAccountPool: false, emailGenerator: 'duck', autoDeleteUsedIcloudAlias: false, icloudHostPreference: 'auto', @@ -270,6 +283,7 @@ const PERSISTED_SETTING_DEFAULTS = { cloudflareTempEmailDomain: '', cloudflareTempEmailDomains: [], hotmailAccounts: [], + mail2925Accounts: [], }; const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS); @@ -344,6 +358,7 @@ const DEFAULT_STATE = { oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, currentHotmailAccountId: null, + currentMail2925AccountId: null, preferredIcloudHost: '', }; @@ -874,6 +889,8 @@ function normalizePersistentSettingValue(key, value) { return normalizeMailProvider(value); case 'mail2925Mode': return normalizeMail2925Mode(value); + case 'mail2925UseAccountPool': + return Boolean(value); case 'emailGenerator': return normalizeEmailGenerator(value); case 'autoDeleteUsedIcloudAlias': @@ -914,6 +931,8 @@ function normalizePersistentSettingValue(key, value) { return normalizeCloudflareTempEmailDomains(value); case 'hotmailAccounts': return normalizeHotmailAccounts(value); + case 'mail2925Accounts': + return normalizeMail2925Accounts(value); default: return value; } @@ -2240,6 +2259,12 @@ function getManagedAliasBaseEmail(state = {}, provider = state?.mailProvider) { } if (normalizedProvider === '2925') { + const currentAccount = Boolean(state?.mail2925UseAccountPool) + ? getCurrentMail2925Account(state) + : null; + if (currentAccount?.email) { + return currentAccount.email; + } const mail2925BaseEmail = String(state?.mail2925BaseEmail || '').trim(); if (mail2925BaseEmail) { return mail2925BaseEmail; @@ -5355,6 +5380,79 @@ function getEmailGeneratorLabel(generator) { if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email'; return 'Duck 邮箱'; } +const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMail2925SessionManager({ + addLog, + broadcastDataUpdate, + chrome, + findMail2925Account, + getMail2925AccountStatus, + getState, + isMail2925AccountAvailable: self.Mail2925Utils?.isMail2925AccountAvailable, + MAIL2925_LIMIT_COOLDOWN_MS, + normalizeMail2925Account, + normalizeMail2925Accounts, + pickMail2925AccountForRun, + reuseOrCreateTab, + sendToMailContentScriptResilient, + setPersistentSettings, + setState, + throwIfStopped, + upsertMail2925AccountInList, +}); + +async function upsertMail2925Account(input = {}) { + return mail2925SessionManager.upsertMail2925Account(input); +} + +async function deleteMail2925Account(accountId) { + return mail2925SessionManager.deleteMail2925Account(accountId); +} + +async function deleteMail2925Accounts(mode = 'all') { + return mail2925SessionManager.deleteMail2925Accounts(mode); +} + +async function patchMail2925Account(accountId, updates = {}) { + return mail2925SessionManager.patchMail2925Account(accountId, updates); +} + +async function setCurrentMail2925Account(accountId, options = {}) { + return mail2925SessionManager.setCurrentMail2925Account(accountId, options); +} + +function getCurrentMail2925Account(state = null) { + return mail2925SessionManager.getCurrentMail2925Account(state || {}); +} + +async function ensureMail2925AccountForFlow(options = {}) { + return mail2925SessionManager.ensureMail2925AccountForFlow(options); +} + +async function ensureMail2925MailboxSession(options = {}) { + return mail2925SessionManager.ensureMail2925MailboxSession(options); +} + +async function handleMail2925LimitReachedError(step, error) { + return mail2925SessionManager.handleMail2925LimitReachedError(step, error); +} + +function isMail2925LimitReachedError(error) { + if (typeof mail2925SessionManager !== 'undefined' && mail2925SessionManager?.isMail2925LimitReachedError) { + return mail2925SessionManager.isMail2925LimitReachedError(error); + } + const message = String(typeof error === 'string' ? error : error?.message || ''); + return /^MAIL2925_LIMIT_REACHED::/.test(message) + || /子邮箱.{0,12}已达上限|已达上限邮箱|子邮箱上限|邮箱已达上限/i.test(message); +} + +function isMail2925ThreadTerminatedError(error) { + if (typeof mail2925SessionManager !== 'undefined' && mail2925SessionManager?.isMail2925ThreadTerminatedError) { + return mail2925SessionManager.isMail2925ThreadTerminatedError(error); + } + const message = String(typeof error === 'string' ? error : error?.message || ''); + return /^MAIL2925_THREAD_TERMINATED::/.test(message); +} + const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGeneratedEmailHelpers({ addLog, buildGeneratedAliasEmail, @@ -5366,6 +5464,7 @@ const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGenerat getCloudflareTempEmailAddressFromResponse, getCloudflareTempEmailConfig, getState, + ensureMail2925AccountForFlow, joinCloudflareTempEmailUrl, normalizeCloudflareDomain, normalizeCloudflareTempEmailAddress, @@ -5690,8 +5789,25 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) { return currentState.email; } - const baseEmail = getManagedAliasBaseEmail(currentState); - if (!baseEmail && !currentState.email) { + let managedAliasState = currentState; + if ( + String(currentState.mailProvider || '').trim().toLowerCase() === '2925' + && Boolean(currentState.mail2925UseAccountPool) + ) { + const account = await ensureMail2925AccountForFlow({ + allowAllocate: true, + preferredAccountId: currentState.currentMail2925AccountId || null, + markUsed: true, + }); + managedAliasState = { + ...(await getState()), + currentMail2925AccountId: account.id, + }; + await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:已分配 2925 账号 ${account.email}(第 ${attemptRuns} 次尝试)===`, 'ok'); + } + + const baseEmail = getManagedAliasBaseEmail(managedAliasState); + if (!baseEmail && !managedAliasState.email) { const baseLabel = currentState.mailProvider === GMAIL_PROVIDER ? 'Gmail 原邮箱' : '2925 基邮箱'; throw new Error(`${baseLabel}未设置,请先填写,或直接在“注册邮箱”中手动填写完整邮箱。`); } @@ -5830,6 +5946,10 @@ async function runAutoSequenceFromStep(startStep, context = {}) { if (isSignupUserAlreadyExistsFailure(err)) { throw err; } + if (isMail2925ThreadTerminatedError(err)) { + await addLog(`步骤 4:2925 已切换账号并要求结束当前尝试:${getErrorMessage(err)}`, 'warn'); + throw err; + } step4RestartCount += 1; const preservedState = await getState(); const preservedEmail = String(preservedState.email || '').trim(); @@ -6019,6 +6139,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe chrome, ensureContentScriptReadyOnTab, ensureHotmailAccountForFlow, + ensureMail2925AccountForFlow, ensureLuckmailPurchaseForFlow, getTabId, isGeneratedAliasProvider, @@ -6046,9 +6167,11 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create }), getHotmailVerificationPollConfig, getHotmailVerificationRequestTimestamp, + handleMail2925LimitReachedError, getState, getTabId, HOTMAIL_PROVIDER, + isMail2925LimitReachedError, isStopError, LUCKMAIL_PROVIDER, MAIL_2925_VERIFICATION_INTERVAL_MS, @@ -6099,6 +6222,7 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({ chrome, completeStepFromBackground, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, + ensureMail2925MailboxSession, getMailConfig, getTabId, HOTMAIL_PROVIDER, @@ -6144,6 +6268,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ chrome, CLOUDFLARE_TEMP_EMAIL_PROVIDER, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, + ensureMail2925MailboxSession, ensureStep8VerificationPageReady, getOAuthFlowRemainingMs, getOAuthFlowStepTimeoutMs, @@ -6219,6 +6344,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter deleteUsedIcloudAliases, disableUsedLuckmailPurchases, doesStepUseCompletionSignal, + ensureMail2925MailboxSession, ensureManualInteractionAllowed, executeStep, executeStepViaCompletionSignal, @@ -6254,12 +6380,15 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter launchAutoRunTimerPlan, listIcloudAliases, listLuckmailPurchasesForManagement, + getCurrentMail2925Account, normalizeHotmailAccounts, + normalizeMail2925Accounts, normalizeRunCount, AUTO_RUN_TIMER_KIND_SCHEDULED_START, notifyStepComplete, notifyStepError, patchHotmailAccount, + patchMail2925Account, registerTab, requestStop, resetState, @@ -6267,6 +6396,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter scheduleAutoRun, selectLuckmailPurchase, setCurrentHotmailAccount, + setCurrentMail2925Account, setContributionMode, setEmailState, setEmailStateSilently, @@ -6284,7 +6414,10 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter startAutoRunLoop, pollContributionStatus: (...args) => contributionOAuthManager?.pollContributionStatus?.(...args), syncHotmailAccounts, + deleteMail2925Account, + deleteMail2925Accounts, testHotmailAccountMailAccess, + upsertMail2925Account, upsertHotmailAccount, verifyHotmailAccount, }); diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js index 1f4d58c..3dee3e7 100644 --- a/background/auto-run-controller.js +++ b/background/auto-run-controller.js @@ -374,6 +374,7 @@ emailGenerator: prevState.emailGenerator, gmailBaseEmail: prevState.gmailBaseEmail, mail2925BaseEmail: prevState.mail2925BaseEmail, + currentMail2925AccountId: prevState.currentMail2925AccountId, emailPrefix: prevState.emailPrefix, inbucketHost: prevState.inbucketHost, inbucketMailbox: prevState.inbucketMailbox, diff --git a/background/generated-email-helpers.js b/background/generated-email-helpers.js index 208b1a3..be98af5 100644 --- a/background/generated-email-helpers.js +++ b/background/generated-email-helpers.js @@ -13,6 +13,7 @@ getCloudflareTempEmailAddressFromResponse, getCloudflareTempEmailConfig, getState, + ensureMail2925AccountForFlow, joinCloudflareTempEmailUrl, normalizeCloudflareDomain, normalizeCloudflareTempEmailAddress, @@ -190,7 +191,7 @@ async function fetchManagedAliasEmail(state, options = {}) { throwIfStopped(); const provider = String(options.mailProvider || state?.mailProvider || '').trim().toLowerCase(); - const mergedState = { + let mergedState = { ...(state || {}), mailProvider: provider, }; @@ -200,6 +201,22 @@ if (options.mail2925BaseEmail !== undefined) { mergedState.mail2925BaseEmail = String(options.mail2925BaseEmail || '').trim(); } + if ( + provider === '2925' + && Boolean(mergedState.mail2925UseAccountPool) + && typeof ensureMail2925AccountForFlow === 'function' + ) { + const account = await ensureMail2925AccountForFlow({ + allowAllocate: true, + preferredAccountId: mergedState.currentMail2925AccountId || null, + }); + const latestState = await getState(); + mergedState = { + ...latestState, + ...mergedState, + currentMail2925AccountId: account.id, + }; + } const email = buildGeneratedAliasEmail(mergedState); await setEmailState(email); diff --git a/background/mail-2925-session.js b/background/mail-2925-session.js new file mode 100644 index 0000000..cfa6a98 --- /dev/null +++ b/background/mail-2925-session.js @@ -0,0 +1,469 @@ +(function attachBackgroundMail2925Session(root, factory) { + root.MultiPageBackgroundMail2925Session = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundMail2925SessionModule() { + function createMail2925SessionManager(deps = {}) { + const { + addLog, + broadcastDataUpdate, + chrome, + findMail2925Account, + getMail2925AccountStatus, + isMail2925AccountAvailable, + MAIL2925_LIMIT_COOLDOWN_MS, + normalizeMail2925Account, + normalizeMail2925Accounts, + pickMail2925AccountForRun, + getState, + reuseOrCreateTab, + sendToMailContentScriptResilient, + setPersistentSettings, + setState, + throwIfStopped, + upsertMail2925AccountInList, + } = deps; + + const MAIL2925_SOURCE = 'mail-2925'; + const MAIL2925_URL = 'https://2925.com/#/mailList'; + const MAIL2925_LOGIN_URL = 'https://2925.com/'; + const MAIL2925_INJECT = ['content/utils.js', 'content/mail-2925.js']; + const MAIL2925_INJECT_SOURCE = 'mail-2925'; + const MAIL2925_COOKIE_DOMAINS = [ + '2925.com', + 'www.2925.com', + 'mail2.xiyouji.com', + ]; + const MAIL2925_COOKIE_ORIGINS = [ + 'https://2925.com', + 'https://www.2925.com', + 'https://mail2.xiyouji.com', + ]; + const MAIL2925_LIMIT_ERROR_PREFIX = 'MAIL2925_LIMIT_REACHED::'; + const MAIL2925_THREAD_TERMINATED_ERROR_PREFIX = 'MAIL2925_THREAD_TERMINATED::'; + + function getMail2925MailConfig() { + return { + provider: '2925', + source: MAIL2925_SOURCE, + url: MAIL2925_URL, + label: '2925 邮箱', + inject: MAIL2925_INJECT, + injectSource: MAIL2925_INJECT_SOURCE, + }; + } + + function getErrorMessage(error) { + return String(typeof error === 'string' ? error : error?.message || ''); + } + + function buildMail2925ThreadTerminatedError(message) { + return new Error(`${MAIL2925_THREAD_TERMINATED_ERROR_PREFIX}${String(message || '').trim()}`); + } + + function isMail2925LimitReachedError(error) { + const message = getErrorMessage(error); + return message.startsWith(MAIL2925_LIMIT_ERROR_PREFIX) + || /子邮箱.{0,12}已达上限|已达上限邮箱|子邮箱上限|邮箱已达上限/i.test(message); + } + + function isMail2925ThreadTerminatedError(error) { + return getErrorMessage(error).startsWith(MAIL2925_THREAD_TERMINATED_ERROR_PREFIX); + } + + async function syncMail2925Accounts(accounts) { + const normalized = normalizeMail2925Accounts(accounts); + await setPersistentSettings({ mail2925Accounts: normalized }); + await setState({ mail2925Accounts: normalized }); + broadcastDataUpdate({ mail2925Accounts: normalized }); + return normalized; + } + + async function upsertMail2925Account(input = {}) { + const state = await getState(); + const accounts = normalizeMail2925Accounts(state.mail2925Accounts); + const normalizedEmail = String(input?.email || '').trim().toLowerCase(); + const existing = input?.id + ? findMail2925Account(accounts, input.id) + : accounts.find((account) => account.email === normalizedEmail) || null; + const credentialsChanged = !existing + || (input?.email !== undefined && normalizedEmail !== existing.email) + || (input?.password !== undefined && String(input.password || '') !== existing.password); + const normalized = normalizeMail2925Account({ + ...(existing || {}), + ...(credentialsChanged ? { lastError: '' } : {}), + ...input, + id: input?.id || existing?.id || crypto.randomUUID(), + }); + + const nextAccounts = existing + ? accounts.map((account) => (account.id === normalized.id ? normalized : account)) + : [...accounts, normalized]; + + await syncMail2925Accounts(nextAccounts); + return normalized; + } + + function getCurrentMail2925Account(state = {}) { + return findMail2925Account(state.mail2925Accounts, state.currentMail2925AccountId) || null; + } + + async function setCurrentMail2925Account(accountId, options = {}) { + const { logMessage = '', updateLastUsedAt = false } = options; + const state = await getState(); + const accounts = normalizeMail2925Accounts(state.mail2925Accounts); + const account = findMail2925Account(accounts, accountId); + if (!account) { + throw new Error('未找到对应的 2925 账号。'); + } + + let nextAccount = account; + if (updateLastUsedAt) { + nextAccount = normalizeMail2925Account({ + ...account, + lastUsedAt: Date.now(), + }); + await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item))); + } + + await setState({ currentMail2925AccountId: nextAccount.id }); + broadcastDataUpdate({ currentMail2925AccountId: nextAccount.id }); + if (logMessage) { + await addLog(logMessage, 'ok'); + } + return nextAccount; + } + + async function patchMail2925Account(accountId, updates = {}) { + const state = await getState(); + const accounts = normalizeMail2925Accounts(state.mail2925Accounts); + const account = findMail2925Account(accounts, accountId); + if (!account) { + throw new Error('未找到对应的 2925 账号。'); + } + + const nextAccount = normalizeMail2925Account({ + ...account, + ...updates, + id: account.id, + }); + await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item))); + + if (state.currentMail2925AccountId === account.id && nextAccount.enabled === false) { + await setState({ currentMail2925AccountId: null }); + broadcastDataUpdate({ currentMail2925AccountId: null }); + } + + return nextAccount; + } + + async function deleteMail2925Account(accountId) { + const state = await getState(); + const accounts = normalizeMail2925Accounts(state.mail2925Accounts); + const nextAccounts = accounts.filter((account) => account.id !== accountId); + await syncMail2925Accounts(nextAccounts); + + if (state.currentMail2925AccountId === accountId) { + await setState({ currentMail2925AccountId: null }); + broadcastDataUpdate({ currentMail2925AccountId: null }); + } + } + + async function deleteMail2925Accounts(mode = 'all') { + const state = await getState(); + const accounts = normalizeMail2925Accounts(state.mail2925Accounts); + const nextAccounts = mode === 'all' + ? [] + : accounts.filter((account) => getMail2925AccountStatus(account) !== String(mode || '').trim()); + const deletedCount = Math.max(0, accounts.length - nextAccounts.length); + await syncMail2925Accounts(nextAccounts); + + if (state.currentMail2925AccountId && !findMail2925Account(nextAccounts, state.currentMail2925AccountId)) { + await setState({ currentMail2925AccountId: null }); + broadcastDataUpdate({ currentMail2925AccountId: null }); + } + + return { + deletedCount, + remainingCount: nextAccounts.length, + }; + } + + async function ensureMail2925AccountForFlow(options = {}) { + const { + allowAllocate = true, + preferredAccountId = null, + excludeIds = [], + markUsed = false, + } = options; + const state = await getState(); + const accounts = normalizeMail2925Accounts(state.mail2925Accounts); + const now = Date.now(); + + let account = null; + if (preferredAccountId) { + account = findMail2925Account(accounts, preferredAccountId); + } + if (!account && state.currentMail2925AccountId) { + account = findMail2925Account(accounts, state.currentMail2925AccountId); + } + if ((!account || !isMail2925AccountAvailable(account, now)) && allowAllocate) { + account = pickMail2925AccountForRun(accounts, { + excludeIds, + now, + }); + } + + if (!account) { + throw new Error('没有可用的 2925 账号。请先在侧边栏添加至少一个带密码的 2925 账号。'); + } + if (!account.password) { + throw new Error(`2925 账号 ${account.email || account.id} 缺少密码,无法自动登录。`); + } + if (!isMail2925AccountAvailable(account, now)) { + const disabledUntil = Number(account.disabledUntil || 0); + if (disabledUntil > now) { + throw new Error(`2925 账号 ${account.email || account.id} 当前处于冷却期,将在 ${new Date(disabledUntil).toLocaleString('zh-CN', { hour12: false })} 后恢复。`); + } + throw new Error(`2925 账号 ${account.email || account.id} 当前不可用。`); + } + + return setCurrentMail2925Account(account.id, { updateLastUsedAt: markUsed }); + } + + function normalizeCookieDomainForMatch(domain) { + return String(domain || '').trim().replace(/^\.+/, '').toLowerCase(); + } + + function shouldClearMail2925Cookie(cookie) { + const domain = normalizeCookieDomainForMatch(cookie?.domain); + if (!domain) return false; + return MAIL2925_COOKIE_DOMAINS.some((target) => ( + domain === target || domain.endsWith(`.${target}`) + )); + } + + function buildCookieRemovalUrl(cookie) { + const host = normalizeCookieDomainForMatch(cookie?.domain); + const path = String(cookie?.path || '/').startsWith('/') + ? String(cookie?.path || '/') + : `/${String(cookie?.path || '')}`; + return `https://${host}${path}`; + } + + async function collectMail2925Cookies() { + if (!chrome.cookies?.getAll) { + return []; + } + + const stores = chrome.cookies.getAllCookieStores + ? await chrome.cookies.getAllCookieStores() + : [{ id: undefined }]; + const cookies = []; + const seen = new Set(); + + for (const store of stores) { + const storeId = store?.id; + const batch = await chrome.cookies.getAll(storeId ? { storeId } : {}); + for (const cookie of batch || []) { + if (!shouldClearMail2925Cookie(cookie)) continue; + const key = [ + cookie.storeId || storeId || '', + cookie.domain || '', + cookie.path || '', + cookie.name || '', + cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '', + ].join('|'); + if (seen.has(key)) continue; + seen.add(key); + cookies.push(cookie); + } + } + + return cookies; + } + + async function removeMail2925Cookie(cookie) { + const details = { + url: buildCookieRemovalUrl(cookie), + name: cookie.name, + }; + + if (cookie.storeId) { + details.storeId = cookie.storeId; + } + if (cookie.partitionKey) { + details.partitionKey = cookie.partitionKey; + } + + try { + return Boolean(await chrome.cookies.remove(details)); + } catch { + return false; + } + } + + async function clearMail2925SessionCookies() { + if (!chrome.cookies?.getAll || !chrome.cookies?.remove) { + return 0; + } + + const cookies = await collectMail2925Cookies(); + let removedCount = 0; + for (const cookie of cookies) { + throwIfStopped(); + if (await removeMail2925Cookie(cookie)) { + removedCount += 1; + } + } + + if (chrome.browsingData?.removeCookies) { + try { + await chrome.browsingData.removeCookies({ + since: 0, + origins: MAIL2925_COOKIE_ORIGINS, + }); + } catch (_) { + // Best-effort cleanup only. + } + } + + return removedCount; + } + + async function ensureMail2925MailboxSession(options = {}) { + const { + accountId = null, + forceRelogin = false, + actionLabel = '确保 2925 邮箱登录态', + } = options; + const account = await ensureMail2925AccountForFlow({ + allowAllocate: true, + preferredAccountId: accountId, + }); + + if (forceRelogin) { + const removedCount = await clearMail2925SessionCookies(); + await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info'); + } + + throwIfStopped(); + await reuseOrCreateTab(MAIL2925_SOURCE, forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL, { + inject: MAIL2925_INJECT, + injectSource: MAIL2925_INJECT_SOURCE, + }); + + const result = await sendToMailContentScriptResilient( + getMail2925MailConfig(), + { + type: 'ENSURE_MAIL2925_SESSION', + step: 0, + source: 'background', + payload: { + email: account.email, + password: account.password, + forceLogin: forceRelogin, + }, + }, + { + timeoutMs: forceRelogin ? 90000 : 45000, + responseTimeoutMs: forceRelogin ? 90000 : 45000, + maxRecoveryAttempts: 2, + } + ); + + if (result?.error) { + throw new Error(result.error); + } + if (result?.limitReached) { + throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '2925 子邮箱已达上限邮箱'}`); + } + if (!result?.loggedIn) { + throw new Error(`2925:${actionLabel}失败,当前页面仍未进入收件箱。`); + } + + await patchMail2925Account(account.id, { + lastLoginAt: Date.now(), + lastError: '', + }); + await setState({ currentMail2925AccountId: account.id }); + broadcastDataUpdate({ currentMail2925AccountId: account.id }); + + return { + account: await ensureMail2925AccountForFlow({ + allowAllocate: false, + preferredAccountId: account.id, + }), + mail: getMail2925MailConfig(), + result, + }; + } + + async function handleMail2925LimitReachedError(step, error) { + const reason = getErrorMessage(error).replace(MAIL2925_LIMIT_ERROR_PREFIX, '').trim() + || '子邮箱已达上限邮箱'; + const state = await getState(); + const currentAccount = getCurrentMail2925Account(state); + if (!currentAccount) { + return buildMail2925ThreadTerminatedError(`步骤 ${step}:2925 检测到“${reason}”,但当前没有可识别的账号,已结束本次尝试。`); + } + + const disabledUntil = Date.now() + Math.max(1, Number(MAIL2925_LIMIT_COOLDOWN_MS) || (24 * 60 * 60 * 1000)); + await patchMail2925Account(currentAccount.id, { + lastLimitAt: Date.now(), + disabledUntil, + lastError: reason, + }); + await addLog( + `步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,已禁用 24 小时,恢复时间 ${new Date(disabledUntil).toLocaleString('zh-CN', { hour12: false })}。`, + 'warn' + ); + + const nextState = await getState(); + const nextAccounts = normalizeMail2925Accounts(nextState.mail2925Accounts); + const nextAccount = pickMail2925AccountForRun(nextAccounts, { + excludeIds: [currentAccount.id], + }); + + if (!nextAccount) { + await setState({ currentMail2925AccountId: null }); + broadcastDataUpdate({ currentMail2925AccountId: null }); + return buildMail2925ThreadTerminatedError( + `步骤 ${step}:2925 账号 ${currentAccount.email} 已因“${reason}”禁用 24 小时,且当前没有可切换的下一个账号,本次尝试结束。` + ); + } + + await setCurrentMail2925Account(nextAccount.id); + await ensureMail2925MailboxSession({ + accountId: nextAccount.id, + forceRelogin: true, + actionLabel: `步骤 ${step}:切换 2925 账号`, + }); + await addLog(`步骤 ${step}:2925 已自动切换到下一个账号 ${nextAccount.email} 并完成登录,当前尝试将直接结束。`, 'warn'); + return buildMail2925ThreadTerminatedError( + `步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”并已禁用 24 小时,已切换到 ${nextAccount.email},当前尝试结束,等待自动重试进入下一次尝试。` + ); + } + + return { + MAIL2925_LIMIT_ERROR_PREFIX, + MAIL2925_THREAD_TERMINATED_ERROR_PREFIX, + clearMail2925SessionCookies, + deleteMail2925Account, + deleteMail2925Accounts, + ensureMail2925AccountForFlow, + ensureMail2925MailboxSession, + getCurrentMail2925Account, + getMail2925MailConfig, + handleMail2925LimitReachedError, + isMail2925LimitReachedError, + isMail2925ThreadTerminatedError, + patchMail2925Account, + setCurrentMail2925Account, + syncMail2925Accounts, + upsertMail2925Account, + }; + } + + return { + createMail2925SessionManager, + }; +}); diff --git a/background/message-router.js b/background/message-router.js index af4c479..b8580fd 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -25,6 +25,7 @@ deleteUsedIcloudAliases, disableUsedLuckmailPurchases, doesStepUseCompletionSignal, + ensureMail2925MailboxSession, ensureManualInteractionAllowed, executeStep, executeStepViaCompletionSignal, @@ -35,6 +36,7 @@ findHotmailAccount, flushCommand, getCurrentLuckmailPurchase, + getCurrentMail2925Account, getPendingAutoRunTimerPlan, getSourceLabel, getState, @@ -52,10 +54,12 @@ listIcloudAliases, listLuckmailPurchasesForManagement, normalizeHotmailAccounts, + normalizeMail2925Accounts, normalizeRunCount, AUTO_RUN_TIMER_KIND_SCHEDULED_START, notifyStepComplete, notifyStepError, + patchMail2925Account, patchHotmailAccount, pollContributionStatus, registerTab, @@ -65,6 +69,7 @@ resumeAutoRun, scheduleAutoRun, selectLuckmailPurchase, + setCurrentMail2925Account, setCurrentHotmailAccount, setContributionMode, setEmailState, @@ -81,8 +86,11 @@ skipStep, startContributionFlow, startAutoRunLoop, + deleteMail2925Account, + deleteMail2925Accounts, syncHotmailAccounts, testHotmailAccountMailAccess, + upsertMail2925Account, upsertHotmailAccount, verifyHotmailAccount, } = deps; @@ -178,6 +186,13 @@ }); await addLog('当前 Hotmail 账号已自动标记为已用。', 'ok'); } + if (String(latestState.mailProvider || '').trim().toLowerCase() === '2925' && latestState.currentMail2925AccountId) { + await patchMail2925Account(latestState.currentMail2925AccountId, { + lastUsedAt: Date.now(), + lastError: '', + }); + await addLog('当前 2925 账号已记录最近使用时间。', 'ok'); + } if (isLuckmailProvider(latestState)) { const currentPurchase = getCurrentLuckmailPurchase(latestState); if (currentPurchase?.id) { @@ -578,6 +593,52 @@ return { ok: true, ...result }; } + case 'UPSERT_MAIL2925_ACCOUNT': { + const account = await upsertMail2925Account(message.payload || {}); + return { ok: true, account }; + } + + case 'DELETE_MAIL2925_ACCOUNT': { + await deleteMail2925Account(String(message.payload?.accountId || '')); + return { ok: true }; + } + + case 'DELETE_MAIL2925_ACCOUNTS': { + const result = await deleteMail2925Accounts(String(message.payload?.mode || 'all')); + return { ok: true, ...result }; + } + + case 'SELECT_MAIL2925_ACCOUNT': { + const account = await setCurrentMail2925Account(String(message.payload?.accountId || ''), { + updateLastUsedAt: false, + }); + return { ok: true, account }; + } + + case 'PATCH_MAIL2925_ACCOUNT': { + const account = await patchMail2925Account( + String(message.payload?.accountId || ''), + message.payload?.updates || {} + ); + return { ok: true, account }; + } + + case 'LOGIN_MAIL2925_ACCOUNT': { + const accountId = String(message.payload?.accountId || ''); + const account = await setCurrentMail2925Account(accountId, { + updateLastUsedAt: false, + }); + if (typeof deps.ensureMail2925MailboxSession !== 'function') { + throw new Error('2925 登录能力尚未接入。'); + } + await deps.ensureMail2925MailboxSession({ + accountId: account.id, + forceRelogin: Boolean(message.payload?.forceRelogin), + actionLabel: '侧边栏手动登录 2925 账号', + }); + return { ok: true, account }; + } + case 'LIST_LUCKMAIL_PURCHASES': { const purchases = await listLuckmailPurchasesForManagement(); return { ok: true, purchases }; diff --git a/background/signup-flow-helpers.js b/background/signup-flow-helpers.js index 59960ef..3ba844b 100644 --- a/background/signup-flow-helpers.js +++ b/background/signup-flow-helpers.js @@ -7,6 +7,7 @@ chrome, ensureContentScriptReadyOnTab, ensureHotmailAccountForFlow, + ensureMail2925AccountForFlow, ensureLuckmailPurchaseForFlow, isGeneratedAliasProvider, isReusableGeneratedAliasEmail, @@ -198,6 +199,15 @@ const purchase = await ensureLuckmailPurchaseForFlow({ allowReuse: true }); resolvedEmail = purchase.email_address; } else if (isGeneratedAliasProvider(state)) { + if (Boolean(state?.mail2925UseAccountPool) + && String(state?.mailProvider || '').trim().toLowerCase() === '2925' + && typeof ensureMail2925AccountForFlow === 'function') { + await ensureMail2925AccountForFlow({ + allowAllocate: true, + preferredAccountId: state.currentMail2925AccountId || null, + markUsed: true, + }); + } if (!isReusableGeneratedAliasEmail?.(state, resolvedEmail)) { resolvedEmail = buildGeneratedAliasEmail(state); } diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index fb7c78c..40dec9a 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -7,6 +7,7 @@ chrome, CLOUDFLARE_TEMP_EMAIL_PROVIDER, confirmCustomVerificationStepBypass, + ensureMail2925MailboxSession, ensureStep8VerificationPageReady, getOAuthFlowRemainingMs, getOAuthFlowStepTimeoutMs, @@ -105,6 +106,14 @@ || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER ) { await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`); + } else if (mail.provider === '2925') { + if (state?.mail2925UseAccountPool && typeof ensureMail2925MailboxSession === 'function') { + await ensureMail2925MailboxSession({ + accountId: state.currentMail2925AccountId || null, + actionLabel: '步骤 8:确认 2925 邮箱登录态', + }); + } + await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`); } else { await addLog(`步骤 8:正在打开${mail.label}...`); diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index a729946..fb5a1d6 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -7,6 +7,7 @@ chrome, completeStepFromBackground, confirmCustomVerificationStepBypass, + ensureMail2925MailboxSession, getMailConfig, getTabId, HOTMAIL_PROVIDER, @@ -69,6 +70,14 @@ throwIfStopped(); if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); + } else if (mail.provider === '2925') { + if (state?.mail2925UseAccountPool && typeof ensureMail2925MailboxSession === 'function') { + await ensureMail2925MailboxSession({ + accountId: state.currentMail2925AccountId || null, + actionLabel: '步骤 4:确认 2925 邮箱登录态', + }); + } + await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); } else { await addLog(`步骤 4:正在打开${mail.label}...`); diff --git a/background/verification-flow.js b/background/verification-flow.js index f21b143..0b38cfe 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -10,9 +10,11 @@ confirmCustomVerificationStepBypassRequest, getHotmailVerificationPollConfig, getHotmailVerificationRequestTimestamp, + handleMail2925LimitReachedError, getState, getTabId, HOTMAIL_PROVIDER, + isMail2925LimitReachedError, isStopError, LUCKMAIL_PROVIDER, MAIL_2925_VERIFICATION_INTERVAL_MS, @@ -417,6 +419,13 @@ if (isStopError(err)) { throw err; } + if (mail?.provider === '2925' && typeof isMail2925LimitReachedError === 'function' && isMail2925LimitReachedError(err)) { + const latestState = await getState(); + if (latestState?.mail2925UseAccountPool && typeof handleMail2925LimitReachedError === 'function') { + throw await handleMail2925LimitReachedError(step, err); + } + throw err; + } lastError = err; await addLog(`步骤 ${step}:${err.message}`, 'warn'); } @@ -554,6 +563,13 @@ if (isStopError(err)) { throw err; } + if (mail?.provider === '2925' && typeof isMail2925LimitReachedError === 'function' && isMail2925LimitReachedError(err)) { + const latestState = await getState(); + if (latestState?.mail2925UseAccountPool && typeof handleMail2925LimitReachedError === 'function') { + throw await handleMail2925LimitReachedError(step, err); + } + throw err; + } lastError = err; await addLog(`步骤 ${step}:${err.message}`, 'warn'); if (round < maxRounds) { diff --git a/content/mail-2925.js b/content/mail-2925.js index 92ccb2b..aa522b8 100644 --- a/content/mail-2925.js +++ b/content/mail-2925.js @@ -81,6 +81,16 @@ async function ensureSeenCodesSession(step, payload = {}) { } chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'ENSURE_MAIL2925_SESSION') { + resetStopState(); + ensureMail2925Session(message.payload).then((result) => { + sendResponse(result); + }).catch((err) => { + sendResponse({ error: err?.message || String(err || '2925 登录失败') }); + }); + return true; + } + if (message.type === 'POLL_EMAIL') { resetStopState(); handlePollEmail(message.step, message.payload).then((result) => { @@ -150,11 +160,70 @@ const MAIL_SELECT_ALL_SELECTORS = [ '[class*="checkbox"]', ]; const MAIL_ACTION_CANDIDATE_SELECTORS = 'button, [role="button"], a, label, span, div'; +const MAIL2925_LIMIT_ERROR_PREFIX = 'MAIL2925_LIMIT_REACHED::'; +const MAIL2925_LOGIN_INPUT_SELECTORS = [ + 'input[type="email"]', + 'input[name*="mail"]', + 'input[id*="mail"]', + 'input[name*="user"]', + 'input[id*="user"]', + 'input[placeholder*="邮箱"]', + 'input[placeholder*="账号"]', + 'input[placeholder*="用户名"]', + 'input[type="text"]', +]; +const MAIL2925_PASSWORD_INPUT_SELECTORS = [ + 'input[type="password"]', +]; +const MAIL2925_LOGIN_BUTTON_SELECTORS = [ + 'button[type="submit"]', + '.ivu-btn-primary', + '.el-button--primary', + '[class*="login"]', + '[class*="Login"]', +]; +const MAIL2925_LOGIN_BUTTON_PATTERNS = [ + /登录/i, + /login/i, + /立即登录/i, + /submit/i, +]; +const MAIL2925_AGREEMENT_PATTERNS = [ + /我已阅读并同意/, + /服务协议/, + /隐私政策/, +]; function normalizeNodeText(value) { return String(value || '').replace(/\s+/g, ' ').trim(); } +function buildMail2925LimitError(message = '') { + const normalized = normalizeNodeText(message || '子邮箱已达上限邮箱') || '子邮箱已达上限邮箱'; + return new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${normalized}`); +} + +function getPageTextSample(limit = 4000) { + return normalizeNodeText(document.body?.innerText || document.body?.textContent || '').slice(0, limit); +} + +function detectMail2925LimitMessage() { + const text = getPageTextSample(); + if (!text) { + return ''; + } + + const match = text.match(/子邮箱.{0,12}已达上限|已达上限邮箱|子邮箱上限|邮箱已达上限/); + return match ? match[0] : ''; +} + +function throwIfMail2925LimitReached() { + const limitMessage = detectMail2925LimitMessage(); + if (limitMessage) { + throw buildMail2925LimitError(limitMessage); + } +} + function isVisibleNode(node) { if (!node) return false; if (node.hidden) return false; @@ -208,6 +277,19 @@ function findActionBySelectors(selectors = []) { return null; } +function findVisibleInputBySelectors(selectors = []) { + for (const selector of selectors) { + const candidates = document.querySelectorAll(selector); + for (const candidate of candidates) { + if (!isVisibleNode(candidate) || candidate.disabled || candidate.readOnly) { + continue; + } + return candidate; + } + } + return null; +} + function findToolbarActionButton(patterns = [], selectors = []) { const directMatch = findActionBySelectors(selectors); if (directMatch) { @@ -231,6 +313,29 @@ function findToolbarActionButton(patterns = [], selectors = []) { return null; } +function findLoginButton() { + const directMatch = findActionBySelectors(MAIL2925_LOGIN_BUTTON_SELECTORS); + if (directMatch) { + return directMatch; + } + + const candidates = document.querySelectorAll(MAIL_ACTION_CANDIDATE_SELECTORS); + for (const candidate of candidates) { + const target = resolveActionTarget(candidate); + if (!isVisibleNode(target) || isMailItemNode(target)) { + continue; + } + + const text = normalizeNodeText(target.innerText || target.textContent || ''); + const label = normalizeNodeText(target.getAttribute?.('aria-label') || target.getAttribute?.('title') || ''); + if (MAIL2925_LOGIN_BUTTON_PATTERNS.some((pattern) => pattern.test(text) || pattern.test(label))) { + return target; + } + } + + return null; +} + function findRefreshButton() { return findToolbarActionButton([ /刷新/i, @@ -253,6 +358,117 @@ function findSelectAllControl() { return findActionBySelectors(MAIL_SELECT_ALL_SELECTORS); } +function findMail2925LoginEmailInput() { + const candidates = Array.from(document.querySelectorAll(MAIL2925_LOGIN_INPUT_SELECTORS.join(', '))); + for (const candidate of candidates) { + if (!isVisibleNode(candidate) || candidate.disabled || candidate.readOnly) { + continue; + } + if (candidate.type === 'password') { + continue; + } + const identifier = normalizeNodeText([ + candidate.name, + candidate.id, + candidate.placeholder, + candidate.getAttribute?.('aria-label'), + candidate.getAttribute?.('autocomplete'), + ].join(' ')).toLowerCase(); + if (/邮箱|账号|用户|mail|user|login/.test(identifier) || candidate.type === 'email') { + return candidate; + } + } + return null; +} + +function findMail2925LoginPasswordInput() { + return findVisibleInputBySelectors(MAIL2925_PASSWORD_INPUT_SELECTORS); +} + +function findAgreementContainer() { + const candidates = document.querySelectorAll('label, div, span, p, form'); + for (const candidate of candidates) { + if (!isVisibleNode(candidate)) { + continue; + } + const text = normalizeNodeText(candidate.innerText || candidate.textContent || ''); + if (!text) { + continue; + } + if (MAIL2925_AGREEMENT_PATTERNS.every((pattern) => pattern.test(text) || /我已阅读并同意/.test(text))) { + return candidate; + } + if (/我已阅读并同意/.test(text) && (/服务协议/.test(text) || /隐私政策/.test(text))) { + return candidate; + } + } + return null; +} + +function findAgreementCheckbox() { + const agreementContainer = findAgreementContainer(); + if (agreementContainer) { + const checkbox = agreementContainer.querySelector('input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox'); + if (checkbox) { + return resolveActionTarget(checkbox); + } + const nearbyCheckbox = agreementContainer.parentElement?.querySelector?.('input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox'); + if (nearbyCheckbox) { + return resolveActionTarget(nearbyCheckbox); + } + } + + const genericCheckboxes = document.querySelectorAll('input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox'); + for (const checkbox of genericCheckboxes) { + const target = resolveActionTarget(checkbox); + if (!isVisibleNode(target)) { + continue; + } + const wrapper = target.closest('label, div, span') || target.parentElement || target; + const text = normalizeNodeText(wrapper?.innerText || wrapper?.textContent || ''); + if (/我已阅读并同意/.test(text) || /服务协议/.test(text) || /隐私政策/.test(text)) { + return target; + } + } + + return null; +} + +async function ensureAgreementChecked() { + const agreementCheckbox = findAgreementCheckbox(); + if (!agreementCheckbox) { + return false; + } + if (isCheckboxChecked(agreementCheckbox)) { + return true; + } + simulateClick(agreementCheckbox); + await sleep(150); + return isCheckboxChecked(agreementCheckbox); +} + +function detectMail2925ViewState() { + const limitMessage = detectMail2925LimitMessage(); + if (limitMessage) { + return { view: 'limit', limitMessage }; + } + + if (findMailItems().length > 0) { + return { view: 'mailbox', limitMessage: '' }; + } + + if (findMail2925LoginPasswordInput() && findMail2925LoginEmailInput()) { + return { view: 'login', limitMessage: '' }; + } + + const pageText = getPageTextSample(); + if (/欢迎使用邮箱|登录|login/i.test(pageText) && /密码|password/i.test(pageText)) { + return { view: 'login', limitMessage: '' }; + } + + return { view: 'unknown', limitMessage: '' }; +} + function isCheckboxChecked(node) { const checkbox = node?.matches?.('input[type="checkbox"], [role="checkbox"]') ? node @@ -518,6 +734,9 @@ async function deleteAllMailboxEmails(step) { } async function refreshInbox() { + if (typeof throwIfMail2925LimitReached === 'function') { + throwIfMail2925LimitReached(); + } const refreshBtn = findRefreshButton(); if (refreshBtn) { simulateClick(refreshBtn); @@ -532,6 +751,100 @@ async function refreshInbox() { } } +async function waitForMail2925View(targetView, timeoutMs = 45000) { + const startedAt = Date.now(); + while (Date.now() - startedAt <= timeoutMs) { + throwIfStopped(); + const currentState = detectMail2925ViewState(); + if (currentState.view === 'limit') { + throw buildMail2925LimitError(currentState.limitMessage); + } + if (currentState.view === targetView) { + return currentState; + } + await sleep(500); + } + return detectMail2925ViewState(); +} + +async function ensureMail2925Session(payload = {}) { + const email = String(payload?.email || '').trim(); + const password = String(payload?.password || ''); + const forceLogin = Boolean(payload?.forceLogin); + + for (let attempt = 0; attempt < 10; attempt += 1) { + throwIfStopped(); + const currentState = detectMail2925ViewState(); + if (currentState.view === 'limit') { + return { + ok: false, + loggedIn: false, + currentView: 'limit', + limitReached: true, + limitMessage: currentState.limitMessage, + }; + } + if (currentState.view === 'mailbox' && !forceLogin) { + return { + ok: true, + loggedIn: true, + currentView: 'mailbox', + }; + } + if (currentState.view === 'login') { + break; + } + await sleep(500); + } + + const loginState = detectMail2925ViewState(); + if (loginState.view === 'mailbox') { + return { + ok: true, + loggedIn: true, + currentView: 'mailbox', + }; + } + if (loginState.view === 'limit') { + return { + ok: false, + loggedIn: false, + currentView: 'limit', + limitReached: true, + limitMessage: loginState.limitMessage, + }; + } + + const emailInput = findMail2925LoginEmailInput(); + const passwordInput = findMail2925LoginPasswordInput(); + const loginButton = findLoginButton(); + if (!emailInput || !passwordInput || !loginButton) { + throw new Error('2925:未识别到可用的登录表单,请确认当前页面处于 2925 登录页。'); + } + if (!email || !password) { + throw new Error('2925:当前账号缺少邮箱或密码,无法自动登录。'); + } + + await ensureAgreementChecked(); + fillInput(emailInput, email); + await sleep(150); + fillInput(passwordInput, password); + await sleep(200); + simulateClick(loginButton); + + const finalState = await waitForMail2925View('mailbox', 60000); + if (finalState.view !== 'mailbox') { + throw new Error('2925:提交账号密码后未进入收件箱。'); + } + + return { + ok: true, + loggedIn: true, + currentView: 'mailbox', + usedCredentials: true, + }; +} + async function handlePollEmail(step, payload) { await ensureSeenCodesSession(step, payload); const { @@ -543,6 +856,9 @@ async function handlePollEmail(step, payload) { strictChatGPTCodeOnly = false, } = payload || {}; const excludedCodeSet = new Set(excludeCodes.filter(Boolean)); + if (typeof throwIfMail2925LimitReached === 'function') { + throwIfMail2925LimitReached(); + } log(`步骤 ${step}:开始轮询 2925 邮箱(最多 ${maxAttempts} 次)`); @@ -562,6 +878,9 @@ async function handlePollEmail(step, payload) { await returnToInbox(); await refreshInbox(); await sleep(2000); + if (typeof throwIfMail2925LimitReached === 'function') { + throwIfMail2925LimitReached(); + } initialItems = findMailItems(); } @@ -572,6 +891,9 @@ async function handlePollEmail(step, payload) { log(`步骤 ${step}:邮件列表已加载,共 ${initialItems.length} 封邮件`); for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + if (typeof throwIfMail2925LimitReached === 'function') { + throwIfMail2925LimitReached(); + } log(`步骤 ${step}:正在轮询 2925 邮箱,第 ${attempt}/${maxAttempts} 次`); if (attempt > 1 || !initialLoadUsedRefresh) { diff --git a/mail2925-utils.js b/mail2925-utils.js new file mode 100644 index 0000000..4dbc799 --- /dev/null +++ b/mail2925-utils.js @@ -0,0 +1,162 @@ +(function mail2925UtilsModule(root, factory) { + if (typeof module !== 'undefined' && module.exports) { + module.exports = factory(); + return; + } + + root.Mail2925Utils = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createMail2925Utils() { + const MAIL2925_LIMIT_COOLDOWN_MS = 24 * 60 * 60 * 1000; + + function normalizeTimestamp(value) { + const numeric = Number(value); + return Number.isFinite(numeric) && numeric > 0 ? numeric : 0; + } + + function normalizeMail2925Account(account = {}) { + return { + id: String(account.id || crypto.randomUUID()), + email: String(account.email || '').trim().toLowerCase(), + password: String(account.password || ''), + enabled: account.enabled !== undefined ? Boolean(account.enabled) : true, + lastUsedAt: normalizeTimestamp(account.lastUsedAt), + lastLoginAt: normalizeTimestamp(account.lastLoginAt), + lastLimitAt: normalizeTimestamp(account.lastLimitAt), + disabledUntil: normalizeTimestamp(account.disabledUntil), + lastError: String(account.lastError || '').trim(), + }; + } + + function normalizeMail2925Accounts(accounts) { + if (!Array.isArray(accounts)) return []; + + const deduped = new Map(); + for (const account of accounts) { + const normalized = normalizeMail2925Account(account); + if (!normalized.email) continue; + deduped.set(normalized.id, normalized); + } + return [...deduped.values()]; + } + + function findMail2925Account(accounts, accountId) { + return normalizeMail2925Accounts(accounts).find((account) => account.id === accountId) || null; + } + + function isMail2925AccountCoolingDown(account, now = Date.now()) { + return normalizeTimestamp(account?.disabledUntil) > normalizeTimestamp(now); + } + + function isMail2925AccountAvailable(account, now = Date.now()) { + return Boolean(account) + && Boolean(account.email) + && Boolean(account.password) + && account.enabled !== false + && !isMail2925AccountCoolingDown(account, now); + } + + function getMail2925AccountStatus(account, now = Date.now()) { + if (!account) return 'missing'; + if (account.enabled === false) return 'disabled'; + if (isMail2925AccountCoolingDown(account, now)) return 'cooldown'; + if (!account.password) return 'pending'; + if (account.lastError) return 'error'; + return 'ready'; + } + + function filterMail2925AccountsByStatus(accounts, mode = 'all', now = Date.now()) { + const list = normalizeMail2925Accounts(accounts); + switch (String(mode || 'all').trim()) { + case 'available': + return list.filter((account) => isMail2925AccountAvailable(account, now)); + case 'cooldown': + return list.filter((account) => isMail2925AccountCoolingDown(account, now)); + case 'disabled': + return list.filter((account) => account.enabled === false); + default: + return list; + } + } + + function pickMail2925AccountForRun(accounts, options = {}) { + const now = normalizeTimestamp(options.now) || Date.now(); + const excludeIds = new Set((options.excludeIds || []).filter(Boolean)); + const candidates = normalizeMail2925Accounts(accounts) + .filter((account) => isMail2925AccountAvailable(account, now)); + if (!candidates.length) return null; + + const filtered = candidates.filter((account) => !excludeIds.has(account.id)); + const pool = filtered.length ? filtered : candidates; + + return pool + .slice() + .sort((left, right) => { + const leftUsedAt = normalizeTimestamp(left.lastUsedAt); + const rightUsedAt = normalizeTimestamp(right.lastUsedAt); + if (leftUsedAt !== rightUsedAt) { + return leftUsedAt - rightUsedAt; + } + return String(left.email || '').localeCompare(String(right.email || '')); + })[0] || null; + } + + function getMail2925BulkActionLabel(mode = 'all', count = 0) { + const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0; + const prefix = mode === 'cooldown' ? '清空冷却' : '全部删除'; + const suffix = normalizedCount > 0 ? `(${normalizedCount})` : ''; + return `${prefix}${suffix}`; + } + + function getMail2925ListToggleLabel(expanded, count = 0) { + const normalizedCount = Number.isFinite(Number(count)) ? Math.max(0, Number(count)) : 0; + const suffix = normalizedCount > 0 ? `(${normalizedCount})` : ''; + return `${expanded ? '收起列表' : '展开列表'}${suffix}`; + } + + function upsertMail2925AccountInList(accounts, nextAccount) { + const list = Array.isArray(accounts) ? accounts.slice() : []; + if (!nextAccount?.id) return list; + + const existingIndex = list.findIndex((account) => account?.id === nextAccount.id); + if (existingIndex === -1) { + list.push(nextAccount); + return list; + } + + list[existingIndex] = nextAccount; + return list; + } + + function parseMail2925ImportText(rawText) { + const lines = String(rawText || '') + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + + return lines + .filter((line, index) => !(index === 0 && /^邮箱----密码$/i.test(line))) + .map((line) => line.split('----').map((part) => part.trim())) + .filter((parts) => parts.length >= 2 && parts[0] && parts[1]) + .map(([email, password]) => ({ + email, + password, + })); + } + + return { + MAIL2925_LIMIT_COOLDOWN_MS, + filterMail2925AccountsByStatus, + findMail2925Account, + getMail2925AccountStatus, + getMail2925BulkActionLabel, + getMail2925ListToggleLabel, + isMail2925AccountAvailable, + isMail2925AccountCoolingDown, + normalizeMail2925Account, + normalizeMail2925Accounts, + normalizeTimestamp, + parseMail2925ImportText, + pickMail2925AccountForRun, + upsertMail2925AccountInList, + }; +}); diff --git a/sidepanel/contribution-mode.js b/sidepanel/contribution-mode.js index 0655a00..0a9d6b0 100644 --- a/sidepanel/contribution-mode.js +++ b/sidepanel/contribution-mode.js @@ -25,7 +25,6 @@ dom.rowSub2ApiGroup, dom.rowSub2ApiDefaultProxy, dom.rowCustomPassword, - dom.rowAccountRunHistoryTextEnabled, dom.rowAccountRunHistoryHelperBaseUrl, ].filter(Boolean); diff --git a/sidepanel/mail-2925-manager.js b/sidepanel/mail-2925-manager.js new file mode 100644 index 0000000..fcb6918 --- /dev/null +++ b/sidepanel/mail-2925-manager.js @@ -0,0 +1,463 @@ +(function attachSidepanelMail2925Manager(globalScope) { + function createMail2925Manager(context = {}) { + const { + state, + dom, + helpers, + runtime, + constants = {}, + mail2925Utils = {}, + } = context; + + const expandedStorageKey = constants.expandedStorageKey || 'multipage-mail2925-list-expanded'; + const displayTimeZone = constants.displayTimeZone || 'Asia/Shanghai'; + const copyIcon = constants.copyIcon || ''; + + let actionInFlight = false; + let listExpanded = false; + + function getMail2925Accounts(currentState = state.getLatestState()) { + return helpers.getMail2925Accounts(currentState); + } + + function getCurrentMail2925AccountId(currentState = state.getLatestState()) { + return String(currentState?.currentMail2925AccountId || ''); + } + + function updateMail2925ListViewport() { + const count = getMail2925Accounts().length; + if (dom.btnDeleteAllMail2925Accounts) { + dom.btnDeleteAllMail2925Accounts.textContent = `全部删除${count > 0 ? `(${count})` : ''}`; + dom.btnDeleteAllMail2925Accounts.disabled = count === 0; + } + if (dom.btnToggleMail2925List) { + const label = typeof mail2925Utils.getMail2925ListToggleLabel === 'function' + ? mail2925Utils.getMail2925ListToggleLabel(listExpanded, count) + : `${listExpanded ? '收起列表' : '展开列表'}${count > 0 ? `(${count})` : ''}`; + dom.btnToggleMail2925List.textContent = label; + dom.btnToggleMail2925List.setAttribute('aria-expanded', String(listExpanded)); + dom.btnToggleMail2925List.disabled = count === 0; + } + if (dom.mail2925ListShell) { + dom.mail2925ListShell.classList.toggle('is-expanded', listExpanded); + dom.mail2925ListShell.classList.toggle('is-collapsed', !listExpanded); + } + } + + function setMail2925ListExpanded(expanded, options = {}) { + const { persist = true } = options; + listExpanded = Boolean(expanded); + updateMail2925ListViewport(); + if (persist) { + localStorage.setItem(expandedStorageKey, listExpanded ? '1' : '0'); + } + } + + function initMail2925ListExpandedState() { + const saved = localStorage.getItem(expandedStorageKey); + setMail2925ListExpanded(saved === '1', { persist: false }); + } + + function formatDateTime(timestamp) { + const value = Number(timestamp); + if (!Number.isFinite(value) || value <= 0) { + return '未记录'; + } + return new Date(value).toLocaleString('zh-CN', { + hour12: false, + timeZone: displayTimeZone, + }); + } + + function getStatusSnapshot(account) { + const status = typeof mail2925Utils.getMail2925AccountStatus === 'function' + ? mail2925Utils.getMail2925AccountStatus(account, Date.now()) + : 'ready'; + switch (status) { + case 'cooldown': + return { label: '冷却中', className: 'status-used' }; + case 'disabled': + return { label: '已禁用', className: 'status-disabled' }; + case 'error': + return { label: '异常', className: 'status-error' }; + case 'pending': + return { label: '待完善', className: 'status-pending' }; + default: + return { label: '可用', className: 'status-authorized' }; + } + } + + function refreshManagedAliasBaseEmail() { + if (typeof helpers.refreshManagedAliasBaseEmail === 'function') { + helpers.refreshManagedAliasBaseEmail(); + } + } + + function applyMail2925AccountMutation(account, options = {}) { + if (!account?.id) return; + const latestState = state.getLatestState(); + const currentId = getCurrentMail2925AccountId(latestState); + const nextAccounts = typeof mail2925Utils.upsertMail2925AccountInList === 'function' + ? mail2925Utils.upsertMail2925AccountInList(getMail2925Accounts(latestState), account) + : getMail2925Accounts(latestState).map((item) => (item.id === account.id ? account : item)); + + const nextState = { + mail2925Accounts: nextAccounts, + }; + if (currentId === account.id && account.enabled === false) { + nextState.currentMail2925AccountId = null; + } + state.syncLatestState(nextState); + refreshManagedAliasBaseEmail(); + renderMail2925Accounts(); + } + + function clearMail2925Form() { + if (dom.inputMail2925Email) dom.inputMail2925Email.value = ''; + if (dom.inputMail2925Password) dom.inputMail2925Password.value = ''; + } + + function renderMail2925Accounts() { + if (!dom.mail2925AccountsList) return; + + const latestState = state.getLatestState(); + const accounts = getMail2925Accounts(latestState); + const currentId = getCurrentMail2925AccountId(latestState); + + if (!accounts.length) { + dom.mail2925AccountsList.innerHTML = '
还没有 2925 账号,先添加一条再使用。
'; + updateMail2925ListViewport(); + return; + } + + dom.mail2925AccountsList.innerHTML = accounts.map((account) => { + const status = getStatusSnapshot(account); + const coolingDown = status.label === '冷却中'; + return ` + + `; + }).join(''); + + updateMail2925ListViewport(); + } + + async function handleAddMail2925Account() { + if (actionInFlight) return; + + const email = String(dom.inputMail2925Email?.value || '').trim(); + const password = String(dom.inputMail2925Password?.value || ''); + if (!email) { + helpers.showToast('请先填写 2925 邮箱。', 'warn'); + return; + } + if (!password) { + helpers.showToast('请先填写 2925 密码。', 'warn'); + return; + } + + actionInFlight = true; + if (dom.btnAddMail2925Account) { + dom.btnAddMail2925Account.disabled = true; + } + + try { + const response = await runtime.sendMessage({ + type: 'UPSERT_MAIL2925_ACCOUNT', + source: 'sidepanel', + payload: { + email, + password, + }, + }); + if (response?.error) { + throw new Error(response.error); + } + + applyMail2925AccountMutation(response.account); + clearMail2925Form(); + helpers.showToast(`已保存 2925 账号 ${email}`, 'success', 1800); + } catch (err) { + helpers.showToast(`保存 2925 账号失败:${err.message}`, 'error'); + } finally { + actionInFlight = false; + if (dom.btnAddMail2925Account) { + dom.btnAddMail2925Account.disabled = false; + } + } + } + + async function handleImportMail2925Accounts() { + if (actionInFlight) return; + if (typeof mail2925Utils.parseMail2925ImportText !== 'function') { + helpers.showToast('2925 导入解析器未加载,请刷新扩展后重试。', 'error'); + return; + } + + const rawText = String(dom.inputMail2925Import?.value || '').trim(); + if (!rawText) { + helpers.showToast('请先粘贴 2925 账号导入内容。', 'warn'); + return; + } + + const parsedAccounts = mail2925Utils.parseMail2925ImportText(rawText); + if (!parsedAccounts.length) { + helpers.showToast('没有解析到有效账号,请检查格式是否为 邮箱----密码。', 'error'); + return; + } + + actionInFlight = true; + if (dom.btnImportMail2925Accounts) { + dom.btnImportMail2925Accounts.disabled = true; + } + + try { + for (const account of parsedAccounts) { + const response = await runtime.sendMessage({ + type: 'UPSERT_MAIL2925_ACCOUNT', + source: 'sidepanel', + payload: account, + }); + if (response?.error) { + throw new Error(response.error); + } + } + + if (dom.inputMail2925Import) { + dom.inputMail2925Import.value = ''; + } + helpers.showToast(`已导入 ${parsedAccounts.length} 条 2925 账号`, 'success', 2200); + } catch (err) { + helpers.showToast(`批量导入 2925 账号失败:${err.message}`, 'error'); + } finally { + actionInFlight = false; + if (dom.btnImportMail2925Accounts) { + dom.btnImportMail2925Accounts.disabled = false; + } + } + } + + async function deleteAllMail2925Accounts() { + const accounts = getMail2925Accounts(); + if (!accounts.length) { + helpers.showToast('没有可删除的 2925 账号。', 'warn'); + return; + } + + const confirmed = await helpers.openConfirmModal({ + title: '全部删除 2925 账号', + message: `确认删除当前全部 ${accounts.length} 个 2925 账号吗?`, + confirmLabel: '确认全部删除', + confirmVariant: 'btn-danger', + }); + if (!confirmed) { + return; + } + + const response = await runtime.sendMessage({ + type: 'DELETE_MAIL2925_ACCOUNTS', + source: 'sidepanel', + payload: { mode: 'all' }, + }); + if (response?.error) { + throw new Error(response.error); + } + + state.syncLatestState({ + mail2925Accounts: [], + currentMail2925AccountId: null, + }); + refreshManagedAliasBaseEmail(); + renderMail2925Accounts(); + helpers.showToast(`已删除全部 ${response.deletedCount || 0} 个 2925 账号`, 'success', 2200); + } + + async function handleAccountListClick(event) { + const actionButton = event.target.closest('[data-account-action]'); + if (!actionButton || actionInFlight) { + return; + } + + const accountId = String(actionButton.dataset.accountId || ''); + const action = String(actionButton.dataset.accountAction || ''); + if (!accountId || !action) { + return; + } + + const targetAccount = getMail2925Accounts().find((account) => account.id === accountId) || null; + actionInFlight = true; + actionButton.disabled = true; + + try { + if (action === 'copy-email') { + if (!targetAccount?.email) throw new Error('未找到可复制的 2925 邮箱。'); + await helpers.copyTextToClipboard(targetAccount.email); + helpers.showToast(`已复制 ${targetAccount.email}`, 'success', 1800); + return; + } + + if (action === 'select') { + const response = await runtime.sendMessage({ + type: 'SELECT_MAIL2925_ACCOUNT', + source: 'sidepanel', + payload: { accountId }, + }); + if (response?.error) throw new Error(response.error); + state.syncLatestState({ currentMail2925AccountId: response.account.id }); + refreshManagedAliasBaseEmail(); + renderMail2925Accounts(); + helpers.showToast(`已切换当前 2925 账号为 ${response.account.email}`, 'success', 2000); + return; + } + + if (action === 'login') { + const response = await runtime.sendMessage({ + type: 'LOGIN_MAIL2925_ACCOUNT', + source: 'sidepanel', + payload: { + accountId, + forceRelogin: true, + }, + }); + if (response?.error) throw new Error(response.error); + state.syncLatestState({ currentMail2925AccountId: response.account.id }); + refreshManagedAliasBaseEmail(); + renderMail2925Accounts(); + helpers.showToast(`已使用 ${response.account.email} 登录 2925 邮箱`, 'success', 2200); + return; + } + + if (action === 'toggle-enabled') { + if (!targetAccount) throw new Error('未找到目标 2925 账号。'); + const response = await runtime.sendMessage({ + type: 'PATCH_MAIL2925_ACCOUNT', + source: 'sidepanel', + payload: { + accountId, + updates: { + enabled: targetAccount.enabled === false, + }, + }, + }); + if (response?.error) throw new Error(response.error); + applyMail2925AccountMutation(response.account); + helpers.showToast(`2925 账号 ${response.account.email} 已${response.account.enabled === false ? '禁用' : '启用'}`, 'success', 2200); + return; + } + + if (action === 'clear-cooldown') { + const response = await runtime.sendMessage({ + type: 'PATCH_MAIL2925_ACCOUNT', + source: 'sidepanel', + payload: { + accountId, + updates: { + disabledUntil: 0, + lastError: '', + }, + }, + }); + if (response?.error) throw new Error(response.error); + applyMail2925AccountMutation(response.account); + helpers.showToast(`2925 账号 ${response.account.email} 已清除冷却`, 'success', 2200); + return; + } + + if (action === 'delete') { + const confirmed = await helpers.openConfirmModal({ + title: '删除 2925 账号', + message: '确认删除这个 2925 账号吗?', + confirmLabel: '确认删除', + confirmVariant: 'btn-danger', + }); + if (!confirmed) { + return; + } + const response = await runtime.sendMessage({ + type: 'DELETE_MAIL2925_ACCOUNT', + source: 'sidepanel', + payload: { accountId }, + }); + if (response?.error) throw new Error(response.error); + + const nextAccounts = getMail2925Accounts().filter((account) => account.id !== accountId); + const nextState = { mail2925Accounts: nextAccounts }; + if (getCurrentMail2925AccountId() === accountId) { + nextState.currentMail2925AccountId = null; + } + state.syncLatestState(nextState); + refreshManagedAliasBaseEmail(); + renderMail2925Accounts(); + helpers.showToast('2925 账号已删除', 'success', 1800); + } + } catch (err) { + helpers.showToast(err.message, 'error'); + } finally { + actionInFlight = false; + actionButton.disabled = false; + } + } + + function bindMail2925Events() { + dom.btnToggleMail2925List?.addEventListener('click', () => { + setMail2925ListExpanded(!listExpanded); + }); + + dom.btnDeleteAllMail2925Accounts?.addEventListener('click', async () => { + if (actionInFlight) return; + actionInFlight = true; + try { + await deleteAllMail2925Accounts(); + } catch (err) { + helpers.showToast(err.message, 'error'); + } finally { + actionInFlight = false; + updateMail2925ListViewport(); + } + }); + + dom.btnAddMail2925Account?.addEventListener('click', handleAddMail2925Account); + dom.btnImportMail2925Accounts?.addEventListener('click', handleImportMail2925Accounts); + dom.mail2925AccountsList?.addEventListener('click', handleAccountListClick); + } + + return { + bindMail2925Events, + initMail2925ListExpandedState, + renderMail2925Accounts, + }; + } + + globalScope.SidepanelMail2925Manager = { + createMail2925Manager, + }; +})(window); diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 938b08e..09e8a71 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -697,6 +697,21 @@ header { min-width: 0; } +.mail2925-base-inline { + gap: 10px; +} + +.mail2925-base-input, +.mail2925-pool-select { + flex: 1; + min-width: 0; +} + +.mail2925-pool-toggle { + flex: 0 0 auto; + white-space: nowrap; +} + .data-value-actions { justify-content: space-between; } @@ -1395,36 +1410,51 @@ header { flex-shrink: 0; } -.fallback-inline { - justify-content: space-between; +.setting-pair { + display: flex; align-items: center; + justify-content: space-between; + gap: 12px; + flex: 1; + min-width: 0; + flex-wrap: nowrap; } -.fallback-thread-interval { - margin-left: auto; -} - -.timing-field { +.setting-group { display: flex; align-items: center; gap: 12px; min-width: 0; } -.timing-field-labeled { - min-width: 196px; - justify-content: flex-end; +.setting-group-primary { + flex: 1; + min-width: 112px; } -.timing-field-right { +.setting-group-secondary { margin-left: auto; -} - -.setting-inline-right { min-width: 196px; justify-content: flex-end; } +.setting-caption { + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + flex: 0 0 auto; + white-space: nowrap; + min-width: 56px; + text-align: right; +} + +.setting-controls { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + .toggle-switch { position: relative; display: inline-flex; @@ -1482,51 +1512,6 @@ header { opacity: 0.55; } -.auto-delay-inline { - justify-content: space-between; - align-items: center; - gap: 12px; - flex-wrap: nowrap; -} - -.auto-delay-side { - display: flex; - align-items: center; - gap: 12px; - flex: 0 0 auto; - min-width: 0; -} - -.auto-delay-side-right { - flex: 0 0 auto; -} - -.auto-delay-check { - flex: 0 0 auto; - align-items: center; -} - -.auto-delay-check span { - white-space: nowrap; -} - -.auto-delay-caption { - font-size: 12px; - font-weight: 600; - color: var(--text-muted); - flex: 0 0 auto; - white-space: nowrap; - min-width: 56px; - text-align: right; -} - -.auto-delay-controls { - display: flex; - align-items: center; - gap: 8px; - flex-shrink: 0; -} - .auto-delay-input { width: 72px; flex: 0 0 auto; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 5f73343..1127659 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -261,7 +261,19 @@
延迟 -
-
+
+
-
+
分钟
-
- 步间间隔 -
+
+ 步间间隔 +
@@ -306,16 +318,18 @@
自动重试 -
- -
- 线程间隔 -
+
+
+ +
+
+ 线程间隔 +
分钟 @@ -325,21 +339,18 @@
本地同步 -
- -
-
-
- 验证码 -
-
- 验证码重发 -
+
+
+ +
+
+ 验证码重发 +
@@ -424,6 +435,40 @@
+