2925邮箱增加账号池功能

This commit is contained in:
QLHazyCoder
2026-04-20 18:56:08 +08:00
parent 8dd6daccd1
commit 6783cf02b4
25 changed files with 2630 additions and 108 deletions
+32
View File
@@ -53,6 +53,7 @@
- 自动显示当前使用中的密码,便于后续保存 - 自动显示当前使用中的密码,便于后续保存
- 自动获取注册验证码与登录验证码 - 自动获取注册验证码与登录验证码
- 支持 `Hotmail`:继续使用 `邮箱 + 客户端 ID + 刷新令牌(refresh token`,并可在远程服务与本地助手两种模式间切换 - 支持 `Hotmail`:继续使用 `邮箱 + 客户端 ID + 刷新令牌(refresh token`,并可在远程服务与本地助手两种模式间切换
- 支持 `2925`:新增多账号池、自动登录登出、Step 4 / Step 8 命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号
- 支持 `QQ Mail``163 Mail``Inbucket mailbox` - 支持 `QQ Mail``163 Mail``Inbucket mailbox`
- 支持从 DuckDuckGo Email Protection 自动生成新的 `@duck.com` 地址 - 支持从 DuckDuckGo Email Protection 自动生成新的 `@duck.com` 地址
- 支持基于 Cloudflare 自定义域名自动生成随机邮箱前缀 - 支持基于 Cloudflare 自定义域名自动生成随机邮箱前缀
@@ -138,6 +139,17 @@
4. 通过后再执行步骤或 `Auto` 4. 通过后再执行步骤或 `Auto`
5. 当前项目中,`Mail = Hotmail` 时会直接使用账号池里的邮箱作为注册邮箱,不再走 `Duck / Cloudflare` 自动生成 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` ### `CPA`
@@ -166,6 +178,7 @@ Step 1 和 Step 10 都依赖这个地址。
支持五种验证码来源: 支持五种验证码来源:
- `Hotmail` - `Hotmail`
- `2925`
- `163 Mail` - `163 Mail`
- `163 VIP Mail` - `163 VIP Mail`
- `QQ Mail` - `QQ Mail`
@@ -174,6 +187,7 @@ Step 1 和 Step 10 都依赖这个地址。
说明: 说明:
- `Hotmail` 通过侧边栏里的 Hotmail 账号池选择账号,可切换为远程服务模式或本地助手模式 - `Hotmail` 通过侧边栏里的 Hotmail 账号池选择账号,可切换为远程服务模式或本地助手模式
- `2925` 通过侧边栏里的 2925 账号池选择账号,并在 Step 4 / Step 8 前自动校验网页邮箱登录态
- `QQ``163``163 VIP` 用于直接轮询网页邮箱 - `QQ``163``163 VIP` 用于直接轮询网页邮箱
- `Inbucket` 通过你在侧边栏里配置的 host 访问 `mailbox` 页面:`https://<your-inbucket-host>/m/<mailbox>/` - `Inbucket` 通过你在侧边栏里配置的 host 访问 `mailbox` 页面:`https://<your-inbucket-host>/m/<mailbox>/`
@@ -207,6 +221,24 @@ Step 1 和 Step 10 都依赖这个地址。
- 校验通过后,可点击 `测试收信` - 校验通过后,可点击 `测试收信`
- Auto 模式每轮会自动选用一个可用账号 - Auto 模式每轮会自动选用一个可用账号
### `2925 账号池`
仅当 `Mail = 2925` 时使用。
每条账号支持保存:
- `email`
- `password`
使用方式:
- 添加一个或多个 `2925.com` 账号
- 点击 `使用此账号` 可以切换当前别名基邮箱来源
- 点击 `登录` 可以让扩展直接打开 / 复用 2925 邮箱标签页,并使用当前账号自动登录
- 如果账号因为“子邮箱已达上限邮箱”被临时禁用,列表里会展示上限记录时间和恢复时间
- 命中冷却的账号可手动 `清冷却`,也可以等待 24 小时后自动恢复可用
- Auto 模式在生成 2925 别名邮箱前,会自动分配当前可用账号;若 Step 4 / Step 8 命中上限,会自动切到下一个账号
#### 本地 helper 启动命令 #### 本地 helper 启动命令
Windows Windows
+135 -2
View File
@@ -2,8 +2,10 @@
importScripts( importScripts(
'managed-alias-utils.js', 'managed-alias-utils.js',
'mail2925-utils.js',
'background/account-run-history.js', 'background/account-run-history.js',
'background/contribution-oauth.js', 'background/contribution-oauth.js',
'background/mail-2925-session.js',
'background/panel-bridge.js', 'background/panel-bridge.js',
'background/generated-email-helpers.js', 'background/generated-email-helpers.js',
'background/signup-flow-helpers.js', 'background/signup-flow-helpers.js',
@@ -57,6 +59,16 @@ const {
pickVerificationMessageWithTimeFallback, pickVerificationMessageWithTimeFallback,
shouldClearHotmailCurrentSelection, shouldClearHotmailCurrentSelection,
} = self.HotmailUtils; } = self.HotmailUtils;
const {
MAIL2925_LIMIT_COOLDOWN_MS,
findMail2925Account,
getMail2925AccountStatus,
normalizeMail2925Account,
normalizeMail2925Accounts,
parseMail2925ImportText,
pickMail2925AccountForRun,
upsertMail2925AccountInList,
} = self.Mail2925Utils;
const { const {
fetchMicrosoftMailboxMessages, fetchMicrosoftMailboxMessages,
} = self.MultiPageMicrosoftEmail; } = self.MultiPageMicrosoftEmail;
@@ -248,6 +260,7 @@ const PERSISTED_SETTING_DEFAULTS = {
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT, verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
mailProvider: '163', mailProvider: '163',
mail2925Mode: DEFAULT_MAIL_2925_MODE, mail2925Mode: DEFAULT_MAIL_2925_MODE,
mail2925UseAccountPool: false,
emailGenerator: 'duck', emailGenerator: 'duck',
autoDeleteUsedIcloudAlias: false, autoDeleteUsedIcloudAlias: false,
icloudHostPreference: 'auto', icloudHostPreference: 'auto',
@@ -270,6 +283,7 @@ const PERSISTED_SETTING_DEFAULTS = {
cloudflareTempEmailDomain: '', cloudflareTempEmailDomain: '',
cloudflareTempEmailDomains: [], cloudflareTempEmailDomains: [],
hotmailAccounts: [], hotmailAccounts: [],
mail2925Accounts: [],
}; };
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS); const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
@@ -344,6 +358,7 @@ const DEFAULT_STATE = {
oauthFlowDeadlineAt: null, oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null, oauthFlowDeadlineSourceUrl: null,
currentHotmailAccountId: null, currentHotmailAccountId: null,
currentMail2925AccountId: null,
preferredIcloudHost: '', preferredIcloudHost: '',
}; };
@@ -874,6 +889,8 @@ function normalizePersistentSettingValue(key, value) {
return normalizeMailProvider(value); return normalizeMailProvider(value);
case 'mail2925Mode': case 'mail2925Mode':
return normalizeMail2925Mode(value); return normalizeMail2925Mode(value);
case 'mail2925UseAccountPool':
return Boolean(value);
case 'emailGenerator': case 'emailGenerator':
return normalizeEmailGenerator(value); return normalizeEmailGenerator(value);
case 'autoDeleteUsedIcloudAlias': case 'autoDeleteUsedIcloudAlias':
@@ -914,6 +931,8 @@ function normalizePersistentSettingValue(key, value) {
return normalizeCloudflareTempEmailDomains(value); return normalizeCloudflareTempEmailDomains(value);
case 'hotmailAccounts': case 'hotmailAccounts':
return normalizeHotmailAccounts(value); return normalizeHotmailAccounts(value);
case 'mail2925Accounts':
return normalizeMail2925Accounts(value);
default: default:
return value; return value;
} }
@@ -2240,6 +2259,12 @@ function getManagedAliasBaseEmail(state = {}, provider = state?.mailProvider) {
} }
if (normalizedProvider === '2925') { if (normalizedProvider === '2925') {
const currentAccount = Boolean(state?.mail2925UseAccountPool)
? getCurrentMail2925Account(state)
: null;
if (currentAccount?.email) {
return currentAccount.email;
}
const mail2925BaseEmail = String(state?.mail2925BaseEmail || '').trim(); const mail2925BaseEmail = String(state?.mail2925BaseEmail || '').trim();
if (mail2925BaseEmail) { if (mail2925BaseEmail) {
return mail2925BaseEmail; return mail2925BaseEmail;
@@ -5355,6 +5380,79 @@ function getEmailGeneratorLabel(generator) {
if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email'; if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email';
return 'Duck 邮箱'; 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({ const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGeneratedEmailHelpers({
addLog, addLog,
buildGeneratedAliasEmail, buildGeneratedAliasEmail,
@@ -5366,6 +5464,7 @@ const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGenerat
getCloudflareTempEmailAddressFromResponse, getCloudflareTempEmailAddressFromResponse,
getCloudflareTempEmailConfig, getCloudflareTempEmailConfig,
getState, getState,
ensureMail2925AccountForFlow,
joinCloudflareTempEmailUrl, joinCloudflareTempEmailUrl,
normalizeCloudflareDomain, normalizeCloudflareDomain,
normalizeCloudflareTempEmailAddress, normalizeCloudflareTempEmailAddress,
@@ -5690,8 +5789,25 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
return currentState.email; return currentState.email;
} }
const baseEmail = getManagedAliasBaseEmail(currentState); let managedAliasState = currentState;
if (!baseEmail && !currentState.email) { 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 基邮箱'; const baseLabel = currentState.mailProvider === GMAIL_PROVIDER ? 'Gmail 原邮箱' : '2925 基邮箱';
throw new Error(`${baseLabel}未设置,请先填写,或直接在“注册邮箱”中手动填写完整邮箱。`); throw new Error(`${baseLabel}未设置,请先填写,或直接在“注册邮箱”中手动填写完整邮箱。`);
} }
@@ -5830,6 +5946,10 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
if (isSignupUserAlreadyExistsFailure(err)) { if (isSignupUserAlreadyExistsFailure(err)) {
throw err; throw err;
} }
if (isMail2925ThreadTerminatedError(err)) {
await addLog(`步骤 4:2925 已切换账号并要求结束当前尝试:${getErrorMessage(err)}`, 'warn');
throw err;
}
step4RestartCount += 1; step4RestartCount += 1;
const preservedState = await getState(); const preservedState = await getState();
const preservedEmail = String(preservedState.email || '').trim(); const preservedEmail = String(preservedState.email || '').trim();
@@ -6019,6 +6139,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
chrome, chrome,
ensureContentScriptReadyOnTab, ensureContentScriptReadyOnTab,
ensureHotmailAccountForFlow, ensureHotmailAccountForFlow,
ensureMail2925AccountForFlow,
ensureLuckmailPurchaseForFlow, ensureLuckmailPurchaseForFlow,
getTabId, getTabId,
isGeneratedAliasProvider, isGeneratedAliasProvider,
@@ -6046,9 +6167,11 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create
}), }),
getHotmailVerificationPollConfig, getHotmailVerificationPollConfig,
getHotmailVerificationRequestTimestamp, getHotmailVerificationRequestTimestamp,
handleMail2925LimitReachedError,
getState, getState,
getTabId, getTabId,
HOTMAIL_PROVIDER, HOTMAIL_PROVIDER,
isMail2925LimitReachedError,
isStopError, isStopError,
LUCKMAIL_PROVIDER, LUCKMAIL_PROVIDER,
MAIL_2925_VERIFICATION_INTERVAL_MS, MAIL_2925_VERIFICATION_INTERVAL_MS,
@@ -6099,6 +6222,7 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({
chrome, chrome,
completeStepFromBackground, completeStepFromBackground,
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
ensureMail2925MailboxSession,
getMailConfig, getMailConfig,
getTabId, getTabId,
HOTMAIL_PROVIDER, HOTMAIL_PROVIDER,
@@ -6144,6 +6268,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
chrome, chrome,
CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER,
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
ensureMail2925MailboxSession,
ensureStep8VerificationPageReady, ensureStep8VerificationPageReady,
getOAuthFlowRemainingMs, getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs, getOAuthFlowStepTimeoutMs,
@@ -6219,6 +6344,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
deleteUsedIcloudAliases, deleteUsedIcloudAliases,
disableUsedLuckmailPurchases, disableUsedLuckmailPurchases,
doesStepUseCompletionSignal, doesStepUseCompletionSignal,
ensureMail2925MailboxSession,
ensureManualInteractionAllowed, ensureManualInteractionAllowed,
executeStep, executeStep,
executeStepViaCompletionSignal, executeStepViaCompletionSignal,
@@ -6254,12 +6380,15 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
launchAutoRunTimerPlan, launchAutoRunTimerPlan,
listIcloudAliases, listIcloudAliases,
listLuckmailPurchasesForManagement, listLuckmailPurchasesForManagement,
getCurrentMail2925Account,
normalizeHotmailAccounts, normalizeHotmailAccounts,
normalizeMail2925Accounts,
normalizeRunCount, normalizeRunCount,
AUTO_RUN_TIMER_KIND_SCHEDULED_START, AUTO_RUN_TIMER_KIND_SCHEDULED_START,
notifyStepComplete, notifyStepComplete,
notifyStepError, notifyStepError,
patchHotmailAccount, patchHotmailAccount,
patchMail2925Account,
registerTab, registerTab,
requestStop, requestStop,
resetState, resetState,
@@ -6267,6 +6396,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
scheduleAutoRun, scheduleAutoRun,
selectLuckmailPurchase, selectLuckmailPurchase,
setCurrentHotmailAccount, setCurrentHotmailAccount,
setCurrentMail2925Account,
setContributionMode, setContributionMode,
setEmailState, setEmailState,
setEmailStateSilently, setEmailStateSilently,
@@ -6284,7 +6414,10 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
startAutoRunLoop, startAutoRunLoop,
pollContributionStatus: (...args) => contributionOAuthManager?.pollContributionStatus?.(...args), pollContributionStatus: (...args) => contributionOAuthManager?.pollContributionStatus?.(...args),
syncHotmailAccounts, syncHotmailAccounts,
deleteMail2925Account,
deleteMail2925Accounts,
testHotmailAccountMailAccess, testHotmailAccountMailAccess,
upsertMail2925Account,
upsertHotmailAccount, upsertHotmailAccount,
verifyHotmailAccount, verifyHotmailAccount,
}); });
+1
View File
@@ -374,6 +374,7 @@
emailGenerator: prevState.emailGenerator, emailGenerator: prevState.emailGenerator,
gmailBaseEmail: prevState.gmailBaseEmail, gmailBaseEmail: prevState.gmailBaseEmail,
mail2925BaseEmail: prevState.mail2925BaseEmail, mail2925BaseEmail: prevState.mail2925BaseEmail,
currentMail2925AccountId: prevState.currentMail2925AccountId,
emailPrefix: prevState.emailPrefix, emailPrefix: prevState.emailPrefix,
inbucketHost: prevState.inbucketHost, inbucketHost: prevState.inbucketHost,
inbucketMailbox: prevState.inbucketMailbox, inbucketMailbox: prevState.inbucketMailbox,
+18 -1
View File
@@ -13,6 +13,7 @@
getCloudflareTempEmailAddressFromResponse, getCloudflareTempEmailAddressFromResponse,
getCloudflareTempEmailConfig, getCloudflareTempEmailConfig,
getState, getState,
ensureMail2925AccountForFlow,
joinCloudflareTempEmailUrl, joinCloudflareTempEmailUrl,
normalizeCloudflareDomain, normalizeCloudflareDomain,
normalizeCloudflareTempEmailAddress, normalizeCloudflareTempEmailAddress,
@@ -190,7 +191,7 @@
async function fetchManagedAliasEmail(state, options = {}) { async function fetchManagedAliasEmail(state, options = {}) {
throwIfStopped(); throwIfStopped();
const provider = String(options.mailProvider || state?.mailProvider || '').trim().toLowerCase(); const provider = String(options.mailProvider || state?.mailProvider || '').trim().toLowerCase();
const mergedState = { let mergedState = {
...(state || {}), ...(state || {}),
mailProvider: provider, mailProvider: provider,
}; };
@@ -200,6 +201,22 @@
if (options.mail2925BaseEmail !== undefined) { if (options.mail2925BaseEmail !== undefined) {
mergedState.mail2925BaseEmail = String(options.mail2925BaseEmail || '').trim(); 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); const email = buildGeneratedAliasEmail(mergedState);
await setEmailState(email); await setEmailState(email);
+469
View File
@@ -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,
};
});
+61
View File
@@ -25,6 +25,7 @@
deleteUsedIcloudAliases, deleteUsedIcloudAliases,
disableUsedLuckmailPurchases, disableUsedLuckmailPurchases,
doesStepUseCompletionSignal, doesStepUseCompletionSignal,
ensureMail2925MailboxSession,
ensureManualInteractionAllowed, ensureManualInteractionAllowed,
executeStep, executeStep,
executeStepViaCompletionSignal, executeStepViaCompletionSignal,
@@ -35,6 +36,7 @@
findHotmailAccount, findHotmailAccount,
flushCommand, flushCommand,
getCurrentLuckmailPurchase, getCurrentLuckmailPurchase,
getCurrentMail2925Account,
getPendingAutoRunTimerPlan, getPendingAutoRunTimerPlan,
getSourceLabel, getSourceLabel,
getState, getState,
@@ -52,10 +54,12 @@
listIcloudAliases, listIcloudAliases,
listLuckmailPurchasesForManagement, listLuckmailPurchasesForManagement,
normalizeHotmailAccounts, normalizeHotmailAccounts,
normalizeMail2925Accounts,
normalizeRunCount, normalizeRunCount,
AUTO_RUN_TIMER_KIND_SCHEDULED_START, AUTO_RUN_TIMER_KIND_SCHEDULED_START,
notifyStepComplete, notifyStepComplete,
notifyStepError, notifyStepError,
patchMail2925Account,
patchHotmailAccount, patchHotmailAccount,
pollContributionStatus, pollContributionStatus,
registerTab, registerTab,
@@ -65,6 +69,7 @@
resumeAutoRun, resumeAutoRun,
scheduleAutoRun, scheduleAutoRun,
selectLuckmailPurchase, selectLuckmailPurchase,
setCurrentMail2925Account,
setCurrentHotmailAccount, setCurrentHotmailAccount,
setContributionMode, setContributionMode,
setEmailState, setEmailState,
@@ -81,8 +86,11 @@
skipStep, skipStep,
startContributionFlow, startContributionFlow,
startAutoRunLoop, startAutoRunLoop,
deleteMail2925Account,
deleteMail2925Accounts,
syncHotmailAccounts, syncHotmailAccounts,
testHotmailAccountMailAccess, testHotmailAccountMailAccess,
upsertMail2925Account,
upsertHotmailAccount, upsertHotmailAccount,
verifyHotmailAccount, verifyHotmailAccount,
} = deps; } = deps;
@@ -178,6 +186,13 @@
}); });
await addLog('当前 Hotmail 账号已自动标记为已用。', 'ok'); 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)) { if (isLuckmailProvider(latestState)) {
const currentPurchase = getCurrentLuckmailPurchase(latestState); const currentPurchase = getCurrentLuckmailPurchase(latestState);
if (currentPurchase?.id) { if (currentPurchase?.id) {
@@ -578,6 +593,52 @@
return { ok: true, ...result }; 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': { case 'LIST_LUCKMAIL_PURCHASES': {
const purchases = await listLuckmailPurchasesForManagement(); const purchases = await listLuckmailPurchasesForManagement();
return { ok: true, purchases }; return { ok: true, purchases };
+10
View File
@@ -7,6 +7,7 @@
chrome, chrome,
ensureContentScriptReadyOnTab, ensureContentScriptReadyOnTab,
ensureHotmailAccountForFlow, ensureHotmailAccountForFlow,
ensureMail2925AccountForFlow,
ensureLuckmailPurchaseForFlow, ensureLuckmailPurchaseForFlow,
isGeneratedAliasProvider, isGeneratedAliasProvider,
isReusableGeneratedAliasEmail, isReusableGeneratedAliasEmail,
@@ -198,6 +199,15 @@
const purchase = await ensureLuckmailPurchaseForFlow({ allowReuse: true }); const purchase = await ensureLuckmailPurchaseForFlow({ allowReuse: true });
resolvedEmail = purchase.email_address; resolvedEmail = purchase.email_address;
} else if (isGeneratedAliasProvider(state)) { } 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)) { if (!isReusableGeneratedAliasEmail?.(state, resolvedEmail)) {
resolvedEmail = buildGeneratedAliasEmail(state); resolvedEmail = buildGeneratedAliasEmail(state);
} }
+9
View File
@@ -7,6 +7,7 @@
chrome, chrome,
CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER,
confirmCustomVerificationStepBypass, confirmCustomVerificationStepBypass,
ensureMail2925MailboxSession,
ensureStep8VerificationPageReady, ensureStep8VerificationPageReady,
getOAuthFlowRemainingMs, getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs, getOAuthFlowStepTimeoutMs,
@@ -105,6 +106,14 @@
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
) { ) {
await addLog(`步骤 8:正在通过 ${mail.label} 轮询验证码...`); 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 { } else {
await addLog(`步骤 8:正在打开${mail.label}...`); await addLog(`步骤 8:正在打开${mail.label}...`);
+9
View File
@@ -7,6 +7,7 @@
chrome, chrome,
completeStepFromBackground, completeStepFromBackground,
confirmCustomVerificationStepBypass, confirmCustomVerificationStepBypass,
ensureMail2925MailboxSession,
getMailConfig, getMailConfig,
getTabId, getTabId,
HOTMAIL_PROVIDER, HOTMAIL_PROVIDER,
@@ -69,6 +70,14 @@
throwIfStopped(); throwIfStopped();
if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); 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 { } else {
await addLog(`步骤 4:正在打开${mail.label}...`); await addLog(`步骤 4:正在打开${mail.label}...`);
+16
View File
@@ -10,9 +10,11 @@
confirmCustomVerificationStepBypassRequest, confirmCustomVerificationStepBypassRequest,
getHotmailVerificationPollConfig, getHotmailVerificationPollConfig,
getHotmailVerificationRequestTimestamp, getHotmailVerificationRequestTimestamp,
handleMail2925LimitReachedError,
getState, getState,
getTabId, getTabId,
HOTMAIL_PROVIDER, HOTMAIL_PROVIDER,
isMail2925LimitReachedError,
isStopError, isStopError,
LUCKMAIL_PROVIDER, LUCKMAIL_PROVIDER,
MAIL_2925_VERIFICATION_INTERVAL_MS, MAIL_2925_VERIFICATION_INTERVAL_MS,
@@ -417,6 +419,13 @@
if (isStopError(err)) { if (isStopError(err)) {
throw 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; lastError = err;
await addLog(`步骤 ${step}${err.message}`, 'warn'); await addLog(`步骤 ${step}${err.message}`, 'warn');
} }
@@ -554,6 +563,13 @@
if (isStopError(err)) { if (isStopError(err)) {
throw 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; lastError = err;
await addLog(`步骤 ${step}${err.message}`, 'warn'); await addLog(`步骤 ${step}${err.message}`, 'warn');
if (round < maxRounds) { if (round < maxRounds) {
+322
View File
@@ -81,6 +81,16 @@ async function ensureSeenCodesSession(step, payload = {}) {
} }
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 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') { if (message.type === 'POLL_EMAIL') {
resetStopState(); resetStopState();
handlePollEmail(message.step, message.payload).then((result) => { handlePollEmail(message.step, message.payload).then((result) => {
@@ -150,11 +160,70 @@ const MAIL_SELECT_ALL_SELECTORS = [
'[class*="checkbox"]', '[class*="checkbox"]',
]; ];
const MAIL_ACTION_CANDIDATE_SELECTORS = 'button, [role="button"], a, label, span, div'; 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) { function normalizeNodeText(value) {
return String(value || '').replace(/\s+/g, ' ').trim(); 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) { function isVisibleNode(node) {
if (!node) return false; if (!node) return false;
if (node.hidden) return false; if (node.hidden) return false;
@@ -208,6 +277,19 @@ function findActionBySelectors(selectors = []) {
return null; 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 = []) { function findToolbarActionButton(patterns = [], selectors = []) {
const directMatch = findActionBySelectors(selectors); const directMatch = findActionBySelectors(selectors);
if (directMatch) { if (directMatch) {
@@ -231,6 +313,29 @@ function findToolbarActionButton(patterns = [], selectors = []) {
return null; 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() { function findRefreshButton() {
return findToolbarActionButton([ return findToolbarActionButton([
/刷新/i, /刷新/i,
@@ -253,6 +358,117 @@ function findSelectAllControl() {
return findActionBySelectors(MAIL_SELECT_ALL_SELECTORS); 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) { function isCheckboxChecked(node) {
const checkbox = node?.matches?.('input[type="checkbox"], [role="checkbox"]') const checkbox = node?.matches?.('input[type="checkbox"], [role="checkbox"]')
? node ? node
@@ -518,6 +734,9 @@ async function deleteAllMailboxEmails(step) {
} }
async function refreshInbox() { async function refreshInbox() {
if (typeof throwIfMail2925LimitReached === 'function') {
throwIfMail2925LimitReached();
}
const refreshBtn = findRefreshButton(); const refreshBtn = findRefreshButton();
if (refreshBtn) { if (refreshBtn) {
simulateClick(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) { async function handlePollEmail(step, payload) {
await ensureSeenCodesSession(step, payload); await ensureSeenCodesSession(step, payload);
const { const {
@@ -543,6 +856,9 @@ async function handlePollEmail(step, payload) {
strictChatGPTCodeOnly = false, strictChatGPTCodeOnly = false,
} = payload || {}; } = payload || {};
const excludedCodeSet = new Set(excludeCodes.filter(Boolean)); const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
if (typeof throwIfMail2925LimitReached === 'function') {
throwIfMail2925LimitReached();
}
log(`步骤 ${step}:开始轮询 2925 邮箱(最多 ${maxAttempts} 次)`); log(`步骤 ${step}:开始轮询 2925 邮箱(最多 ${maxAttempts} 次)`);
@@ -562,6 +878,9 @@ async function handlePollEmail(step, payload) {
await returnToInbox(); await returnToInbox();
await refreshInbox(); await refreshInbox();
await sleep(2000); await sleep(2000);
if (typeof throwIfMail2925LimitReached === 'function') {
throwIfMail2925LimitReached();
}
initialItems = findMailItems(); initialItems = findMailItems();
} }
@@ -572,6 +891,9 @@ async function handlePollEmail(step, payload) {
log(`步骤 ${step}:邮件列表已加载,共 ${initialItems.length} 封邮件`); log(`步骤 ${step}:邮件列表已加载,共 ${initialItems.length} 封邮件`);
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
if (typeof throwIfMail2925LimitReached === 'function') {
throwIfMail2925LimitReached();
}
log(`步骤 ${step}:正在轮询 2925 邮箱,第 ${attempt}/${maxAttempts}`); log(`步骤 ${step}:正在轮询 2925 邮箱,第 ${attempt}/${maxAttempts}`);
if (attempt > 1 || !initialLoadUsedRefresh) { if (attempt > 1 || !initialLoadUsedRefresh) {
+162
View File
@@ -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,
};
});
-1
View File
@@ -25,7 +25,6 @@
dom.rowSub2ApiGroup, dom.rowSub2ApiGroup,
dom.rowSub2ApiDefaultProxy, dom.rowSub2ApiDefaultProxy,
dom.rowCustomPassword, dom.rowCustomPassword,
dom.rowAccountRunHistoryTextEnabled,
dom.rowAccountRunHistoryHelperBaseUrl, dom.rowAccountRunHistoryHelperBaseUrl,
].filter(Boolean); ].filter(Boolean);
+463
View File
@@ -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 = '<div class="hotmail-empty">还没有 2925 账号,先添加一条再使用。</div>';
updateMail2925ListViewport();
return;
}
dom.mail2925AccountsList.innerHTML = accounts.map((account) => {
const status = getStatusSnapshot(account);
const coolingDown = status.label === '冷却中';
return `
<div class="hotmail-account-item${account.id === currentId ? ' is-current' : ''}">
<div class="hotmail-account-top">
<div class="hotmail-account-title-row">
<div class="hotmail-account-email">${helpers.escapeHtml(account.email || '(未命名账号)')}</div>
<button
class="hotmail-copy-btn"
type="button"
data-account-action="copy-email"
data-account-id="${helpers.escapeHtml(account.id)}"
title="复制邮箱"
aria-label="复制邮箱 ${helpers.escapeHtml(account.email || '')}"
>${copyIcon}</button>
</div>
<span class="hotmail-status-chip ${helpers.escapeHtml(status.className)}">${helpers.escapeHtml(status.label)}</span>
</div>
<div class="hotmail-account-meta">
<span>密码:${account.password ? '已保存' : '未保存'}</span>
<span>上次登录:${helpers.escapeHtml(formatDateTime(account.lastLoginAt))}</span>
<span>上次使用:${helpers.escapeHtml(formatDateTime(account.lastUsedAt))}</span>
<span>上限记录:${helpers.escapeHtml(formatDateTime(account.lastLimitAt))}</span>
<span>恢复时间:${helpers.escapeHtml(formatDateTime(account.disabledUntil))}</span>
</div>
${account.lastError ? `<div class="hotmail-account-error">${helpers.escapeHtml(account.lastError)}</div>` : ''}
<div class="hotmail-account-actions">
<button class="btn btn-outline btn-sm" type="button" data-account-action="select" data-account-id="${helpers.escapeHtml(account.id)}">使用此账号</button>
<button class="btn btn-primary btn-sm" type="button" data-account-action="login" data-account-id="${helpers.escapeHtml(account.id)}">登录</button>
<button class="btn btn-outline btn-sm" type="button" data-account-action="toggle-enabled" data-account-id="${helpers.escapeHtml(account.id)}">${account.enabled === false ? '启用' : '禁用'}</button>
${coolingDown ? `<button class="btn btn-outline btn-sm" type="button" data-account-action="clear-cooldown" data-account-id="${helpers.escapeHtml(account.id)}">清冷却</button>` : ''}
<button class="btn btn-ghost btn-sm" type="button" data-account-action="delete" data-account-id="${helpers.escapeHtml(account.id)}">删除</button>
</div>
</div>
`;
}).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);
+44 -59
View File
@@ -697,6 +697,21 @@ header {
min-width: 0; 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 { .data-value-actions {
justify-content: space-between; justify-content: space-between;
} }
@@ -1395,36 +1410,51 @@ header {
flex-shrink: 0; flex-shrink: 0;
} }
.fallback-inline { .setting-pair {
justify-content: space-between; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: 12px;
flex: 1;
min-width: 0;
flex-wrap: nowrap;
} }
.fallback-thread-interval { .setting-group {
margin-left: auto;
}
.timing-field {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
min-width: 0; min-width: 0;
} }
.timing-field-labeled { .setting-group-primary {
min-width: 196px; flex: 1;
justify-content: flex-end; min-width: 112px;
} }
.timing-field-right { .setting-group-secondary {
margin-left: auto; margin-left: auto;
}
.setting-inline-right {
min-width: 196px; min-width: 196px;
justify-content: flex-end; 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 { .toggle-switch {
position: relative; position: relative;
display: inline-flex; display: inline-flex;
@@ -1482,51 +1512,6 @@ header {
opacity: 0.55; 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 { .auto-delay-input {
width: 72px; width: 72px;
flex: 0 0 auto; flex: 0 0 auto;
+79 -32
View File
@@ -261,7 +261,19 @@
</div> </div>
<div class="data-row" id="row-email-prefix" style="display:none;"> <div class="data-row" id="row-email-prefix" style="display:none;">
<span class="data-label" id="label-email-prefix">别名基邮箱</span> <span class="data-label" id="label-email-prefix">别名基邮箱</span>
<input type="text" id="input-email-prefix" class="data-input" placeholder="例如 yourname@example.com" /> <div class="data-inline mail2925-base-inline">
<input type="text" id="input-email-prefix" class="data-input mail2925-base-input" placeholder="例如 yourname@example.com" />
<select id="select-mail2925-pool-account" class="data-select mail2925-pool-select" style="display:none;">
<option value="">请选择号池邮箱</option>
</select>
<label class="toggle-switch mail2925-pool-toggle" id="label-mail2925-use-account-pool" for="input-mail2925-use-account-pool" style="display:none;" title="开启后只能从号池中选择 2925 基邮箱">
<input type="checkbox" id="input-mail2925-use-account-pool" />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
<span>号池</span>
</label>
</div>
</div> </div>
<div class="data-row" id="row-inbucket-host" style="display:none;"> <div class="data-row" id="row-inbucket-host" style="display:none;">
<span class="data-label">Inbucket</span> <span class="data-label">Inbucket</span>
@@ -280,23 +292,23 @@
</div> </div>
<div class="data-row"> <div class="data-row">
<span class="data-label">延迟</span> <span class="data-label">延迟</span>
<div class="data-inline auto-delay-inline"> <div class="data-inline setting-pair">
<div class="auto-delay-side auto-delay-side-left"> <div class="setting-group setting-group-primary">
<label class="toggle-switch" for="input-auto-delay-enabled"> <label class="toggle-switch" for="input-auto-delay-enabled">
<input type="checkbox" id="input-auto-delay-enabled" /> <input type="checkbox" id="input-auto-delay-enabled" />
<span class="toggle-switch-track" aria-hidden="true"> <span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span> <span class="toggle-switch-thumb"></span>
</span> </span>
</label> </label>
<div class="auto-delay-controls"> <div class="setting-controls">
<input type="number" id="input-auto-delay-minutes" class="data-input auto-delay-input" value="30" min="1" <input type="number" id="input-auto-delay-minutes" class="data-input auto-delay-input" value="30" min="1"
max="1440" step="1" title="启动前倒计时分钟数" /> max="1440" step="1" title="启动前倒计时分钟数" />
<span class="data-unit">分钟</span> <span class="data-unit">分钟</span>
</div> </div>
</div> </div>
<div class="timing-field timing-field-labeled timing-field-right auto-delay-side auto-delay-side-right"> <div class="setting-group setting-group-secondary">
<span class="auto-delay-caption">步间间隔</span> <span class="setting-caption">步间间隔</span>
<div class="auto-delay-controls"> <div class="setting-controls">
<input type="number" id="input-auto-step-delay-seconds" class="data-input auto-delay-input" value="" <input type="number" id="input-auto-step-delay-seconds" class="data-input auto-delay-input" value=""
min="0" max="600" step="1" title="步间延迟秒数,0 或留空则不延迟" /> min="0" max="600" step="1" title="步间延迟秒数,0 或留空则不延迟" />
<span class="data-unit"></span> <span class="data-unit"></span>
@@ -306,16 +318,18 @@
</div> </div>
<div class="data-row"> <div class="data-row">
<span class="data-label">自动重试</span> <span class="data-label">自动重试</span>
<div class="data-inline fallback-inline"> <div class="data-inline setting-pair">
<label class="toggle-switch" for="input-auto-skip-failures"> <div class="setting-group setting-group-primary">
<input type="checkbox" id="input-auto-skip-failures" /> <label class="toggle-switch" for="input-auto-skip-failures">
<span class="toggle-switch-track" aria-hidden="true"> <input type="checkbox" id="input-auto-skip-failures" />
<span class="toggle-switch-thumb"></span> <span class="toggle-switch-track" aria-hidden="true">
</span> <span class="toggle-switch-thumb"></span>
</label> </span>
<div class="timing-field timing-field-labeled timing-field-right fallback-thread-interval"> </label>
<span class="auto-delay-caption">线程间隔</span> </div>
<div class="auto-delay-controls"> <div class="setting-group setting-group-secondary">
<span class="setting-caption">线程间隔</span>
<div class="setting-controls">
<input type="number" id="input-auto-skip-failures-thread-interval-minutes" class="data-input auto-delay-input" value="0" <input type="number" id="input-auto-skip-failures-thread-interval-minutes" class="data-input auto-delay-input" value="0"
min="0" max="1440" step="1" title="兜底模式下,两轮线程之间的等待分钟数" /> min="0" max="1440" step="1" title="兜底模式下,两轮线程之间的等待分钟数" />
<span class="data-unit">分钟</span> <span class="data-unit">分钟</span>
@@ -325,21 +339,18 @@
</div> </div>
<div class="data-row" id="row-account-run-history-text-enabled"> <div class="data-row" id="row-account-run-history-text-enabled">
<span class="data-label">本地同步</span> <span class="data-label">本地同步</span>
<div class="data-inline"> <div class="data-inline setting-pair">
<label class="toggle-switch" for="input-account-run-history-text-enabled"> <div class="setting-group setting-group-primary">
<input type="checkbox" id="input-account-run-history-text-enabled" /> <label class="toggle-switch" for="input-account-run-history-text-enabled">
<span class="toggle-switch-track" aria-hidden="true"> <input type="checkbox" id="input-account-run-history-text-enabled" />
<span class="toggle-switch-thumb"></span> <span class="toggle-switch-track" aria-hidden="true">
</span> <span class="toggle-switch-thumb"></span>
</label> </span>
</div> </label>
</div> </div>
<div class="data-row" id="row-verification-resend-count"> <div class="setting-group setting-group-secondary">
<span class="data-label">验证码</span> <span class="setting-caption">验证码重发</span>
<div class="data-inline"> <div class="setting-controls">
<div class="timing-field timing-field-labeled timing-field-right setting-inline-right">
<span class="auto-delay-caption">验证码重发</span>
<div class="auto-delay-controls">
<input type="number" id="input-verification-resend-count" class="data-input auto-delay-input" value="4" <input type="number" id="input-verification-resend-count" class="data-input auto-delay-input" value="4"
min="0" max="20" step="1" title="自动点击重新发送验证码的次数" /> min="0" max="20" step="1" title="自动点击重新发送验证码的次数" />
<span class="data-unit"></span> <span class="data-unit"></span>
@@ -424,6 +435,40 @@
<div id="hotmail-accounts-list" class="hotmail-accounts-list"></div> <div id="hotmail-accounts-list" class="hotmail-accounts-list"></div>
</div> </div>
</div> </div>
<div id="mail2925-section" class="data-card hotmail-card" style="display:none;">
<div class="section-mini-header">
<div class="section-mini-copy">
<span class="section-label">2925 账号池</span>
</div>
<div class="section-mini-actions">
<button id="btn-delete-all-mail2925-accounts" class="btn btn-ghost btn-xs" type="button">全部删除</button>
<button id="btn-toggle-mail2925-list" class="btn btn-ghost btn-xs" type="button" aria-expanded="false">展开列表</button>
</div>
</div>
<div class="data-row">
<span class="data-label">邮箱</span>
<input type="text" id="input-mail2925-email" class="data-input" placeholder="name@2925.com" />
</div>
<div class="data-row">
<span class="data-label">密码</span>
<input type="password" id="input-mail2925-password" class="data-input" placeholder="2925 登录密码" />
</div>
<div class="data-row hotmail-actions-row">
<span class="data-label"></span>
<button id="btn-add-mail2925-account" class="btn btn-primary btn-sm" type="button">添加账号</button>
</div>
<div class="data-row hotmail-import-row">
<span class="data-label">批量导入</span>
<div class="hotmail-import-box">
<textarea id="input-mail2925-import" class="data-textarea mono"
placeholder="邮箱----密码&#10;name@2925.com----password"></textarea>
<button id="btn-import-mail2925-accounts" class="btn btn-outline btn-sm" type="button">导入</button>
</div>
</div>
<div id="mail2925-list-shell" class="hotmail-list-shell is-collapsed">
<div id="mail2925-accounts-list" class="hotmail-accounts-list"></div>
</div>
</div>
<div id="luckmail-section" class="data-card luckmail-card" style="display:none;"> <div id="luckmail-section" class="data-card luckmail-card" style="display:none;">
<div class="section-mini-header"> <div class="section-mini-header">
<div class="section-mini-copy"> <div class="section-mini-copy">
@@ -656,12 +701,14 @@
<div id="toast-container"></div> <div id="toast-container"></div>
<input id="input-import-settings-file" type="file" accept=".json,application/json" hidden /> <input id="input-import-settings-file" type="file" accept=".json,application/json" hidden />
<script src="../managed-alias-utils.js"></script> <script src="../managed-alias-utils.js"></script>
<script src="../mail2925-utils.js"></script>
<script src="../icloud-utils.js"></script> <script src="../icloud-utils.js"></script>
<script src="../hotmail-utils.js"></script> <script src="../hotmail-utils.js"></script>
<script src="../luckmail-utils.js"></script> <script src="../luckmail-utils.js"></script>
<script src="../data/step-definitions.js"></script> <script src="../data/step-definitions.js"></script>
<script src="update-service.js"></script> <script src="update-service.js"></script>
<script src="hotmail-manager.js"></script> <script src="hotmail-manager.js"></script>
<script src="mail-2925-manager.js"></script>
<script src="icloud-manager.js"></script> <script src="icloud-manager.js"></script>
<script src="luckmail-manager.js"></script> <script src="luckmail-manager.js"></script>
<script src="contribution-mode.js"></script> <script src="contribution-mode.js"></script>
+218 -1
View File
@@ -110,6 +110,7 @@ const selectTempEmailDomain = document.getElementById('select-temp-email-domain'
const inputTempEmailDomain = document.getElementById('input-temp-email-domain'); const inputTempEmailDomain = document.getElementById('input-temp-email-domain');
const btnTempEmailDomainMode = document.getElementById('btn-temp-email-domain-mode'); const btnTempEmailDomainMode = document.getElementById('btn-temp-email-domain-mode');
const hotmailSection = document.getElementById('hotmail-section'); const hotmailSection = document.getElementById('hotmail-section');
const mail2925Section = document.getElementById('mail2925-section');
const luckmailSection = document.getElementById('luckmail-section'); const luckmailSection = document.getElementById('luckmail-section');
const icloudSection = document.getElementById('icloud-section'); const icloudSection = document.getElementById('icloud-section');
const icloudSummary = document.getElementById('icloud-summary'); const icloudSummary = document.getElementById('icloud-summary');
@@ -150,6 +151,15 @@ const btnDeleteAllHotmailAccounts = document.getElementById('btn-delete-all-hotm
const btnToggleHotmailList = document.getElementById('btn-toggle-hotmail-list'); const btnToggleHotmailList = document.getElementById('btn-toggle-hotmail-list');
const hotmailListShell = document.getElementById('hotmail-list-shell'); const hotmailListShell = document.getElementById('hotmail-list-shell');
const hotmailAccountsList = document.getElementById('hotmail-accounts-list'); const hotmailAccountsList = document.getElementById('hotmail-accounts-list');
const inputMail2925Email = document.getElementById('input-mail2925-email');
const inputMail2925Password = document.getElementById('input-mail2925-password');
const inputMail2925Import = document.getElementById('input-mail2925-import');
const btnAddMail2925Account = document.getElementById('btn-add-mail2925-account');
const btnImportMail2925Accounts = document.getElementById('btn-import-mail2925-accounts');
const btnDeleteAllMail2925Accounts = document.getElementById('btn-delete-all-mail2925-accounts');
const btnToggleMail2925List = document.getElementById('btn-toggle-mail2925-list');
const mail2925ListShell = document.getElementById('mail2925-list-shell');
const mail2925AccountsList = document.getElementById('mail2925-accounts-list');
const inputLuckmailApiKey = document.getElementById('input-luckmail-api-key'); const inputLuckmailApiKey = document.getElementById('input-luckmail-api-key');
const inputLuckmailBaseUrl = document.getElementById('input-luckmail-base-url'); const inputLuckmailBaseUrl = document.getElementById('input-luckmail-base-url');
const selectLuckmailEmailType = document.getElementById('select-luckmail-email-type'); const selectLuckmailEmailType = document.getElementById('select-luckmail-email-type');
@@ -171,6 +181,9 @@ const luckmailList = document.getElementById('luckmail-list');
const rowEmailPrefix = document.getElementById('row-email-prefix'); const rowEmailPrefix = document.getElementById('row-email-prefix');
const labelEmailPrefix = document.getElementById('label-email-prefix'); const labelEmailPrefix = document.getElementById('label-email-prefix');
const inputEmailPrefix = document.getElementById('input-email-prefix'); const inputEmailPrefix = document.getElementById('input-email-prefix');
const selectMail2925PoolAccount = document.getElementById('select-mail2925-pool-account');
const inputMail2925UseAccountPool = document.getElementById('input-mail2925-use-account-pool');
const labelMail2925UseAccountPool = document.getElementById('label-mail2925-use-account-pool');
const rowInbucketHost = document.getElementById('row-inbucket-host'); const rowInbucketHost = document.getElementById('row-inbucket-host');
const inputInbucketHost = document.getElementById('input-inbucket-host'); const inputInbucketHost = document.getElementById('input-inbucket-host');
const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox'); const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
@@ -307,7 +320,70 @@ function getManagedAliasBaseEmailKey(provider = selectMailProvider.value) {
return ''; return '';
} }
function isMail2925AccountPoolEnabled(state = latestState) {
return Boolean(state?.mail2925UseAccountPool);
}
function getPreferredMail2925PoolAccountId(state = latestState) {
const currentId = String(state?.currentMail2925AccountId || '').trim();
if (currentId && getMail2925Accounts(state).some((account) => account.id === currentId)) {
return currentId;
}
return '';
}
function syncMail2925PoolAccountOptions(state = latestState) {
if (!selectMail2925PoolAccount) {
return;
}
const accounts = getMail2925Accounts(state);
const selectedId = getPreferredMail2925PoolAccountId(state);
const options = ['<option value="">请选择号池邮箱</option>'].concat(
accounts.map((account) => `<option value="${escapeHtml(account.id)}">${escapeHtml(account.email || '(未命名账号)')}</option>`)
);
selectMail2925PoolAccount.innerHTML = options.join('');
selectMail2925PoolAccount.value = selectedId;
}
async function syncSelectedMail2925PoolAccount(options = {}) {
const { silent = false } = options;
if (!selectMail2925PoolAccount || !isMail2925AccountPoolEnabled(latestState)) {
return null;
}
const accountId = String(selectMail2925PoolAccount.value || '').trim();
if (!accountId) {
syncLatestState({ currentMail2925AccountId: null });
setManagedAliasBaseEmailInputForProvider('2925', latestState);
return null;
}
const response = await chrome.runtime.sendMessage({
type: 'SELECT_MAIL2925_ACCOUNT',
source: 'sidepanel',
payload: { accountId },
});
if (response?.error) {
throw new Error(response.error);
}
syncLatestState({ currentMail2925AccountId: response.account?.id || accountId });
setManagedAliasBaseEmailInputForProvider('2925', latestState);
if (!silent) {
showToast(`已切换当前 2925 号池邮箱为 ${response.account?.email || accountId}`, 'success', 1800);
}
return response.account || null;
}
function getManagedAliasBaseEmailForProvider(provider = selectMailProvider.value, state = latestState) { function getManagedAliasBaseEmailForProvider(provider = selectMailProvider.value, state = latestState) {
if (String(provider || '').trim().toLowerCase() === '2925' && isMail2925AccountPoolEnabled(state)) {
const currentMail2925Email = getCurrentMail2925Email(state);
if (currentMail2925Email) {
return currentMail2925Email;
}
}
const key = getManagedAliasBaseEmailKey(provider); const key = getManagedAliasBaseEmailKey(provider);
if (!key) { if (!key) {
return ''; return '';
@@ -326,11 +402,16 @@ function buildManagedAliasBaseEmailPayload(state = latestState) {
const payload = { const payload = {
gmailBaseEmail: String(state?.gmailBaseEmail || '').trim(), gmailBaseEmail: String(state?.gmailBaseEmail || '').trim(),
mail2925BaseEmail: String(state?.mail2925BaseEmail || '').trim(), mail2925BaseEmail: String(state?.mail2925BaseEmail || '').trim(),
mail2925UseAccountPool: Boolean(state?.mail2925UseAccountPool),
emailPrefix: '', emailPrefix: '',
}; };
const key = getManagedAliasBaseEmailKey(); const key = getManagedAliasBaseEmailKey();
if (key) { if (key) {
payload[key] = inputEmailPrefix.value.trim(); if (key === 'mail2925BaseEmail' && isMail2925AccountPoolEnabled(state)) {
payload[key] = String(state?.mail2925BaseEmail || '').trim();
} else {
payload[key] = inputEmailPrefix.value.trim();
}
} }
return payload; return payload;
} }
@@ -340,10 +421,14 @@ function syncManagedAliasBaseEmailDraftFromInput(provider = selectMailProvider.v
if (!key) { if (!key) {
return; return;
} }
if (key === 'mail2925BaseEmail' && isMail2925AccountPoolEnabled(latestState)) {
return;
}
syncLatestState({ [key]: inputEmailPrefix.value.trim() }); syncLatestState({ [key]: inputEmailPrefix.value.trim() });
} }
function setManagedAliasBaseEmailInputForProvider(provider = selectMailProvider.value, state = latestState) { function setManagedAliasBaseEmailInputForProvider(provider = selectMailProvider.value, state = latestState) {
syncMail2925PoolAccountOptions(state);
inputEmailPrefix.value = getManagedAliasBaseEmailForProvider(provider, state); inputEmailPrefix.value = getManagedAliasBaseEmailForProvider(provider, state);
} }
@@ -1342,6 +1427,9 @@ function collectSettingsPayload() {
!cloudflareTempEmailDomainEditMode ? selectTempEmailDomain.value : tempEmailActiveDomain !cloudflareTempEmailDomainEditMode ? selectTempEmailDomain.value : tempEmailActiveDomain
) || tempEmailActiveDomain; ) || tempEmailActiveDomain;
const contributionModeEnabled = Boolean(latestState?.contributionMode); const contributionModeEnabled = Boolean(latestState?.contributionMode);
const mail2925UseAccountPool = typeof inputMail2925UseAccountPool !== 'undefined'
? Boolean(inputMail2925UseAccountPool?.checked)
: Boolean(latestState?.mail2925UseAccountPool);
return { return {
panelMode: selectPanelMode.value, panelMode: selectPanelMode.value,
vpsUrl: inputVpsUrl.value.trim(), vpsUrl: inputVpsUrl.value.trim(),
@@ -1357,6 +1445,7 @@ function collectSettingsPayload() {
}), }),
mailProvider: selectMailProvider.value, mailProvider: selectMailProvider.value,
mail2925Mode: getSelectedMail2925Mode(), mail2925Mode: getSelectedMail2925Mode(),
mail2925UseAccountPool,
emailGenerator: selectEmailGenerator.value, emailGenerator: selectEmailGenerator.value,
autoDeleteUsedIcloudAlias: checkboxAutoDeleteIcloud?.checked, autoDeleteUsedIcloudAlias: checkboxAutoDeleteIcloud?.checked,
icloudHostPreference: selectIcloudHostPreference?.value || 'auto', icloudHostPreference: selectIcloudHostPreference?.value || 'auto',
@@ -1771,6 +1860,9 @@ function applySettingsState(state) {
if (inputContributionQq) { if (inputContributionQq) {
inputContributionQq.value = state?.contributionQq || ''; inputContributionQq.value = state?.contributionQq || '';
} }
if (inputMail2925UseAccountPool) {
inputMail2925UseAccountPool.checked = Boolean(state?.mail2925UseAccountPool);
}
setManagedAliasBaseEmailInputForProvider(restoredMailProvider, state); setManagedAliasBaseEmailInputForProvider(restoredMailProvider, state);
inputInbucketHost.value = state?.inbucketHost || ''; inputInbucketHost.value = state?.inbucketHost || '';
inputInbucketMailbox.value = state?.inbucketMailbox || ''; inputInbucketMailbox.value = state?.inbucketMailbox || '';
@@ -2226,6 +2318,19 @@ function getCurrentHotmailEmail(state = latestState) {
return String(getCurrentHotmailAccount(state)?.email || '').trim(); return String(getCurrentHotmailAccount(state)?.email || '').trim();
} }
function getMail2925Accounts(state = latestState) {
return Array.isArray(state?.mail2925Accounts) ? state.mail2925Accounts : [];
}
function getCurrentMail2925Account(state = latestState) {
const currentId = state?.currentMail2925AccountId;
return getMail2925Accounts(state).find((account) => account.id === currentId) || null;
}
function getCurrentMail2925Email(state = latestState) {
return String(getCurrentMail2925Account(state)?.email || '').trim();
}
function getCurrentLuckmailPurchase(state = latestState) { function getCurrentLuckmailPurchase(state = latestState) {
return state?.currentLuckmailPurchase || null; return state?.currentLuckmailPurchase || null;
} }
@@ -2365,6 +2470,8 @@ function updateMailLoginButtonState() {
function updateMailProviderUI() { function updateMailProviderUI() {
const use2925 = selectMailProvider.value === '2925'; const use2925 = selectMailProvider.value === '2925';
const useGmail = selectMailProvider.value === GMAIL_PROVIDER; const useGmail = selectMailProvider.value === GMAIL_PROVIDER;
const useMail2925 = selectMailProvider.value === '2925';
const useMail2925AccountPool = useMail2925 && Boolean(inputMail2925UseAccountPool?.checked);
const mail2925Mode = getSelectedMail2925Mode(); const mail2925Mode = getSelectedMail2925Mode();
const useGeneratedAlias = usesGeneratedAliasMailProvider(selectMailProvider.value, mail2925Mode); const useGeneratedAlias = usesGeneratedAliasMailProvider(selectMailProvider.value, mail2925Mode);
const useInbucket = selectMailProvider.value === 'inbucket'; const useInbucket = selectMailProvider.value === 'inbucket';
@@ -2427,11 +2534,24 @@ function updateMailProviderUI() {
if (hotmailSection) { if (hotmailSection) {
hotmailSection.style.display = useHotmail ? '' : 'none'; hotmailSection.style.display = useHotmail ? '' : 'none';
} }
if (mail2925Section) {
mail2925Section.style.display = useMail2925AccountPool ? '' : 'none';
}
if (luckmailSection) { if (luckmailSection) {
luckmailSection.style.display = useLuckmail ? '' : 'none'; luckmailSection.style.display = useLuckmail ? '' : 'none';
} }
labelEmailPrefix.textContent = '邮箱前缀'; labelEmailPrefix.textContent = '邮箱前缀';
inputEmailPrefix.placeholder = '例如 abc'; inputEmailPrefix.placeholder = '例如 abc';
if (labelMail2925UseAccountPool) {
labelMail2925UseAccountPool.style.display = useMail2925 ? '' : 'none';
}
syncMail2925PoolAccountOptions(latestState);
if (selectMail2925PoolAccount) {
selectMail2925PoolAccount.style.display = useMail2925AccountPool ? '' : 'none';
selectMail2925PoolAccount.disabled = !useMail2925AccountPool || getMail2925Accounts().length === 0;
}
inputEmailPrefix.style.display = useMail2925AccountPool ? 'none' : '';
inputEmailPrefix.readOnly = useMail2925AccountPool;
selectEmailGenerator.disabled = useHotmail || useLuckmail || useGeneratedAlias || useCustomEmail; selectEmailGenerator.disabled = useHotmail || useLuckmail || useGeneratedAlias || useCustomEmail;
if (useGmail) { if (useGmail) {
labelEmailPrefix.textContent = 'Gmail 原邮箱'; labelEmailPrefix.textContent = 'Gmail 原邮箱';
@@ -2480,6 +2600,11 @@ function updateMailProviderUI() {
if (autoHintText && useGeneratedAlias && aliasUiCopy?.hint) { if (autoHintText && useGeneratedAlias && aliasUiCopy?.hint) {
autoHintText.textContent = aliasUiCopy.hint; autoHintText.textContent = aliasUiCopy.hint;
} }
if (autoHintText && useMail2925AccountPool) {
autoHintText.textContent = getMail2925Accounts().length
? '当前已启用 2925 号池模式,步骤 3 会基于下拉框选中的号池邮箱生成别名地址'
: '当前已启用 2925 号池模式,请先在下方 2925 账号池中添加账号并选择邮箱';
}
if (autoHintText && showCloudflareTempEmailReceiveMailbox) { if (autoHintText && showCloudflareTempEmailReceiveMailbox) {
autoHintText.textContent = '若注册邮箱会转发到 Cloudflare Temp Email,请在“邮件接收”中填写实际接收转发邮件的邮箱。'; autoHintText.textContent = '若注册邮箱会转发到 Cloudflare Temp Email,请在“邮件接收”中填写实际接收转发邮件的邮箱。';
} }
@@ -2489,6 +2614,9 @@ function updateMailProviderUI() {
inputEmail.value = getCurrentLuckmailEmail(); inputEmail.value = getCurrentLuckmailEmail();
} }
renderHotmailAccounts(); renderHotmailAccounts();
if (useMail2925) {
renderMail2925Accounts();
}
if (useLuckmail) { if (useLuckmail) {
renderLuckmailPurchases(); renderLuckmailPurchases();
} }
@@ -2905,6 +3033,50 @@ const bindHotmailEvents = hotmailManager?.bindHotmailEvents
|| (() => { }); || (() => { });
bindHotmailEvents(); bindHotmailEvents();
const mail2925Manager = window.SidepanelMail2925Manager?.createMail2925Manager({
state: {
getLatestState: () => latestState,
syncLatestState,
},
dom: {
btnAddMail2925Account,
btnDeleteAllMail2925Accounts,
btnImportMail2925Accounts,
btnToggleMail2925List,
inputMail2925Email,
inputMail2925Import,
inputMail2925Password,
mail2925AccountsList,
mail2925ListShell,
},
helpers: {
copyTextToClipboard,
escapeHtml,
getMail2925Accounts,
openConfirmModal,
refreshManagedAliasBaseEmail: () => {
setManagedAliasBaseEmailInputForProvider('2925', latestState);
},
showToast,
},
runtime: {
sendMessage: (message) => chrome.runtime.sendMessage(message),
},
constants: {
copyIcon: COPY_ICON,
displayTimeZone: DISPLAY_TIMEZONE,
expandedStorageKey: 'multipage-mail2925-list-expanded',
},
mail2925Utils: window.Mail2925Utils || {},
});
const initMail2925ListExpandedState = mail2925Manager?.initMail2925ListExpandedState
|| (() => { });
const renderMail2925Accounts = mail2925Manager?.renderMail2925Accounts
|| (() => { });
const bindMail2925Events = mail2925Manager?.bindMail2925Events
|| (() => { });
bindMail2925Events();
const icloudManager = window.SidepanelIcloudManager?.createIcloudManager({ const icloudManager = window.SidepanelIcloudManager?.createIcloudManager({
dom: { dom: {
btnIcloudBulkDelete, btnIcloudBulkDelete,
@@ -3775,6 +3947,13 @@ selectMailProvider.addEventListener('change', async () => {
if (leavingHotmail || leavingLuckmail || leavingGeneratedAlias) { if (leavingHotmail || leavingLuckmail || leavingGeneratedAlias) {
await clearRegistrationEmail({ silent: true }).catch(() => { }); await clearRegistrationEmail({ silent: true }).catch(() => { });
} }
if (nextProvider === '2925' && Boolean(inputMail2925UseAccountPool?.checked)) {
syncMail2925PoolAccountOptions(latestState);
if (!selectMail2925PoolAccount.value && getMail2925Accounts().length > 0) {
selectMail2925PoolAccount.value = String(getMail2925Accounts()[0]?.id || '');
}
await syncSelectedMail2925PoolAccount({ silent: true }).catch(() => { });
}
if (nextProvider === LUCKMAIL_PROVIDER) { if (nextProvider === LUCKMAIL_PROVIDER) {
queueLuckmailPurchaseRefresh(); queueLuckmailPurchaseRefresh();
} }
@@ -3956,6 +4135,36 @@ inputEmailPrefix.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => {}); saveSettings({ silent: true }).catch(() => {});
}); });
selectMail2925PoolAccount?.addEventListener('change', async () => {
try {
await syncSelectedMail2925PoolAccount();
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => {});
} catch (err) {
showToast(err.message, 'error');
}
});
inputMail2925UseAccountPool?.addEventListener('change', async () => {
const enabled = Boolean(inputMail2925UseAccountPool.checked);
syncLatestState({ mail2925UseAccountPool: enabled });
if (enabled) {
syncMail2925PoolAccountOptions(latestState);
if (!selectMail2925PoolAccount.value && getMail2925Accounts().length > 0) {
selectMail2925PoolAccount.value = String(getMail2925Accounts()[0]?.id || '');
}
try {
await syncSelectedMail2925PoolAccount({ silent: true });
} catch (err) {
showToast(err.message, 'error');
}
}
setManagedAliasBaseEmailInputForProvider('2925', latestState);
updateMailProviderUI();
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => {});
});
inputInbucketMailbox.addEventListener('input', () => { inputInbucketMailbox.addEventListener('input', () => {
markSettingsDirty(true); markSettingsDirty(true);
scheduleSettingsAutoSave(); scheduleSettingsAutoSave();
@@ -4207,6 +4416,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
updateProgressCounter(); updateProgressCounter();
updateButtonStates(); updateButtonStates();
renderHotmailAccounts(); renderHotmailAccounts();
renderMail2925Accounts();
if (isLuckmailProvider()) { if (isLuckmailProvider()) {
queueLuckmailPurchaseRefresh(); queueLuckmailPurchaseRefresh();
} }
@@ -4261,6 +4471,12 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
inputEmail.value = getCurrentHotmailEmail(); inputEmail.value = getCurrentHotmailEmail();
} }
} }
if (message.payload.currentMail2925AccountId !== undefined || message.payload.mail2925Accounts !== undefined) {
renderMail2925Accounts();
if (selectMailProvider.value === '2925') {
setManagedAliasBaseEmailInputForProvider('2925', latestState);
}
}
if (message.payload.luckmailApiKey !== undefined) { if (message.payload.luckmailApiKey !== undefined) {
inputLuckmailApiKey.value = message.payload.luckmailApiKey || ''; inputLuckmailApiKey.value = message.payload.luckmailApiKey || '';
} }
@@ -4421,6 +4637,7 @@ document.addEventListener('keydown', (event) => {
initializeManualStepActions(); initializeManualStepActions();
initTheme(); initTheme();
initHotmailListExpandedState(); initHotmailListExpandedState();
initMail2925ListExpandedState();
updateSaveButtonState(); updateSaveButtonState();
updateConfigMenuControls(); updateConfigMenuControls();
setLocalCpaStep9Mode(DEFAULT_LOCAL_CPA_STEP9_MODE); setLocalCpaStep9Mode(DEFAULT_LOCAL_CPA_STEP9_MODE);
@@ -0,0 +1,183 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
const bundle = [
extractFunction('isAddPhoneAuthUrl'),
extractFunction('isAddPhoneAuthState'),
extractFunction('isMail2925ThreadTerminatedError'),
extractFunction('isSignupUserAlreadyExistsFailure'),
extractFunction('getPostStep6AutoRestartDecision'),
extractFunction('runAutoSequenceFromStep'),
].join('\n');
test('auto-run stops step4 restart path when mail2925 ends the current thread', async () => {
const api = new Function(`
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const chrome = {
tabs: {
update: async () => {},
},
runtime: {
sendMessage: async () => {},
},
};
let currentState = {
email: 'demo123456@2925.com',
password: 'Secret123!',
mailProvider: '2925',
stepStatuses: {
1: 'pending',
2: 'pending',
3: 'pending',
4: 'pending',
5: 'pending',
6: 'pending',
7: 'pending',
8: 'pending',
9: 'pending',
10: 'pending',
},
};
const events = {
steps: [],
invalidations: [],
logs: [],
};
async function addLog(message, level = 'info') {
events.logs.push({ message, level });
}
async function ensureAutoEmailReady() {
return currentState.email;
}
async function broadcastAutoRunStatus() {}
async function getState() {
return currentState;
}
async function setState(updates) {
currentState = {
...currentState,
...updates,
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
};
}
function isStopError(error) {
return false;
}
function isStepDoneStatus(status) {
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
}
async function executeStepAndWait(step) {
events.steps.push(step);
if (step === 4) {
throw new Error('MAIL2925_THREAD_TERMINATED::步骤 42925 已切换账号并要求结束当前尝试。');
}
}
async function getTabId() {
return 1;
}
async function invalidateDownstreamAfterStepRestart(step, options = {}) {
events.invalidations.push({ step, options });
}
function getLoginAuthStateLabel(state) {
return state || 'unknown';
}
function getErrorMessage(error) {
return error?.message || String(error || '');
}
async function getLoginAuthStateFromContent() {
return { state: 'password_page', url: 'https://auth.openai.com/log-in' };
}
${bundle}
return {
async run() {
try {
await runAutoSequenceFromStep(1, {
targetRun: 1,
totalRuns: 1,
attemptRuns: 1,
continued: false,
});
return { events, currentState, error: null };
} catch (error) {
return { events, currentState, error: error.message };
}
},
};
`)();
const result = await api.run();
assert.match(result.error, /^MAIL2925_THREAD_TERMINATED::/);
assert.deepStrictEqual(result.events.invalidations, []);
assert.deepStrictEqual(result.events.steps, [1, 2, 3, 4]);
assert.equal(result.events.logs.some(({ message }) => /回到步骤 1/.test(message)), false);
});
+1
View File
@@ -55,6 +55,7 @@ function extractFunction(name) {
const bundle = [ const bundle = [
extractFunction('isAddPhoneAuthUrl'), extractFunction('isAddPhoneAuthUrl'),
extractFunction('isAddPhoneAuthState'), extractFunction('isAddPhoneAuthState'),
extractFunction('isMail2925ThreadTerminatedError'),
extractFunction('isSignupUserAlreadyExistsFailure'), extractFunction('isSignupUserAlreadyExistsFailure'),
extractFunction('getPostStep6AutoRestartDecision'), extractFunction('getPostStep6AutoRestartDecision'),
extractFunction('runAutoSequenceFromStep'), extractFunction('runAutoSequenceFromStep'),
@@ -0,0 +1,94 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const mail2925Utils = require('../mail2925-utils.js');
test('background mail2925 session module exposes a factory', () => {
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
assert.equal(typeof api?.createMail2925SessionManager, 'function');
});
test('handleMail2925LimitReachedError disables current account and switches to the next one', async () => {
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
let currentState = {
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'current', email: 'current@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
{ id: 'next', email: 'next@2925.com', password: 'p2', enabled: true, lastUsedAt: 20 },
]),
currentMail2925AccountId: 'current',
};
const events = {
logs: [],
dataUpdates: [],
tabOpens: 0,
sessionChecks: 0,
};
const manager = api.createMail2925SessionManager({
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
broadcastDataUpdate: (payload) => {
events.dataUpdates.push(payload);
},
chrome: {
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
reuseOrCreateTab: async () => {
events.tabOpens += 1;
return 1;
},
sendToMailContentScriptResilient: async () => {
events.sessionChecks += 1;
return { loggedIn: true };
},
setPersistentSettings: async (payload) => {
currentState = {
...currentState,
...payload,
};
},
setState: async (updates) => {
currentState = {
...currentState,
...updates,
};
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
const error = await manager.handleMail2925LimitReachedError(
4,
new Error('MAIL2925_LIMIT_REACHED::子邮箱已达上限邮箱')
);
assert.match(error.message, /^MAIL2925_THREAD_TERMINATED::/);
assert.equal(currentState.currentMail2925AccountId, 'next');
assert.ok(events.tabOpens >= 1);
assert.ok(events.sessionChecks >= 1);
const currentAccount = currentState.mail2925Accounts.find((account) => account.id === 'current');
assert.equal(currentAccount.lastError, '子邮箱已达上限邮箱');
assert.ok(currentAccount.disabledUntil > Date.now());
});
@@ -0,0 +1,103 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const signupFlowSource = fs.readFileSync('background/signup-flow-helpers.js', 'utf8');
const signupFlowGlobalScope = {};
const signupFlowApi = new Function('self', `${signupFlowSource}; return self.MultiPageSignupFlowHelpers;`)(signupFlowGlobalScope);
test('signup flow helper allocates mail2925 account before generating alias email', async () => {
const calls = {
ensureMail2925: [],
buildAlias: 0,
setEmail: [],
};
const helpers = signupFlowApi.createSignupFlowHelpers({
buildGeneratedAliasEmail: (state) => {
calls.buildAlias += 1;
assert.equal(state.currentMail2925AccountId, 'acc-2');
return 'demo123456@2925.com';
},
chrome: { tabs: { get: async () => ({ id: 1, url: 'https://auth.openai.com/create-account/password' }) } },
ensureContentScriptReadyOnTab: async () => {},
ensureHotmailAccountForFlow: async () => ({}),
ensureMail2925AccountForFlow: async (options) => {
calls.ensureMail2925.push(options);
return { id: 'acc-2', email: 'demo@2925.com' };
},
ensureLuckmailPurchaseForFlow: async () => ({}),
isGeneratedAliasProvider: () => true,
isReusableGeneratedAliasEmail: () => false,
isHotmailProvider: () => false,
isLuckmailProvider: () => false,
isSignupEmailVerificationPageUrl: () => false,
isSignupPasswordPageUrl: () => true,
reuseOrCreateTab: async () => 1,
sendToContentScriptResilient: async () => ({}),
setEmailState: async (email) => {
calls.setEmail.push(email);
},
SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
SIGNUP_PAGE_INJECT_FILES: [],
waitForTabUrlMatch: async () => null,
});
const email = await helpers.resolveSignupEmailForFlow({
mailProvider: '2925',
mail2925UseAccountPool: true,
currentMail2925AccountId: 'acc-2',
email: '',
});
assert.equal(email, 'demo123456@2925.com');
assert.deepStrictEqual(calls.ensureMail2925, [
{
allowAllocate: true,
preferredAccountId: 'acc-2',
markUsed: true,
},
]);
assert.equal(calls.buildAlias, 1);
assert.deepStrictEqual(calls.setEmail, ['demo123456@2925.com']);
});
test('signup flow helper skips mail2925 account allocation when account pool switch is off', async () => {
const calls = {
ensureMail2925: 0,
};
const helpers = signupFlowApi.createSignupFlowHelpers({
buildGeneratedAliasEmail: () => 'manual123456@2925.com',
chrome: { tabs: { get: async () => ({ id: 1, url: 'https://auth.openai.com/create-account/password' }) } },
ensureContentScriptReadyOnTab: async () => {},
ensureHotmailAccountForFlow: async () => ({}),
ensureMail2925AccountForFlow: async () => {
calls.ensureMail2925 += 1;
return { id: 'acc-2', email: 'demo@2925.com' };
},
ensureLuckmailPurchaseForFlow: async () => ({}),
isGeneratedAliasProvider: () => true,
isReusableGeneratedAliasEmail: () => false,
isHotmailProvider: () => false,
isLuckmailProvider: () => false,
isSignupEmailVerificationPageUrl: () => false,
isSignupPasswordPageUrl: () => true,
reuseOrCreateTab: async () => 1,
sendToContentScriptResilient: async () => ({}),
setEmailState: async () => {},
SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
SIGNUP_PAGE_INJECT_FILES: [],
waitForTabUrlMatch: async () => null,
});
const email = await helpers.resolveSignupEmailForFlow({
mailProvider: '2925',
mail2925UseAccountPool: false,
currentMail2925AccountId: 'acc-2',
email: '',
});
assert.equal(email, 'manual123456@2925.com');
assert.equal(calls.ensureMail2925, 0);
});
+62
View File
@@ -0,0 +1,62 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const utils = require('../mail2925-utils.js');
test('normalizeMail2925Account normalizes key fields', () => {
const account = utils.normalizeMail2925Account({
id: 'a-1',
email: ' Demo@2925.com ',
password: 'secret',
enabled: 0,
lastUsedAt: '123',
disabledUntil: '456',
});
assert.deepStrictEqual(account, {
id: 'a-1',
email: 'demo@2925.com',
password: 'secret',
enabled: false,
lastUsedAt: 123,
lastLoginAt: 0,
lastLimitAt: 0,
disabledUntil: 456,
lastError: '',
});
});
test('pickMail2925AccountForRun skips cooldown and picks the least recently used account', () => {
const now = 1000;
const picked = utils.pickMail2925AccountForRun([
{ id: 'cooldown', email: 'cool@2925.com', password: 'x', disabledUntil: now + 10, lastUsedAt: 1 },
{ id: 'b', email: 'b@2925.com', password: 'x', lastUsedAt: 50 },
{ id: 'a', email: 'a@2925.com', password: 'x', lastUsedAt: 10 },
], { now });
assert.equal(picked.id, 'a');
});
test('getMail2925AccountStatus reports cooldown before error', () => {
const status = utils.getMail2925AccountStatus({
email: 'demo@2925.com',
password: 'secret',
enabled: true,
disabledUntil: Date.now() + 60_000,
lastError: '子邮箱已达上限邮箱',
});
assert.equal(status, 'cooldown');
});
test('parseMail2925ImportText parses 邮箱----密码 rows', () => {
const parsed = utils.parseMail2925ImportText(`
邮箱----密码
demo1@2925.com----pass1
demo2@2925.com----pass2
`);
assert.deepStrictEqual(parsed, [
{ email: 'demo1@2925.com', password: 'pass1' },
{ email: 'demo2@2925.com', password: 'pass2' },
]);
});
+89
View File
@@ -0,0 +1,89 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('sidepanel loads mail2925 manager before sidepanel bootstrap', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const managerIndex = html.indexOf('<script src="mail-2925-manager.js"></script>');
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
assert.notEqual(managerIndex, -1);
assert.notEqual(sidepanelIndex, -1);
assert.ok(managerIndex < sidepanelIndex);
});
test('sidepanel html contains mail2925 pool toggle and selector controls', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="input-mail2925-use-account-pool"/);
assert.match(html, /id="select-mail2925-pool-account"/);
});
test('mail2925 manager exposes a factory and renders empty state', () => {
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
const windowObject = {};
const localStorageMock = {
getItem() {
return null;
},
setItem() {},
};
const api = new Function('window', 'localStorage', `${source}; return window.SidepanelMail2925Manager;`)(
windowObject,
localStorageMock
);
assert.equal(typeof api?.createMail2925Manager, 'function');
const mail2925AccountsList = { innerHTML: '', addEventListener() {} };
const toggleButton = {
textContent: '',
disabled: false,
setAttribute() {},
addEventListener() {},
};
const noopClassList = { toggle() {} };
const manager = api.createMail2925Manager({
state: {
getLatestState: () => ({ currentMail2925AccountId: null, mail2925Accounts: [] }),
syncLatestState() {},
},
dom: {
btnAddMail2925Account: { disabled: false, addEventListener() {} },
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
btnToggleMail2925List: toggleButton,
inputMail2925Email: { value: '' },
inputMail2925Import: { value: '' },
inputMail2925Password: { value: '' },
mail2925AccountsList,
mail2925ListShell: { classList: noopClassList },
},
helpers: {
getMail2925Accounts: () => [],
escapeHtml: (value) => String(value || ''),
showToast() {},
openConfirmModal: async () => true,
copyTextToClipboard: async () => {},
refreshManagedAliasBaseEmail() {},
},
runtime: {
sendMessage: async () => ({}),
},
constants: {
copyIcon: '',
displayTimeZone: 'Asia/Shanghai',
expandedStorageKey: 'multipage-mail2925-list-expanded',
},
mail2925Utils: {},
});
assert.equal(typeof manager.renderMail2925Accounts, 'function');
assert.equal(typeof manager.bindMail2925Events, 'function');
assert.equal(typeof manager.initMail2925ListExpandedState, 'function');
manager.renderMail2925Accounts();
assert.match(mail2925AccountsList.innerHTML, /还没有 2925 账号/);
});
+32 -2
View File
@@ -138,6 +138,7 @@
- 标签注册表 - 标签注册表
- 最近打开的来源地址 - 最近打开的来源地址
- LuckMail 当前运行时选择 - LuckMail 当前运行时选择
- 2925 当前运行时账号选择 `currentMail2925AccountId`
补充: 补充:
@@ -150,6 +151,8 @@
- CPA / SUB2API 配置 - CPA / SUB2API 配置
- 邮箱 provider 配置 - 邮箱 provider 配置
- Hotmail 账号池 - Hotmail 账号池
- 2925 账号池
- 2925 是否启用号池模式 `mail2925UseAccountPool`
- Cloudflare / Temp Email 设置 - Cloudflare / Temp Email 设置
- iCloud 相关偏好 - iCloud 相关偏好
- LuckMail API 配置 - LuckMail API 配置
@@ -305,14 +308,16 @@
补充行为: 补充行为:
- `2925` provider 会关闭 Step 4 / 8 的自动重发间隔 25 秒节流;每次“重新发送验证码”之间,会在邮箱页内部执行一轮固定 15 次的刷新轮询,不再因 OAuth 剩余时间预算而缩短。 - `2925` provider 会关闭 Step 4 / 8 的自动重发间隔 25 秒节流;每次“重新发送验证码”之间,会在邮箱页内部执行一轮固定 15 次的刷新轮询,不再因 OAuth 剩余时间预算而缩短。
- 当 provider 为 `2925` 时,后台会在进入 Step 4 / Step 8 之前先确保当前 2925 账号已登录网页邮箱;如果当前标签页仍未登录,会自动补一次“清理 2925 cookie -> 使用当前账号密码重新登录”。
- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 当前既不依赖时间窗,也不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中的匹配邮件。 - 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 当前既不依赖时间窗,也不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中的匹配邮件。
- 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。 - 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。
- 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。 - 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。
- `2925` 内容脚本会把每一封实际打开检测的邮件立即删除;同一验证码步骤启动后,试过的验证码会按“步骤 ID + 启动时间”隔离缓存,不会在本次步骤里重复提交;如果再次遇到相同验证码,对应邮件也会在读取后立即删除,避免后续反复打开。 - `2925` 内容脚本会把每一封实际打开检测的邮件立即删除;同一验证码步骤启动后,试过的验证码会按“步骤 ID + 启动时间”隔离缓存,不会在本次步骤里重复提交;如果再次遇到相同验证码,对应邮件也会在读取后立即删除,避免后续反复打开。
- `2925` 当前不再对邮件里的收件邮箱做比对,只要邮件内容命中 ChatGPT / OpenAI 验证码过滤条件,就会尝试该邮件。 - `2925` 当前不再对邮件里的收件邮箱做比对,只要邮件内容命中 ChatGPT / OpenAI 验证码过滤条件,就会尝试该邮件。
- 当验证码最终提交成功后,后台会异步向 2925 邮箱页发送 `DELETE_ALL_EMAILS`,执行“全选 + 删除”清理剩余邮件,不阻塞主流程。 - 当验证码最终提交成功后,后台会异步向 2925 邮箱页发送 `DELETE_ALL_EMAILS`,执行“全选 + 删除”清理剩余邮件,不阻塞主流程。
- 如果 `2925` 邮箱页在轮询期间出现“子邮箱已达上限邮箱”,后台会记录当前时间,把当前 2925 账号禁用 24 小时,自动切到下一个可用账号并完成登录,然后直接报错结束当前尝试;如果 Auto 开启了自动重试,现有控制器会按原逻辑进入下一次尝试。
- Auto 模式下,如果 Step 4 当前轮失败,后台会沿用当前邮箱回到 Step 1 重新开始当前轮,而不是立刻换邮箱开新尝试 - Auto 模式下,普通 Step 4 失败仍会沿用当前邮箱回到 Step 1 重新开始当前轮;但若失败原因是 2925 主动要求“结束当前尝试”,则不会再回到 Step 1 重开,而是直接把错误抛给自动重试控制器
### Step 5 ### Step 5
@@ -500,7 +505,32 @@
- [luckmail-utils.js](c:/Users/projectf/Downloads/codex注册扩展/luckmail-utils.js) - [luckmail-utils.js](c:/Users/projectf/Downloads/codex注册扩展/luckmail-utils.js)
- LuckMail 相关后台领域逻辑仍在 [background.js](c:/Users/projectf/Downloads/codex注册扩展/background.js) - LuckMail 相关后台领域逻辑仍在 [background.js](c:/Users/projectf/Downloads/codex注册扩展/background.js)
### 7.4 iCloud ### 7.4 2925 账号池
组成:
- [mail2925-utils.js](c:/Users/projectf/Downloads/codex注册扩展/mail2925-utils.js)
- [background/mail-2925-session.js](c:/Users/projectf/Downloads/codex注册扩展/background/mail-2925-session.js)
- [sidepanel/mail-2925-manager.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/mail-2925-manager.js)
职责:
- `mail2925-utils.js`
统一承接 2925 账号池的归一化、冷却期判断、可用账号挑选、导入格式解析与列表更新。
- `background/mail-2925-session.js`
统一承接 2925 账号池的持久化、当前账号切换、cookie 清理登出、网页登录态确认、自动登录,以及命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
- `sidepanel/mail-2925-manager.js`
负责 2925 账号池的新增、导入、切换、手动登录、禁用、清冷却与删除。
链路:
1. 用户在 sidepanel 的 2925 账号池中保存 `email / password`
2. 只有当 sidepanel 中的 `mail2925UseAccountPool` 开关开启时,别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail`
3. 手动点击 `登录` 或自动流程进入 Step 4 / Step 8 前,只有在号池模式开启时,后台才会确保当前 2925 账号已登录网页邮箱
4. 一旦轮询期间出现“子邮箱已达上限邮箱”,且当前处于号池模式,后台会记录当前时间、把当前账号禁用 24 小时、自动切到下一个账号并重新登录
5. 切号完成后,后台直接抛出“结束当前尝试”错误;如果 Auto 开启了自动重试,现有控制器会自动进入下一次尝试
### 7.5 iCloud
组成: 组成:
+18 -10
View File
@@ -26,6 +26,7 @@
- `hotmail-utils.js`:Hotmail 账号与验证码提取相关的纯工具函数,负责账号筛选、验证码匹配、第三方接口数据归一化。 - `hotmail-utils.js`:Hotmail 账号与验证码提取相关的纯工具函数,负责账号筛选、验证码匹配、第三方接口数据归一化。
- `icloud-utils.js`:iCloud 隐私邮箱相关的纯工具函数,负责 host、别名列表、保留状态、已用状态等归一化。 - `icloud-utils.js`:iCloud 隐私邮箱相关的纯工具函数,负责 host、别名列表、保留状态、已用状态等归一化。
- `luckmail-utils.js`:LuckMail 相关的纯工具函数,负责邮箱购买记录、标签、邮件 cursor、验证码匹配等归一化。 - `luckmail-utils.js`:LuckMail 相关的纯工具函数,负责邮箱购买记录、标签、邮件 cursor、验证码匹配等归一化。
- `mail2925-utils.js`:2925 账号池相关的纯工具函数,负责账号归一化、冷却期判断、可用账号挑选、批量导入解析与列表更新。
- `managed-alias-utils.js`:共享 Gmail / 2925 的别名邮箱规则,负责解析基邮箱、校验“当前完整注册邮箱”是否仍与基邮箱兼容、生成 Gmail `+tag` 与 2925 随机后缀邮箱,并输出 sidepanel 可复用的 UI 文案。 - `managed-alias-utils.js`:共享 Gmail / 2925 的别名邮箱规则,负责解析基邮箱、校验“当前完整注册邮箱”是否仍与基邮箱兼容、生成 Gmail `+tag` 与 2925 随机后缀邮箱,并输出 sidepanel 可复用的 UI 文案。
- `manifest.json`:Chrome 扩展清单,声明权限、背景脚本、侧边栏、内容脚本与规则集。 - `manifest.json`:Chrome 扩展清单,声明权限、背景脚本、侧边栏、内容脚本与规则集。
- `microsoft-email.js`Microsoft Graph / Outlook 邮件读取辅助模块,负责刷新令牌换 token、邮箱夹轮询和验证码提取。 - `microsoft-email.js`Microsoft Graph / Outlook 邮件读取辅助模块,负责刷新令牌换 token、邮箱夹轮询和验证码提取。
@@ -41,23 +42,24 @@
## `background/` ## `background/`
- `background/account-run-history.js`:邮箱记录模块,负责以邮箱为主键维护最新记录(同邮箱后续状态覆盖),在步骤 2 设定邮箱后支持先写入“停止/未完成”占位状态,统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,支持清理记录,并在启用独立本地同步配置后把完整快照同步到本地 helper。 - `background/account-run-history.js`:邮箱记录模块,负责以邮箱为主键维护最新记录(同邮箱后续状态覆盖),在步骤 2 设定邮箱后支持先写入“停止/未完成”占位状态,统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,支持清理记录,并在启用独立本地同步配置后把完整快照同步到本地 helper。
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 `gmailBaseEmail``mail2925BaseEmail`,避免自动流程重置后丢失别名基邮箱配置 - `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 `gmailBaseEmail``mail2925BaseEmail` 与当前 2925 账号选择,避免自动流程重置后丢失别名基邮箱与 2925 切号上下文
- `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `apikey.qzz.io` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。 - `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `apikey.qzz.io` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口。 - `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口;当 provider 为 2925 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。 - `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息 - `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;当前额外接入 2925 账号池的新增、导入、切换、登录、禁用与删除消息。
- `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API 地址、步骤跳转相关判断。 - `background/navigation-utils.js`:导航与 URL 判断工具层,负责 callback、入口页、CPA / SUB2API 地址、步骤跳转相关判断。
- `background/panel-bridge.js`CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。 - `background/panel-bridge.js`CPA / SUB2API 面板桥接层,封装 OAuth 地址获取所需的页面打开、脚本注入和通信。
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail / 2925 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成。 - `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail / 2925 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 时,会先确保账号池中已选中可用账号
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。 - `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询,步骤 8 提交登录验证码后若页面进入 `add-phone / 手机号页`,会直接抛出 fatal 错误而不是把当前步骤视为成功,并在 2925 提交成功后异步触发“删除全部邮件”的清理消息 - `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试
## `background/steps/` ## `background/steps/`
- `background/steps/clear-login-cookies.js`:步骤 6 实现,负责登录前 Cookie 清理。 - `background/steps/clear-login-cookies.js`:步骤 6 实现,负责登录前 Cookie 清理。
- `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成。 - `background/steps/confirm-oauth.js`:步骤 9 实现,负责 OAuth 同意页按钮定位、点击、localhost 回调监听与回调完成。
- `background/steps/fetch-login-code.js`:步骤 8 实现,负责登录验证码阶段的邮箱轮询、验证码回填与回退控制;验证码获取后直接提交,不再在提交前回放步骤 7 刷新 OAuth;对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱。 - `background/steps/fetch-login-code.js`:步骤 8 实现,负责登录验证码阶段的邮箱轮询、验证码回填与回退控制;验证码获取后直接提交,不再在提交前回放步骤 7 刷新 OAuth;对非 2925 provider 会固定当前验证码页显示邮箱作为本次 Step 8 的目标邮箱;当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录
- `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口。 - `background/steps/fetch-signup-code.js`:步骤 4 实现,负责注册验证码阶段的页面准备与验证码流程入口;当 provider 为 2925 时,会在轮询前先确保当前 2925 账号已自动登录
- `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交。 - `background/steps/fill-password.js`:步骤 3 实现,负责密码生成、保存、回填与提交。
- `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本。 - `background/steps/fill-profile.js`:步骤 5 实现,负责姓名、生日填写并把资料提交给注册页内容脚本。
- `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。 - `background/steps/oauth-login.js`:步骤 7 实现,负责刷新 OAuth 链接、登录和确保进入验证码页;普通可恢复登录态失败会按上限重试,但一旦识别到认证流程进入 `add-phone / 手机号页`,会立即退出步骤 7 内部重试。
@@ -75,7 +77,7 @@
- `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。 - `content/icloud-mail.js`:iCloud 邮箱页面脚本,负责在 iCloud Mail 页面中读取邮件详情和验证码。
- `content/inbucket-mail.js`:Inbucket 邮箱轮询脚本,负责在 Inbucket 页面中读取、删除验证码邮件。 - `content/inbucket-mail.js`:Inbucket 邮箱轮询脚本,负责在 Inbucket 页面中读取、删除验证码邮件。
- `content/mail-163.js`163 / 163 VIP 邮箱轮询脚本,负责网页邮箱验证码读取和邮件清理。 - `content/mail-163.js`163 / 163 VIP 邮箱轮询脚本,负责网页邮箱验证码读取和邮件清理。
- `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱收件轮询、按步骤会话隔离“已试验证码”、在每次重发验证码之间执行一轮最多 15 次的邮箱刷新轮询、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理。 - `content/mail-2925.js`:2925 邮箱页面脚本,负责 2925 邮箱自动登录态确认、收件轮询、按步骤会话隔离“已试验证码”、在每次重发验证码之间执行一轮最多 15 次的邮箱刷新轮询、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理;若页面出现“子邮箱已达上限邮箱”提示,会立即上报后台进入切号链路
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。 - `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击。 - `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击。
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;当前承接步骤 10。 - `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;当前承接步骤 10。
@@ -113,11 +115,12 @@
- `sidepanel/hotmail-manager.js`:侧边栏 Hotmail 账号池管理器,负责列表、导入、验证、测试收信和批量操作。 - `sidepanel/hotmail-manager.js`:侧边栏 Hotmail 账号池管理器,负责列表、导入、验证、测试收信和批量操作。
- `sidepanel/icloud-manager.js`:侧边栏 iCloud 隐私邮箱管理器,负责列表、筛选、保留、删除和批量操作。 - `sidepanel/icloud-manager.js`:侧边栏 iCloud 隐私邮箱管理器,负责列表、筛选、保留、删除和批量操作。
- `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。 - `sidepanel/luckmail-manager.js`:侧边栏 LuckMail 管理器,负责邮箱列表、筛选、启停、保留与批量操作。
- `sidepanel/mail-2925-manager.js`:侧边栏 2925 账号池管理器,负责 2925 账号的新增、导入、切换、手动登录、启停、清冷却与删除。
- `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认。 - `sidepanel/account-records-manager.js`:侧边栏邮箱记录面板管理器,负责“记录”按钮、覆盖层开关、分页列表、成功/失败/停止统计摘要和清理确认。
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行的禁用与隐藏。 - `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行的禁用与隐藏。
- `sidepanel/sidepanel.css`:侧边栏样式文件。 - `sidepanel/sidepanel.css`:侧边栏样式文件。
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”。 - `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,别名基邮箱行右侧会显示“号池”开关,开启后输入框切换为号池邮箱下拉选择,并显示 2925 账号池管理区
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 manager;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上。 - `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 manager;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 2925 号池开关开启时,当前账号变化会同步映射到 2925 基邮箱下拉选择,关闭时则回退到原来的手填基邮箱路线
- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Pro` / `v` 双版本族排序、缓存读取与版本展示。 - `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Pro` / `v` 双版本族排序、缓存读取与版本展示。
## `tests/` ## `tests/`
@@ -125,6 +128,7 @@
- `tests/activation-utils.test.js`:测试内容脚本激活策略与平台回调可恢复错误判断。 - `tests/activation-utils.test.js`:测试内容脚本激活策略与平台回调可恢复错误判断。
- `tests/auth-page-recovery.test.js`:测试认证页共享恢复层的重试页识别与恢复行为。 - `tests/auth-page-recovery.test.js`:测试认证页共享恢复层的重试页识别与恢复行为。
- `tests/auto-run-fresh-attempt-reset.test.js`:测试自动运行在新一轮开始前会重置旧运行时上下文,并补充 `gmailBaseEmail` / `mail2925BaseEmail` 在 fresh reset 后仍被保留的回归验证。 - `tests/auto-run-fresh-attempt-reset.test.js`:测试自动运行在新一轮开始前会重置旧运行时上下文,并补充 `gmailBaseEmail` / `mail2925BaseEmail` 在 fresh reset 后仍被保留的回归验证。
- `tests/auto-run-step4-mail2925-thread-terminate.test.js`:测试步骤 4 命中 2925“结束当前尝试”错误时,不会再沿用当前邮箱回到步骤 1 重开,而是直接把错误抛给自动重试控制器。
- `tests/auto-run-timer-session-guard.test.js`:测试自动运行的旧计时计划在 Stop 使 session 失效后,不能再把已停止的自动流程重新拉起。 - `tests/auto-run-timer-session-guard.test.js`:测试自动运行的旧计时计划在 Stop 使 session 失效后,不能再把已停止的自动流程重新拉起。
- `tests/auto-run-add-phone-stop.test.js`:测试自动运行控制器在开启自动重试时,遇到 `add-phone / 手机号页` 会直接结束当前轮并继续下一轮,而不是把整条自动流程暂停。 - `tests/auto-run-add-phone-stop.test.js`:测试自动运行控制器在开启自动重试时,遇到 `add-phone / 手机号页` 会直接结束当前轮并继续下一轮,而不是把整条自动流程暂停。
- `tests/auto-run-step6-restart.test.js`:测试自动运行在后半段授权链路遇错时会回到步骤 7 重开,并在命中 add-phone 或泛化手机号页 fatal 错误时停止重开。 - `tests/auto-run-step6-restart.test.js`:测试自动运行在后半段授权链路遇错时会回到步骤 7 重开,并在命中 add-phone 或泛化手机号页 fatal 错误时停止重开。
@@ -138,6 +142,8 @@
- `tests/background-icloud-mail-provider.test.js`:测试 iCloud 邮箱 provider 配置解析。 - `tests/background-icloud-mail-provider.test.js`:测试 iCloud 邮箱 provider 配置解析。
- `tests/background-logging-status-module.test.js`:测试日志 / 状态模块已接入且导出工厂。 - `tests/background-logging-status-module.test.js`:测试日志 / 状态模块已接入且导出工厂。
- `tests/background-luckmail.test.js`:测试 LuckMail 相关后台逻辑,如购买、复用、标记已用与重置。 - `tests/background-luckmail.test.js`:测试 LuckMail 相关后台逻辑,如购买、复用、标记已用与重置。
- `tests/background-mail2925-session-module.test.js`:测试 2925 会话模块已接入且导出工厂,并覆盖命中上限后“禁用 24 小时 + 切下一个号 + 自动登录”的核心链路。
- `tests/background-mail2925-signup-flow.test.js`:测试注册页辅助层在 2925 provider 下,会先确保账号池里已分配可用账号,再继续生成别名邮箱。
- `tests/background-message-router-module.test.js`:测试消息路由模块已接入且导出工厂。 - `tests/background-message-router-module.test.js`:测试消息路由模块已接入且导出工厂。
- `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑。 - `tests/background-message-router-step2-skip.test.js`:测试步骤 2 直接落到验证码页时的跳步与状态保护逻辑。
- `tests/background-navigation-utils-module.test.js`:测试导航工具模块已接入且导出工厂。 - `tests/background-navigation-utils-module.test.js`:测试导航工具模块已接入且导出工厂。
@@ -161,6 +167,7 @@
- `tests/icloud-utils.test.js`:测试 iCloud 工具层的 host、别名列表、保留状态与筛选逻辑。 - `tests/icloud-utils.test.js`:测试 iCloud 工具层的 host、别名列表、保留状态与筛选逻辑。
- `tests/luckmail-utils.test.js`:测试 LuckMail 工具层的购买记录、邮件、游标和验证码匹配逻辑。 - `tests/luckmail-utils.test.js`:测试 LuckMail 工具层的购买记录、邮件、游标和验证码匹配逻辑。
- `tests/mail-2925-content.test.js`:测试 2925 邮箱内容脚本的轮询快照、忽略邮箱对比、按步骤会话隔离已试验证码、单封邮件删除与整箱删除逻辑。 - `tests/mail-2925-content.test.js`:测试 2925 邮箱内容脚本的轮询快照、忽略邮箱对比、按步骤会话隔离已试验证码、单封邮件删除与整箱删除逻辑。
- `tests/mail2925-utils.test.js`:测试 2925 账号池工具层的账号归一化、冷却期判断、可用账号挑选与批量导入解析。
- `tests/managed-alias-utils.test.js`:覆盖 Gmail / 2925 共享别名工具的解析、生成、兼容性判断。 - `tests/managed-alias-utils.test.js`:覆盖 Gmail / 2925 共享别名工具的解析、生成、兼容性判断。
- `tests/microsoft-email.test.js`:测试 Microsoft 邮件拉取与验证码提取逻辑。 - `tests/microsoft-email.test.js`:测试 Microsoft 邮件拉取与验证码提取逻辑。
- `tests/sidepanel-hotmail-manager.test.js`:测试侧边栏 Hotmail 管理器模块接线与空态渲染。 - `tests/sidepanel-hotmail-manager.test.js`:测试侧边栏 Hotmail 管理器模块接线与空态渲染。
@@ -170,6 +177,7 @@
- `tests/sidepanel-icloud-manager.test.js`:测试侧边栏 iCloud 管理器模块接线与空态渲染。 - `tests/sidepanel-icloud-manager.test.js`:测试侧边栏 iCloud 管理器模块接线与空态渲染。
- `tests/sidepanel-icloud-provider.test.js`:测试侧边栏 iCloud 登录地址解析逻辑。 - `tests/sidepanel-icloud-provider.test.js`:测试侧边栏 iCloud 登录地址解析逻辑。
- `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。 - `tests/sidepanel-luckmail-manager.test.js`:测试侧边栏 LuckMail 管理器模块接线与空态渲染。
- `tests/sidepanel-mail2925-manager.test.js`:测试侧边栏 2925 管理器模块接线、脚本加载顺序与空态渲染。
- `tests/signup-entry-diagnostics.test.js`:测试注册入口诊断快照输出。 - `tests/signup-entry-diagnostics.test.js`:测试注册入口诊断快照输出。
- `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。 - `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。
- `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。 - `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。