feat: support custom email pool mail assignment in auto-run

- 合并 PR #138 的核心改动:新增自定义邮箱池生成方式,支持按轮次为自动流程分配预置注册邮箱\n- 本地补充修复:吸收 dev 上最新的 sidepanel 与收码链路改动,并修正生成邮箱 helper 对运行态参数的判断与文档同步\n- 影响范围:邮箱生成配置、auto-run 轮次分配、generated-email helper、README 与根文档、相关回归测试
This commit is contained in:
QLHazyCoder
2026-04-23 20:43:26 +08:00
committed by GitHub
10 changed files with 732 additions and 31 deletions
+16 -5
View File
@@ -110,6 +110,16 @@
- `2925` 旧的“只填前缀”使用方式已经不再推荐,应该改为填写完整基邮箱
- 如果你手动填写了与当前 `Gmail / 2925 provide` 基邮箱不匹配的完整邮箱,侧边栏会在保存或执行 Step 3 时拦截
## 2026-04-23 更新补充:自定义邮箱池
本次版本新增 `自定义邮箱池` 生成方式,用于把一批已经准备好的邮箱按顺序分配给自动流程:
-`邮箱生成` 中选择 `自定义邮箱池`
- 在新出现的 `邮箱池` 文本框里按“每行一个邮箱”填写
- `Auto` 运行次数会自动跟随邮箱池数量,无需再手动对齐轮数
- 同一目标轮次的失败重试会继续复用当前轮邮箱,不会提前跳到下一个
- 实际收码仍然走当前 `Mail` 对应的邮箱服务,因此应保证邮箱池里的地址与当前收码链路匹配
## 快速开始
如果你只是想先跑通一套最稳的组合,建议直接按下面三种方案之一配置。
@@ -118,9 +128,9 @@
1. `CPA` 填你的管理面板 OAuth 页面地址
2. `Mail` 选择 `QQ Mail``163 Mail``163 VIP Mail`
3. `邮箱生成` 选择 `DuckDuckGo``Cloudflare`
3. `邮箱生成` 选择 `DuckDuckGo``Cloudflare``自定义邮箱池`
4. 若你选择 `Cloudflare`,先按下文把 Cloudflare Email Routing 配好
5. 点击 `获取` 生成邮箱,或手动粘贴一个你能收信的邮箱
5. 若你选择 `自定义邮箱池`,就在 `邮箱池` 中按行填入邮箱;否则点击 `获取` 生成邮箱,或手动粘贴一个你能收信的邮箱
6. 先单步验证 `Step 1 ~ Step 4`
7. 验证没问题后再点右上角 `Auto`
@@ -349,17 +359,18 @@ Step 3 使用的注册邮箱。
来源有两种:
- 手动粘贴
- 点击 `获取` 自动生成邮箱(DuckDuckGo Cloudflare
- 按当前生成方式自动生成或分配邮箱(DuckDuckGo / Cloudflare / 自定义邮箱池
注意:
-`邮箱生成 = Cloudflare`,插件里只需要维护 `CF 域名`
-`邮箱生成 = 自定义邮箱池`,需要在 `邮箱池` 文本框中按行维护邮箱列表
- `CF 域名` 支持保存多个,并通过下拉框切换当前要生成的域名
- Cloudflare 侧的转发规则、Catch-all、路由目标邮箱等,都需要你自己提前在 Cloudflare 后台配置好
-`Mail = Hotmail` 时,这个输入框由账号池自动同步当前账号邮箱
-`Mail = Hotmail` 时,Step 3 会直接使用 Hotmail 账号池里的邮箱;`Duck / Cloudflare` 不参与自动邮箱生成
- 若你准备走 `Cloudflare`,更推荐把 `Mail` 设为 `QQ / 163 / 163 VIP``Inbucket` 仅在它能真实接收外部邮件并完成 Cloudflare 验证时再使用
- 当前 `Auto` 按钮只负责 DuckDuckGo 地址获取
- `Auto` 会按当前“邮箱生成”配置自动获取或分配邮箱;若当前是 `自定义邮箱池`,则会按邮箱池顺序取用
- 如果你使用 Inbucket,它只是验证码收件箱,不会自动生成 Inbucket 地址
### `邮箱生成 = Cloudflare` 时的配置
@@ -509,7 +520,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
1. Step 1 打开 `https://chatgpt.com/`
2. 根据 `Mail` 选择邮箱来源
3. 如果 `Mail = Hotmail`,会从账号池自动分配一个可用账号
4. 如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取邮箱(Duck / Cloudflare / iCloud 等)
4. 如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取或分配邮箱(Duck / Cloudflare / iCloud / 自定义邮箱池等)
5. Step 2 点击注册、填写邮箱,并按真实落地页进入密码页或直接进入邮箱验证码页
6. 如果自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue`
7. 继续执行 Step 3 ~ Step 10
+121 -1
View File
@@ -139,10 +139,12 @@ const ICLOUD_LOGIN_URLS = [
];
const ICLOUD_PROVIDER = 'icloud';
const GMAIL_PROVIDER = 'gmail';
const GMAIL_ALIAS_GENERATOR = 'gmail-alias';
const HOTMAIL_PROVIDER = 'hotmail-api';
const LUCKMAIL_PROVIDER = 'luckmail-api';
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool';
const HOTMAIL_MAILBOXES = ['INBOX', 'Junk'];
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const CLOUDFLARE_SECURITY_BLOCK_ERROR_PREFIX = 'CF_SECURITY_BLOCKED::';
@@ -266,6 +268,7 @@ const PERSISTED_SETTING_DEFAULTS = {
mail2925Mode: DEFAULT_MAIL_2925_MODE,
mail2925UseAccountPool: false,
emailGenerator: 'duck',
customEmailPool: [],
autoDeleteUsedIcloudAlias: false,
icloudHostPreference: 'auto',
accountRunHistoryTextEnabled: false,
@@ -647,9 +650,21 @@ function getAutoRunTimerStatusPayload(plan) {
function normalizeEmailGenerator(value = '') {
const normalized = String(value || '').trim().toLowerCase();
const customEmailPoolGenerator = typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
? CUSTOM_EMAIL_POOL_GENERATOR
: 'custom-pool';
const gmailAliasGenerator = typeof GMAIL_ALIAS_GENERATOR === 'string'
? GMAIL_ALIAS_GENERATOR
: 'gmail-alias';
if (normalized === 'custom' || normalized === 'manual') {
return 'custom';
}
if (normalized === gmailAliasGenerator) {
return gmailAliasGenerator;
}
if (normalized === customEmailPoolGenerator) {
return customEmailPoolGenerator;
}
if (normalized === 'icloud') {
return 'icloud';
}
@@ -658,6 +673,36 @@ function normalizeEmailGenerator(value = '') {
return 'duck';
}
function normalizeCustomEmailPool(value = []) {
const source = Array.isArray(value)
? value
: String(value || '').split(/[\r\n,;]+/);
return source
.map((item) => String(item || '').trim().toLowerCase())
.filter((item) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(item));
}
function isCustomEmailPoolGenerator(stateOrValue = {}) {
const generator = typeof stateOrValue === 'string'
? stateOrValue
: stateOrValue?.emailGenerator;
const customEmailPoolGenerator = typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
? CUSTOM_EMAIL_POOL_GENERATOR
: 'custom-pool';
return normalizeEmailGenerator(generator) === customEmailPoolGenerator;
}
function getCustomEmailPool(state = {}) {
return normalizeCustomEmailPool(state?.customEmailPool);
}
function getCustomEmailPoolEmailForRun(state = {}, targetRun = 1) {
const entries = getCustomEmailPool(state);
const numericRun = Math.max(1, Math.floor(Number(targetRun) || 1));
return entries[numericRun - 1] || '';
}
function normalizePanelMode(value = '') {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'sub2api') {
@@ -913,6 +958,8 @@ function normalizePersistentSettingValue(key, value) {
return Boolean(value);
case 'emailGenerator':
return normalizeEmailGenerator(value);
case 'customEmailPool':
return normalizeCustomEmailPool(value);
case 'autoDeleteUsedIcloudAlias':
case 'accountRunHistoryTextEnabled':
case 'cloudflareTempEmailUseRandomSubdomain':
@@ -2151,6 +2198,18 @@ function parseGmailBaseEmail(rawValue) {
}
function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) {
if (
stateOrProvider
&& typeof stateOrProvider === 'object'
&& !Array.isArray(stateOrProvider)
&& normalizeEmailGenerator(stateOrProvider.emailGenerator) === (
typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
? CUSTOM_EMAIL_POOL_GENERATOR
: 'custom-pool'
)
) {
return false;
}
const provider = typeof stateOrProvider === 'string'
? stateOrProvider
: stateOrProvider?.mailProvider;
@@ -2308,6 +2367,18 @@ function getManagedAliasBaseEmail(state = {}, provider = state?.mailProvider) {
}
function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) {
if (
stateOrProvider
&& typeof stateOrProvider === 'object'
&& !Array.isArray(stateOrProvider)
&& normalizeEmailGenerator(stateOrProvider.emailGenerator) === (
typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
? CUSTOM_EMAIL_POOL_GENERATOR
: 'custom-pool'
)
) {
return false;
}
const provider = typeof stateOrProvider === 'string'
? stateOrProvider
: stateOrProvider?.mailProvider;
@@ -5051,7 +5122,12 @@ async function handleStepData(step, payload) {
});
}
await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
if (shouldUseCustomRegistrationEmail(latestState) && latestState.email) {
const shouldClearCustomPoolEmail = String(latestState?.emailGenerator || '').trim().toLowerCase() === (
typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
? CUSTOM_EMAIL_POOL_GENERATOR
: 'custom-pool'
);
if ((shouldUseCustomRegistrationEmail(latestState) || shouldClearCustomPoolEmail) && latestState.email) {
await setEmailStateSilently(null);
}
break;
@@ -5465,9 +5541,21 @@ async function executeStepAndWait(step, delayAfter = 2000) {
}
function getEmailGeneratorLabel(generator) {
const customEmailPoolGenerator = typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
? CUSTOM_EMAIL_POOL_GENERATOR
: 'custom-pool';
const gmailAliasGenerator = typeof GMAIL_ALIAS_GENERATOR === 'string'
? GMAIL_ALIAS_GENERATOR
: 'gmail-alias';
if (generator === 'custom') {
return '自定义邮箱';
}
if (generator === gmailAliasGenerator) {
return 'Gmail +tag 邮箱';
}
if (generator === customEmailPoolGenerator) {
return '自定义邮箱池';
}
if (generator === 'icloud') {
return 'iCloud 隐私邮箱';
}
@@ -5568,11 +5656,13 @@ const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGenerat
buildGeneratedAliasEmail,
buildCloudflareTempEmailHeaders,
CLOUDFLARE_TEMP_EMAIL_GENERATOR,
CUSTOM_EMAIL_POOL_GENERATOR,
DUCK_AUTOFILL_URL,
fetch,
fetchIcloudHideMyEmail,
getCloudflareTempEmailAddressFromResponse,
getCloudflareTempEmailConfig,
getCustomEmailPoolEmail: getCustomEmailPoolEmailForRun,
getState,
ensureMail2925AccountForFlow,
joinCloudflareTempEmailUrl,
@@ -5815,6 +5905,21 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
return currentState.email;
}
if (isCustomEmailPoolGenerator(currentState)) {
const queuedEmail = getCustomEmailPoolEmailForRun(currentState, targetRun);
if (!queuedEmail) {
const poolSize = getCustomEmailPool(currentState).length;
throw new Error(
poolSize > 0
? `自定义邮箱池第 ${targetRun} 个邮箱不存在,请检查邮箱池数量是否与自动轮数一致。`
: '自定义邮箱池为空,请先至少填写 1 个邮箱。'
);
}
await setEmailState(queuedEmail);
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:自定义邮箱池已就绪:${queuedEmail}(第 ${attemptRuns} 次尝试)===`, 'ok');
return queuedEmail;
}
if (shouldUseCustomRegistrationEmail(currentState)) {
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先填写自定义注册邮箱,然后继续 ===`, 'warn');
await broadcastAutoRunStatus('waiting_email', {
@@ -5933,6 +6038,21 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
return currentState.email;
}
if (isCustomEmailPoolGenerator(currentState)) {
const queuedEmail = getCustomEmailPoolEmailForRun(currentState, targetRun);
if (!queuedEmail) {
const poolSize = getCustomEmailPool(currentState).length;
throw new Error(
poolSize > 0
? `自定义邮箱池第 ${targetRun} 个邮箱不存在,请检查邮箱池数量是否与自动轮数一致。`
: '自定义邮箱池为空,请先至少填写 1 个邮箱。'
);
}
await setEmailState(queuedEmail);
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:自定义邮箱池已就绪:${queuedEmail}(第 ${attemptRuns} 次尝试)===`, 'ok');
return queuedEmail;
}
if (shouldUseCustomRegistrationEmail(currentState)) {
await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮已暂停:请先填写自定义注册邮箱,然后继续 ===`, 'warn');
await broadcastAutoRunStatus('waiting_email', {
+47 -5
View File
@@ -7,11 +7,13 @@
buildGeneratedAliasEmail,
buildCloudflareTempEmailHeaders,
CLOUDFLARE_TEMP_EMAIL_GENERATOR,
CUSTOM_EMAIL_POOL_GENERATOR,
DUCK_AUTOFILL_URL,
fetch,
fetchIcloudHideMyEmail,
getCloudflareTempEmailAddressFromResponse,
getCloudflareTempEmailConfig,
getCustomEmailPoolEmail,
getState,
ensureMail2925AccountForFlow,
joinCloudflareTempEmailUrl,
@@ -189,6 +191,24 @@
return result.email;
}
async function fetchCustomEmailPoolEmail(state, options = {}) {
throwIfStopped();
const latestState = state || await getState();
const requestedIndex = Math.max(0, Math.floor(Number(options.poolIndex) || 0));
const email = String(getCustomEmailPoolEmail?.(latestState, requestedIndex + 1) || '').trim().toLowerCase();
if (!email) {
throw new Error(
requestedIndex > 0
? `自定义邮箱池第 ${requestedIndex + 1} 个邮箱不存在,请检查邮箱池配置。`
: '自定义邮箱池为空,请先至少填写 1 个邮箱。'
);
}
await setEmailState(email);
await addLog(`自定义邮箱池:已取用 ${email}`, 'ok');
return email;
}
async function fetchManagedAliasEmail(state, options = {}) {
throwIfStopped();
const provider = String(options.mailProvider || state?.mailProvider || '').trim().toLowerCase();
@@ -234,21 +254,42 @@
const mail2925Mode = options.mail2925Mode !== undefined
? options.mail2925Mode
: currentState.mail2925Mode;
if (isGeneratedAliasProvider?.(provider, mail2925Mode)) {
return fetchManagedAliasEmail(currentState, options);
}
const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator);
const mergedState = {
...currentState,
mailProvider: provider || currentState.mailProvider,
mail2925Mode,
emailGenerator: generator,
};
if (options.gmailBaseEmail !== undefined) {
mergedState.gmailBaseEmail = String(options.gmailBaseEmail || '').trim();
}
if (options.mail2925BaseEmail !== undefined) {
mergedState.mail2925BaseEmail = String(options.mail2925BaseEmail || '').trim();
}
if (options.customEmailPool !== undefined) {
mergedState.customEmailPool = options.customEmailPool;
}
if (generator === 'custom') {
throw new Error('当前邮箱生成方式为自定义邮箱,请直接填写注册邮箱。');
}
if (generator === CUSTOM_EMAIL_POOL_GENERATOR) {
return fetchCustomEmailPoolEmail(mergedState, options);
}
const shouldUseManagedAlias = typeof isGeneratedAliasProvider === 'function'
? isGeneratedAliasProvider(mergedState, mail2925Mode)
: false;
if (shouldUseManagedAlias) {
return fetchManagedAliasEmail(mergedState, options);
}
if (generator === 'icloud') {
return fetchIcloudHideMyEmail();
}
if (generator === 'cloudflare') {
return fetchCloudflareEmail(currentState, options);
return fetchCloudflareEmail(mergedState, options);
}
if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) {
return fetchCloudflareTempEmailAddress(currentState, options);
return fetchCloudflareTempEmailAddress(mergedState, options);
}
return fetchDuckEmail(options);
}
@@ -256,6 +297,7 @@
return {
ensureCloudflareTempEmailConfig,
fetchCloudflareEmail,
fetchCustomEmailPoolEmail,
fetchCloudflareTempEmailAddress,
fetchDuckEmail,
fetchGeneratedEmail,
+7
View File
@@ -240,12 +240,19 @@
<div class="data-row" id="row-email-generator">
<span class="data-label">邮箱生成</span>
<select id="select-email-generator" class="data-select">
<option value="gmail-alias">Gmail +tag</option>
<option value="duck">DuckDuckGo</option>
<option value="custom-pool">自定义邮箱池</option>
<option value="cloudflare">Cloudflare</option>
<option value="icloud">iCloud 隐私邮箱</option>
<option value="cloudflare-temp-email">Cloudflare Temp Email</option>
</select>
</div>
<div class="data-row data-check-row" id="row-custom-email-pool" style="display:none;">
<span class="data-label">邮箱池</span>
<textarea id="input-custom-email-pool" class="data-textarea"
placeholder="每行一个邮箱,例如&#10;alias001@gmail.com&#10;alias002@gmail.com"></textarea>
</div>
<div class="data-row" id="row-cf-domain" style="display:none;">
<span class="data-label">CF 域名</span>
<div class="data-inline">
+171 -17
View File
@@ -106,6 +106,8 @@ const rowMail2925PoolSettings = document.getElementById('row-mail2925-pool-setti
const mail2925ModeButtons = Array.from(document.querySelectorAll('[data-mail2925-mode]'));
const rowEmailGenerator = document.getElementById('row-email-generator');
const selectEmailGenerator = document.getElementById('select-email-generator');
const rowCustomEmailPool = document.getElementById('row-custom-email-pool');
const inputCustomEmailPool = document.getElementById('input-custom-email-pool');
const rowTempEmailBaseUrl = document.getElementById('row-temp-email-base-url');
const inputTempEmailBaseUrl = document.getElementById('input-temp-email-base-url');
const rowTempEmailAdminAuth = document.getElementById('row-temp-email-admin-auth');
@@ -264,7 +266,9 @@ const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
const ICLOUD_PROVIDER = 'icloud';
const GMAIL_PROVIDER = 'gmail';
const GMAIL_ALIAS_GENERATOR = 'gmail-alias';
const LUCKMAIL_PROVIDER = 'luckmail-api';
const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool';
const DEFAULT_LUCKMAIL_BASE_URL = 'https://mails.luckyous.com';
const DEFAULT_LUCKMAIL_EMAIL_TYPE = 'ms_graph';
const DISPLAY_TIMEZONE = 'Asia/Shanghai';
@@ -473,14 +477,14 @@ function getCurrentRegistrationEmailUiCopy() {
if (isCustomMailProvider()) {
return getCustomMailProviderUiCopy();
}
if (isManagedAliasProvider()) {
if (usesGeneratedAliasMailProvider()) {
return getManagedAliasProviderUiCopy();
}
return getEmailGeneratorUiCopy();
}
function isCurrentRegistrationEmailCompatible(email = inputEmail.value.trim(), provider = selectMailProvider.value, state = latestState) {
if (!isManagedAliasProvider(provider) || !email) {
if (!usesGeneratedAliasMailProvider(provider, getSelectedMail2925Mode()) || !email) {
return true;
}
const baseEmail = getManagedAliasBaseEmailForProvider(provider, state);
@@ -620,8 +624,19 @@ const CLOUDFLARE_TEMP_EMAIL_REPOSITORY_URL = 'https://github.com/dreamhunter2333
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
function usesGeneratedAliasMailProvider(provider, mail2925Mode = getSelectedMail2925Mode()) {
return isManagedAliasProvider(provider, mail2925Mode);
function usesGeneratedAliasMailProvider(
provider,
mail2925Mode = getSelectedMail2925Mode(),
generator = undefined
) {
const customEmailPoolGenerator = typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
? CUSTOM_EMAIL_POOL_GENERATOR
: 'custom-pool';
const resolvedGenerator = generator !== undefined
? generator
: (typeof getSelectedEmailGenerator === 'function' ? getSelectedEmailGenerator() : '');
return resolvedGenerator !== customEmailPoolGenerator
&& isManagedAliasProvider(provider, mail2925Mode);
}
function parseGmailBaseEmail(rawValue = '') {
@@ -1253,7 +1268,37 @@ function formatAutoStepDelayInputValue(value) {
return normalized === null ? '' : String(normalized);
}
function normalizeCustomEmailPoolEntries(value = '') {
const source = Array.isArray(value)
? value
: String(value || '').split(/[\r\n,;]+/);
return source
.map((item) => String(item || '').trim().toLowerCase())
.filter((item) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(item));
}
function usesCustomEmailPoolGenerator(provider = selectMailProvider.value) {
return !isCustomMailProvider(provider)
&& !isLuckmailProvider(provider)
&& getSelectedEmailGenerator() === CUSTOM_EMAIL_POOL_GENERATOR;
}
function getCustomEmailPoolSize() {
return normalizeCustomEmailPoolEntries(inputCustomEmailPool?.value).length;
}
function syncRunCountFromCustomEmailPool() {
if (!usesCustomEmailPoolGenerator()) {
return;
}
inputRunCount.value = String(getCustomEmailPoolSize());
}
function getRunCountValue() {
if (typeof usesCustomEmailPoolGenerator === 'function' && usesCustomEmailPoolGenerator()) {
return getCustomEmailPoolSize();
}
return Math.max(1, parseInt(inputRunCount.value, 10) || 1);
}
@@ -1581,6 +1626,9 @@ function collectSettingsPayload() {
mail2925UseAccountPool,
currentMail2925AccountId: String(latestState?.currentMail2925AccountId || '').trim(),
emailGenerator: selectEmailGenerator.value,
customEmailPool: typeof normalizeCustomEmailPoolEntries === 'function'
? normalizeCustomEmailPoolEntries(inputCustomEmailPool?.value)
: [],
autoDeleteUsedIcloudAlias: checkboxAutoDeleteIcloud?.checked,
icloudHostPreference: selectIcloudHostPreference?.value || 'auto',
...(contributionModeEnabled ? {} : {
@@ -1834,10 +1882,11 @@ function applyAutoRunStatus(payload = currentAutoRun) {
setSettingsCardLocked(settingsCardLocked);
inputRunCount.disabled = currentAutoRun.autoRunning;
inputRunCount.disabled = currentAutoRun.autoRunning || usesCustomEmailPoolGenerator();
btnAutoRun.disabled = currentAutoRun.autoRunning;
btnFetchEmail.disabled = locked
|| isCustomMailProvider();
|| isCustomMailProvider()
|| usesCustomEmailPoolGenerator();
inputEmail.disabled = locked;
inputAutoSkipFailures.disabled = scheduled;
@@ -1875,7 +1924,7 @@ function applyAutoRunStatus(payload = currentAutoRun) {
setDefaultAutoRunButton();
inputEmail.disabled = false;
if (!locked) {
btnFetchEmail.disabled = isCustomMailProvider();
btnFetchEmail.disabled = isCustomMailProvider() || usesCustomEmailPoolGenerator();
}
break;
}
@@ -1967,7 +2016,13 @@ function applySettingsState(state) {
setMail2925Mode(state?.mail2925Mode);
{
const restoredEmailGenerator = String(state?.emailGenerator || '').trim().toLowerCase();
if (restoredEmailGenerator === 'icloud') {
if (restoredMailProvider === GMAIL_PROVIDER) {
selectEmailGenerator.value = restoredEmailGenerator === CUSTOM_EMAIL_POOL_GENERATOR
? CUSTOM_EMAIL_POOL_GENERATOR
: GMAIL_ALIAS_GENERATOR;
} else if (restoredEmailGenerator === CUSTOM_EMAIL_POOL_GENERATOR) {
selectEmailGenerator.value = CUSTOM_EMAIL_POOL_GENERATOR;
} else if (restoredEmailGenerator === 'icloud') {
selectEmailGenerator.value = 'icloud';
} else if (restoredEmailGenerator === 'cloudflare') {
selectEmailGenerator.value = 'cloudflare';
@@ -2003,6 +2058,9 @@ function applySettingsState(state) {
setManagedAliasBaseEmailInputForProvider(restoredMailProvider, state);
inputInbucketHost.value = state?.inbucketHost || '';
inputInbucketMailbox.value = state?.inbucketMailbox || '';
if (inputCustomEmailPool) {
inputCustomEmailPool.value = normalizeCustomEmailPoolEntries(state?.customEmailPool).join('\n');
}
setHotmailServiceMode(state?.hotmailServiceMode);
inputHotmailRemoteBaseUrl.value = state?.hotmailRemoteBaseUrl || '';
inputHotmailLocalBaseUrl.value = state?.hotmailLocalBaseUrl || '';
@@ -2587,6 +2645,12 @@ function getSelectedEmailGenerator() {
if (generator === 'custom' || generator === 'manual') {
return 'custom';
}
if (generator === GMAIL_ALIAS_GENERATOR) {
return GMAIL_ALIAS_GENERATOR;
}
if (generator === CUSTOM_EMAIL_POOL_GENERATOR) {
return CUSTOM_EMAIL_POOL_GENERATOR;
}
if (generator === 'icloud') {
return 'icloud';
}
@@ -2599,6 +2663,22 @@ function getEmailGeneratorUiCopy() {
if (getSelectedEmailGenerator() === 'custom') {
return getCustomMailProviderUiCopy();
}
if (getSelectedEmailGenerator() === GMAIL_ALIAS_GENERATOR) {
return {
buttonLabel: '生成',
placeholder: '步骤 3 自动生成 Gmail +tag 邮箱并回填',
successVerb: '生成',
label: 'Gmail +tag 邮箱',
};
}
if (getSelectedEmailGenerator() === CUSTOM_EMAIL_POOL_GENERATOR) {
return {
buttonLabel: '取下一个',
placeholder: '按邮箱池顺序自动回填,也可以手动粘贴当前轮邮箱',
successVerb: '取用',
label: '自定义邮箱池',
};
}
if (getSelectedEmailGenerator() === 'icloud') {
return {
buttonLabel: '获取',
@@ -2839,13 +2919,35 @@ function updateMailProviderUI() {
const useMail2925 = selectMailProvider.value === '2925';
const useMail2925AccountPool = useMail2925 && Boolean(inputMail2925UseAccountPool?.checked);
const mail2925Mode = getSelectedMail2925Mode();
const useGeneratedAlias = usesGeneratedAliasMailProvider(selectMailProvider.value, mail2925Mode);
const gmailAliasGenerator = typeof GMAIL_ALIAS_GENERATOR === 'string'
? GMAIL_ALIAS_GENERATOR
: 'gmail-alias';
const customEmailPoolGenerator = typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
? CUSTOM_EMAIL_POOL_GENERATOR
: 'custom-pool';
const gmailOnlyGenerators = new Set([gmailAliasGenerator, customEmailPoolGenerator]);
Array.from(selectEmailGenerator?.options || []).forEach((option) => {
if (!option) return;
if (useGmail) {
option.hidden = !gmailOnlyGenerators.has(String(option.value || '').trim().toLowerCase());
return;
}
option.hidden = String(option.value || '').trim().toLowerCase() === gmailAliasGenerator;
});
if (useGmail && !gmailOnlyGenerators.has(String(selectEmailGenerator.value || '').trim().toLowerCase())) {
selectEmailGenerator.value = gmailAliasGenerator;
}
if (!useGmail && String(selectEmailGenerator.value || '').trim().toLowerCase() === gmailAliasGenerator) {
selectEmailGenerator.value = 'duck';
}
const selectedGenerator = getSelectedEmailGenerator();
const useGeneratedAlias = usesGeneratedAliasMailProvider(selectMailProvider.value, mail2925Mode, selectedGenerator);
const useInbucket = selectMailProvider.value === 'inbucket';
const useHotmail = selectMailProvider.value === 'hotmail-api';
const useLuckmail = isLuckmailProvider();
const useCustomEmail = isCustomMailProvider();
const useIcloudProvider = isIcloudMailProvider();
const useEmailGenerator = !useHotmail && !useLuckmail && !useGeneratedAlias && !useCustomEmail;
const useEmailGenerator = !useHotmail && !useLuckmail && !useCustomEmail && (!useGeneratedAlias || useGmail);
const useCloudflareTempEmailProvider = selectMailProvider.value === 'cloudflare-temp-email';
const aliasUiCopy = useGeneratedAlias
? getManagedAliasProviderUiCopy(selectMailProvider.value, mail2925Mode)
@@ -2862,7 +2964,7 @@ function updateMailProviderUI() {
const hotmailServiceMode = getSelectedHotmailServiceMode();
rowInbucketHost.style.display = useInbucket ? '' : 'none';
rowInbucketMailbox.style.display = useInbucket ? '' : 'none';
const selectedGenerator = getSelectedEmailGenerator();
const useCustomEmailPool = useEmailGenerator && selectedGenerator === customEmailPoolGenerator;
const useCloudflare = selectedGenerator === 'cloudflare';
const useIcloud = selectedGenerator === 'icloud';
const useCloudflareTempEmailGenerator = selectedGenerator === 'cloudflare-temp-email';
@@ -2874,6 +2976,9 @@ function updateMailProviderUI() {
if (rowEmailGenerator) {
rowEmailGenerator.style.display = useEmailGenerator ? '' : 'none';
}
if (typeof rowCustomEmailPool !== 'undefined' && rowCustomEmailPool) {
rowCustomEmailPool.style.display = useCustomEmailPool ? '' : 'none';
}
if (cloudflareTempEmailSection) {
cloudflareTempEmailSection.style.display = showCloudflareTempEmailSettings ? '' : 'none';
}
@@ -2930,7 +3035,7 @@ function updateMailProviderUI() {
}
inputEmailPrefix.style.display = '';
inputEmailPrefix.readOnly = false;
selectEmailGenerator.disabled = useHotmail || useLuckmail || useGeneratedAlias || useCustomEmail;
selectEmailGenerator.disabled = useHotmail || useLuckmail || useCustomEmail || (useGeneratedAlias && !useGmail);
if (useGmail) {
labelEmailPrefix.textContent = 'Gmail 原邮箱';
inputEmailPrefix.placeholder = '例如 yourname@gmail.com';
@@ -2946,7 +3051,7 @@ function updateMailProviderUI() {
if (rowHotmailLocalBaseUrl) {
rowHotmailLocalBaseUrl.style.display = useHotmail && hotmailServiceMode === HOTMAIL_SERVICE_MODE_LOCAL ? '' : 'none';
}
btnFetchEmail.hidden = useHotmail || useLuckmail || useCustomEmail;
btnFetchEmail.hidden = useHotmail || useLuckmail || useCustomEmail || useCustomEmailPool;
inputEmail.readOnly = useHotmail || useLuckmail;
inputEmail.placeholder = useHotmail
? '由 Hotmail 账号池自动分配'
@@ -2959,7 +3064,7 @@ function updateMailProviderUI() {
if (!useHotmail && !useLuckmail) {
inputEmail.placeholder = uiCopy.placeholder;
}
btnFetchEmail.disabled = useLuckmail || useCustomEmail || isAutoRunLockedPhase();
btnFetchEmail.disabled = useLuckmail || useCustomEmail || useCustomEmailPool || isAutoRunLockedPhase();
if (!btnFetchEmail.disabled) {
btnFetchEmail.textContent = uiCopy.buttonLabel;
}
@@ -2972,20 +3077,25 @@ function updateMailProviderUI() {
? '步骤 3 会自动生成邮箱,无需手动获取'
: (useCustomEmail ? '请先填写自定义注册邮箱,成功一轮后会自动清空' : `先自动获取${uiCopy.label},或手动粘贴邮箱后再继续`)));
}
if (autoHintText && useCustomEmailPool) {
autoHintText.textContent = getCustomEmailPoolSize() > 0
? `当前邮箱池共 ${getCustomEmailPoolSize()} 个邮箱,自动轮数会跟随数量;实际收码仍走当前邮箱服务`
: '请先在邮箱池里每行填写一个邮箱,自动轮数会跟随数量';
}
if (autoHintText && useGmail && useGeneratedAlias) {
autoHintText.textContent = '请先填写 Gmail 原邮箱,步骤 3 会自动生成 Gmail +tag 地址';
}
if (autoHintText && useGeneratedAlias && aliasUiCopy?.hint) {
autoHintText.textContent = aliasUiCopy.hint;
}
if (autoHintText && useMail2925AccountPool) {
if (autoHintText && useMail2925AccountPool && !useCustomEmailPool) {
autoHintText.textContent = getMail2925Accounts().length
? (useGeneratedAlias
? '当前已启用 2925 号池模式,步骤 3 会基于下拉框选中的号池邮箱生成别名地址'
: '当前已启用 2925 号池模式,步骤 4 / 8 遇到登录页时会优先使用下拉框选中的账号自动登录')
: '当前已启用 2925 号池模式,请先在下方 2925 账号池中添加账号并选择邮箱';
}
if (autoHintText && showCloudflareTempEmailReceiveMailbox) {
if (autoHintText && showCloudflareTempEmailReceiveMailbox && !useCustomEmailPool) {
autoHintText.textContent = '若注册邮箱会转发到 Cloudflare Temp Email,请在“邮件接收”中填写实际接收转发邮件的邮箱。';
}
if (autoHintText && showCloudflareTempEmailRandomSubdomainToggle && inputTempEmailUseRandomSubdomain?.checked) {
@@ -2996,6 +3106,12 @@ function updateMailProviderUI() {
} else if (useLuckmail) {
inputEmail.value = getCurrentLuckmailEmail();
}
if (useCustomEmailPool) {
syncRunCountFromCustomEmailPool();
}
if (typeof inputRunCount !== 'undefined' && inputRunCount) {
inputRunCount.disabled = currentAutoRun.autoRunning || useCustomEmailPool;
}
renderHotmailAccounts();
if (useMail2925) {
renderMail2925Accounts();
@@ -3320,6 +3436,11 @@ async function fetchGeneratedEmail(options = {}) {
generator: selectEmailGenerator.value,
mailProvider: selectMailProvider.value,
mail2925Mode: getSelectedMail2925Mode(),
...(getSelectedEmailGenerator() === CUSTOM_EMAIL_POOL_GENERATOR
? {
customEmailPool: normalizeCustomEmailPoolEntries(inputCustomEmailPool?.value),
}
: {}),
...buildManagedAliasBaseEmailPayload(),
},
});
@@ -4053,7 +4174,22 @@ async function startAutoRunFromCurrentSettings() {
return false;
}
const totalRuns = getRunCountValue();
if (typeof settingsDirty !== 'undefined' && settingsDirty && typeof saveSettings === 'function') {
await saveSettings({ silent: true });
}
const customEmailPoolEnabled = typeof usesCustomEmailPoolGenerator === 'function'
&& usesCustomEmailPoolGenerator();
const customEmailPoolSize = customEmailPoolEnabled && typeof getCustomEmailPoolSize === 'function'
? getCustomEmailPoolSize()
: 0;
if (customEmailPoolEnabled && customEmailPoolSize <= 0) {
throw new Error('请先在邮箱池里至少填写 1 个邮箱。');
}
const totalRuns = customEmailPoolEnabled ? customEmailPoolSize : getRunCountValue();
if (customEmailPoolEnabled) {
inputRunCount.value = String(customEmailPoolSize);
}
let mode = 'restart';
const autoRunSkipFailures = inputAutoSkipFailures.checked;
const contributionNickname = String(inputContributionNickname?.value || '').trim();
@@ -4525,6 +4661,19 @@ inputEmailPrefix.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => {});
});
inputCustomEmailPool?.addEventListener('input', () => {
syncRunCountFromCustomEmailPool();
updateMailProviderUI();
markSettingsDirty(true);
scheduleSettingsAutoSave();
});
inputCustomEmailPool?.addEventListener('blur', () => {
inputCustomEmailPool.value = normalizeCustomEmailPoolEntries(inputCustomEmailPool.value).join('\n');
syncRunCountFromCustomEmailPool();
updateMailProviderUI();
saveSettings({ silent: true }).catch(() => {});
});
selectMail2925PoolAccount?.addEventListener('change', async () => {
try {
await syncSelectedMail2925PoolAccount();
@@ -4575,6 +4724,11 @@ inputRunCount.addEventListener('input', () => {
updateFallbackThreadIntervalInputState();
});
inputRunCount.addEventListener('blur', () => {
if (usesCustomEmailPoolGenerator()) {
syncRunCountFromCustomEmailPool();
updateFallbackThreadIntervalInputState();
return;
}
inputRunCount.value = String(getRunCountValue());
updateFallbackThreadIntervalInputState();
});
+104
View File
@@ -0,0 +1,104 @@
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('normalizeEmailGenerator'),
extractFunction('normalizeCustomEmailPool'),
extractFunction('getCustomEmailPool'),
extractFunction('getCustomEmailPoolEmailForRun'),
extractFunction('getEmailGeneratorLabel'),
].join('\n');
function createApi() {
return new Function(`
const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool';
const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
${bundle}
return {
normalizeEmailGenerator,
normalizeCustomEmailPool,
getCustomEmailPool,
getCustomEmailPoolEmailForRun,
getEmailGeneratorLabel,
};
`)();
}
test('background recognizes custom email pool generator and label', () => {
const api = createApi();
assert.equal(api.normalizeEmailGenerator('custom-pool'), 'custom-pool');
assert.equal(api.getEmailGeneratorLabel('custom-pool'), '自定义邮箱池');
});
test('background normalizes custom email pool input and keeps order', () => {
const api = createApi();
assert.deepEqual(
api.normalizeCustomEmailPool(' Foo@Example.com \ninvalid\nbar@example.combaz@example.com '),
['foo@example.com', 'bar@example.com', 'baz@example.com']
);
});
test('background selects the matching email for the current auto-run round', () => {
const api = createApi();
const state = {
customEmailPool: ['first@example.com', 'second@example.com', 'third@example.com'],
};
assert.equal(api.getCustomEmailPoolEmailForRun(state, 1), 'first@example.com');
assert.equal(api.getCustomEmailPoolEmailForRun(state, 2), 'second@example.com');
assert.equal(api.getCustomEmailPoolEmailForRun(state, 4), '');
});
@@ -78,6 +78,136 @@ test('generated email helper falls back to normal generator when 2925 is in rece
]);
});
test('generated email helper can read the requested address from custom email pool', async () => {
const api = loadGeneratedEmailHelpersApi();
const events = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build alias');
},
buildCloudflareTempEmailHeaders: () => ({}),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
CUSTOM_EMAIL_POOL_GENERATOR: 'custom-pool',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async () => ({ ok: true, text: async () => '{}' }),
fetchIcloudHideMyEmail: async () => {
throw new Error('should not use icloud generator');
},
getCloudflareTempEmailAddressFromResponse: () => '',
getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }),
getCustomEmailPoolEmail: (state, targetRun) => state.customEmailPool?.[targetRun - 1] || '',
getState: async () => ({
customEmailPool: ['first@example.com', 'second@example.com'],
emailGenerator: 'custom-pool',
mailProvider: 'gmail',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate 2925 account');
},
joinCloudflareTempEmailUrl: () => '',
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: () => '',
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: () => false,
reuseOrCreateTab: async () => {},
sendToContentScript: async () => {
throw new Error('should not open duck tab');
},
setEmailState: async (email) => {
events.push(['email', email]);
},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
customEmailPool: ['first@example.com', 'second@example.com'],
emailGenerator: 'custom-pool',
mailProvider: 'gmail',
}, {
generator: 'custom-pool',
poolIndex: 1,
});
assert.equal(email, 'second@example.com');
assert.deepStrictEqual(events, [
['email', 'second@example.com'],
]);
});
test('generated email helper respects runtime generator overrides when deciding alias flow', async () => {
const api = loadGeneratedEmailHelpersApi();
const aliasStates = [];
const savedEmails = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: (state) => {
aliasStates.push({ ...state });
return 'base+tag@gmail.com';
},
buildCloudflareTempEmailHeaders: () => ({}),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
CUSTOM_EMAIL_POOL_GENERATOR: 'custom-pool',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async () => ({ ok: true, text: async () => '{}' }),
fetchIcloudHideMyEmail: async () => {
throw new Error('should not use icloud generator');
},
getCloudflareTempEmailAddressFromResponse: () => '',
getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }),
getCustomEmailPoolEmail: () => '',
getState: async () => ({
mailProvider: '163',
emailGenerator: 'duck',
gmailBaseEmail: '',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate mail2925 account');
},
joinCloudflareTempEmailUrl: () => '',
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: () => '',
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: (stateOrProvider, mail2925Mode) => {
const provider = typeof stateOrProvider === 'string'
? stateOrProvider
: stateOrProvider?.mailProvider;
const generator = typeof stateOrProvider === 'string'
? ''
: stateOrProvider?.emailGenerator;
return String(provider || '').trim().toLowerCase() === 'gmail'
&& String(generator || '').trim().toLowerCase() !== 'custom-pool'
&& String(mail2925Mode || '').trim().toLowerCase() !== 'receive';
},
reuseOrCreateTab: async () => {},
sendToContentScript: async () => {
throw new Error('should not use duck generator');
},
setEmailState: async (email) => {
savedEmails.push(email);
},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
mailProvider: '163',
emailGenerator: 'duck',
}, {
generator: 'gmail-alias',
mailProvider: 'gmail',
gmailBaseEmail: 'base@gmail.com',
});
assert.equal(email, 'base+tag@gmail.com');
assert.deepEqual(savedEmails, ['base+tag@gmail.com']);
assert.equal(aliasStates.length, 1);
assert.equal(aliasStates[0].mailProvider, 'gmail');
assert.equal(aliasStates[0].emailGenerator, 'gmail-alias');
assert.equal(aliasStates[0].gmailBaseEmail, 'base@gmail.com');
});
test('generated email helper uses the regular temp email domain when random subdomain mode is disabled', async () => {
const api = loadGeneratedEmailHelpersApi();
const requests = [];
+115
View File
@@ -0,0 +1,115 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('sidepanel/sidepanel.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);
}
test('sidepanel html exposes custom email pool generator option and input row', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /option value="custom-pool">自定义邮箱池<\/option>/);
assert.match(html, /id="row-custom-email-pool"/);
assert.match(html, /id="input-custom-email-pool"/);
});
test('sidepanel locks run count to custom email pool size', () => {
const bundle = [
extractFunction('isCustomMailProvider'),
extractFunction('normalizeCustomEmailPoolEntries'),
extractFunction('getSelectedEmailGenerator'),
extractFunction('usesGeneratedAliasMailProvider'),
extractFunction('usesCustomEmailPoolGenerator'),
extractFunction('getCustomEmailPoolSize'),
extractFunction('getRunCountValue'),
].join('\n');
const api = new Function(`
const GMAIL_PROVIDER = 'gmail';
const GMAIL_ALIAS_GENERATOR = 'gmail-alias';
const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool';
const selectMailProvider = { value: 'gmail' };
const selectEmailGenerator = { value: 'custom-pool' };
const inputCustomEmailPool = { value: 'first@example.com\\nsecond@example.com' };
const inputRunCount = { value: '99' };
function isLuckmailProvider() {
return false;
}
function isManagedAliasProvider() {
return false;
}
function getSelectedMail2925Mode() {
return 'provide';
}
function isManagedAliasProvider(provider) {
return String(provider || '').trim().toLowerCase() === GMAIL_PROVIDER;
}
${bundle}
return {
getSelectedEmailGenerator,
usesGeneratedAliasMailProvider,
usesCustomEmailPoolGenerator,
getCustomEmailPoolSize,
getRunCountValue,
};
`)();
assert.equal(api.getSelectedEmailGenerator(), 'custom-pool');
assert.equal(api.usesGeneratedAliasMailProvider('gmail', 'provide', 'gmail-alias'), true);
assert.equal(api.usesGeneratedAliasMailProvider('gmail', 'provide', 'custom-pool'), false);
assert.equal(api.usesCustomEmailPoolGenerator(), true);
assert.equal(api.getCustomEmailPoolSize(), 2);
assert.equal(api.getRunCountValue(), 2);
});
+16
View File
@@ -493,6 +493,20 @@ Codex2API 补充:
- `gmailBaseEmail`
- `mail2925BaseEmail`
### 7.1.1 2026-04-23 补充:自定义邮箱池链路
本轮新增 `custom-pool` 生成方式,用于把一批已准备好的邮箱按顺序并入现有自动流,而不是继续为每种邮箱页面单独适配一套“新地址生成器”。
当前行为约定:
1. sidepanel 在 `邮箱生成` 中新增 `自定义邮箱池` 选项,并显示多行 `邮箱池` 输入框
2. 输入框中的邮箱会按“每行一个”归一化为数组,写入持久配置 `customEmailPool`
3. Auto 启动前会先保存当前配置;如果当前生成方式是 `custom-pool`,则自动把总轮数锁定为邮箱池长度
4. 后台在每个目标轮次开始前,会按 `targetRun` 从邮箱池读取对应邮箱,并写入当前 `state.email`
5. 同一目标轮次里的失败重试会继续复用该轮邮箱,不会提前跳到下一个
6. 如果当前目标轮次超出了邮箱池数量,后台会直接报错提示数量不一致
7. 自定义邮箱池只负责“本轮注册邮箱分配”,实际收码仍由当前 `mailProvider` 对应的既有链路负责
### 7.2 共享模块分工
- `managed-alias-utils.js`
@@ -519,6 +533,7 @@ Codex2API 补充:
- Duck
- Cloudflare
- Cloudflare Temp Email
- 自定义邮箱池
- iCloud 隐私邮箱
### 7.2 Hotmail
@@ -652,6 +667,7 @@ Codex2API 补充:
2. 为本轮自动流程分配唯一 `autoRunSessionId`
3. 计算是否从中断点继续
4. 每轮执行前重置必要运行态
- 如果当前生成方式是 `custom-pool`,会先按当前目标轮次把邮箱池中的对应邮箱写回运行态
5. 执行 `runAutoSequenceFromStep`
- 步骤 7 内部仍保留登录态恢复的有限重试,但 `add-phone / 手机号页` 属于立即跳出的不可重试错误
- 步骤 8 若在验证码提交后进入 `add-phone / 手机号页`,会直接抛出 fatal 错误,不再先标记步骤成功
+5 -3
View File
@@ -44,7 +44,7 @@
- `background/account-run-history.js`:邮箱记录模块,负责以邮箱为主键维护最新记录(同邮箱后续状态覆盖),在步骤 2 设定邮箱后支持先写入“停止/未完成”占位状态,统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,支持清理记录,并在启用独立本地同步配置后把完整快照同步到本地 helper。
- `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/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路。
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。
- `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;当前额外接入 2925 账号池的新增、导入、切换、登录、禁用与删除消息。
@@ -122,8 +122,8 @@
- `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行(包括 Codex2API 配置)的禁用与隐藏。
- `sidepanel/sidepanel.css`:侧边栏样式文件;当前额外提供 Hotmail / 2925 共用的号池表单容器、操作按钮行与统一的收起态列表高度样式。
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,会额外显示 `提供邮箱 / 接收邮箱` 模式切换,并把“2925 号池”从别名基邮箱行拆成独立配置行,避免 receive 模式把账号池开关一起隐藏;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密钥配置行;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显示并同时服务于 provide / receive 两种模式;新来源 Codex2API 在这里仅补充来源配置接线、表单显隐和 Step 10 按钮文案,不承接协议业务逻辑;Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;侧边栏初始化与点击“自动”前会刷新一次贡献站公开内容摘要,并按本地关闭版本决定是否展示轻提示。
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,会额外显示 `提供邮箱 / 接收邮箱` 模式切换,并把“2925 号池”从别名基邮箱行拆成独立配置行,避免 receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密钥配置行;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显示并同时服务于 provide / receive 两种模式;`自定义邮箱池` 模式会按邮箱池长度锁定自动轮数,并把当前输入的邮箱池配置参与自动启动前保存;新来源 Codex2API 在这里仅补充来源配置接线、表单显隐和 Step 10 按钮文案,不承接协议业务逻辑;Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;侧边栏初始化与点击“自动”前会刷新一次贡献站公开内容摘要,并按本地关闭版本决定是否展示轻提示。
- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Pro` / `v` 双版本族排序、缓存读取与版本展示。
## `tests/`
@@ -140,6 +140,7 @@
- `tests/background-account-run-history-module.test.js`:测试邮箱记录模块已接入、导出工厂,能够保留并归一化停止记录、归一化失败标签、计算自动重试次数,并支持清理后同步完整快照。
- `tests/background-account-history-settings.test.js`:测试账号运行历史的独立配置项归一化,不再复用 Hotmail 模式作为启停条件。
- `tests/background-contribution-mode.test.js`:测试贡献模式的后台运行态与公开 OAuth 接入,覆盖 `contributionMode` 只存在于运行态、`SET_CONTRIBUTION_MODE / POLL_CONTRIBUTION_STATUS` 消息接入、reset 保留贡献运行态,以及 callback 自动提交在“无需手动提交”场景下的兼容处理。
- `tests/background-custom-email-pool.test.js`:测试后台对自定义邮箱池生成方式的归一化、标签文案和按轮次取邮箱逻辑。
- `tests/background-generated-email-module.test.js`:测试生成邮箱辅助模块已接入且导出工厂。
- `tests/background-icloud.test.js`:测试 iCloud 相关后台纯函数与别名收尾逻辑。
- `tests/background-icloud-mail-provider.test.js`:测试 iCloud 邮箱 provider 配置解析。
@@ -178,6 +179,7 @@
- `tests/sidepanel-contribution-button.test.js`:测试侧边栏顶部 `贡献` 按钮的 HTML 接线、更新提示气泡接线,以及相关脚本加载顺序。
- `tests/sidepanel-account-records-manager.test.js`:测试侧边栏邮箱记录覆盖层的 HTML 接入、helper 地址归一化与 manager 渲染逻辑。
- `tests/contribution-content-update-service.test.js`:测试贡献内容更新服务对公开摘要接口的归一化、版本提取与失败回退缓存逻辑。
- `tests/sidepanel-custom-email-pool.test.js`:测试侧边栏自定义邮箱池的 HTML 接线,以及邮箱池长度与自动轮数之间的联动规则。
- `tests/sidepanel-contribution-mode.test.js`:测试侧边栏贡献模式的 HTML 接线、runtime-only 设置保护,以及贡献模式 manager 复用主自动流启动、状态轮询和退出清理逻辑。
- `tests/sidepanel-auto-run-content-refresh.test.js`:测试点击“自动”时会先刷新贡献内容更新摘要,且刷新失败不会阻塞自动流程启动。
- `tests/sidepanel-icloud-manager.test.js`:测试侧边栏 iCloud 管理器模块接线与空态渲染。