2925邮箱添加邮箱接收和发送功能

This commit is contained in:
QLHazyCoder
2026-04-21 23:21:19 +08:00
parent ab935a33b1
commit cf42dc73fd
15 changed files with 515 additions and 58 deletions
+19 -15
View File
@@ -81,33 +81,34 @@
## 安装 ## 安装
1. 打开 `chrome://extensions/` 1. 打开 `chrome://extensions/`
2. 开启“开发者模式”
3. 点击“加载已解压的扩展程序”
4. 选择本项目目录
5. 打开扩展侧边栏
## 2026-04-17 更新补充:Gmail / 2925 别名邮箱 ## 2026-04-17 更新补充:Gmail / 2925 别名邮箱
本次版本对 `Gmail``2925` 的注册邮箱逻辑做了统一整理: 本次版本对 `Gmail``2925 provide` 的注册邮箱逻辑做了统一整理:
- `Gmail``2925` 现在都走同一套“别名邮箱”逻辑。 - `Gmail``2925 provide` 现在都走同一套“别名邮箱”逻辑。
- 两者都不再使用“只填前缀再特殊拼接”的界面交互。 - 两者都不再使用“只填前缀再特殊拼接”的界面交互。
- 两者都要求先填写“基邮箱”: - 两者都要求先填写“基邮箱”:
- `Gmail`:例如 `name@gmail.com` - `Gmail`:例如 `name@gmail.com`
- `2925`:例如 `name@2925.com` - `2925`(仅 provide 模式):例如 `name@2925.com`
- 侧边栏里的“注册邮箱”输入框对这两种模式都已开放,可直接手动填写完整邮箱。 - 侧边栏里的“注册邮箱”输入框对这两种模式都已开放,可直接手动填写完整邮箱。
- 侧边栏里的 `获取 / 生成` 按钮对这两种模式也可用,行为与 Duck / Cloudflare 一样,都是“可自动生成,也可手动覆盖”。 - 侧边栏里的 `获取 / 生成` 按钮对这两种模式也可用,行为与 Duck / Cloudflare 一样,都是“可自动生成,也可手动覆盖”。
-`Mail = 2925` 且模式切到 `接收邮箱` 时,不再走别名基邮箱链路,而是回退到普通“邮箱生成 / 手动填写注册邮箱”路线,2925 只负责后续收信。
具体行为: 具体行为:
- `Gmail` 会基于完整基邮箱生成 `name+tag@gmail.com` - `Gmail` 会基于完整基邮箱生成 `name+tag@gmail.com`
- `2925` 会基于完整基邮箱生成 `name123456@2925.com` - `2925` 仅在 provide 模式下会基于完整基邮箱生成 `name123456@2925.com`
- 如果当前“注册邮箱”里已经是与当前基邮箱兼容的完整邮箱,流程会优先复用,不会强行重新生成 - 如果当前“注册邮箱”里已经是与当前基邮箱兼容的完整邮箱,流程会优先复用,不会强行重新生成
注意: 注意:
- `2925` 旧的“只填前缀”使用方式已经不再推荐,应该改为填写完整基邮箱 - `2925` 旧的“只填前缀”使用方式已经不再推荐,应该改为填写完整基邮箱
- 如果你手动填写了与当前 `Gmail / 2925` 基邮箱不匹配的完整邮箱,侧边栏会在保存或执行 Step 3 时拦截 - 如果你手动填写了与当前 `Gmail / 2925 provide` 基邮箱不匹配的完整邮箱,侧边栏会在保存或执行 Step 3 时拦截
2. 开启“开发者模式”
3. 点击“加载已解压的扩展程序”
4. 选择本项目目录
5. 打开扩展侧边栏
## 快速开始 ## 快速开始
@@ -143,12 +144,15 @@
1. `Mail` 选择 `2925` 1. `Mail` 选择 `2925`
2.`2925 账号池` 中添加 `邮箱 / 密码` 2.`2925 账号池` 中添加 `邮箱 / 密码`
3. 在“2925 基邮箱”右侧打开 `号池` 开关后,基邮箱输入框会切成下拉框,只能从 2925 账号池中选择邮箱;关闭开关则回退到原来的手填基邮箱路线 3. 先根据你的用途选择 `2925 模式`
4. 可先点 `使用此账号` 让当前别名基邮箱切到这条 2925 账号,再点 `登录` 手动验证网页邮箱登录态 - `提供邮箱`:注册邮箱本身就是 2925 别名,会显示“别名基邮箱”输入
5. 只有在 `号池` 开关开启时,自动流程执行到 Step 4 / Step 8 前才会自动检查 2925 登录态;如果未登录,会先清理登录 cookie、等待 `3 秒`,再打开登录页,并在页面打开后再等待 `3 秒`,然后使用当前账号自动登录;点击登录后若 `20 秒`内仍未进入收件箱,则会判定当前登录失败 - `接收邮箱`:注册邮箱回退到普通“邮箱生成 / 手动填写”路线,2925 只负责收信
6. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间;如果还有下一个可用账号,就禁用当前账号 24 小时并自动切换登录;如果没有下一个可用账号,或当前未启用号池模式,则会直接复用现有“手动暂停 / 停止”逻辑终止自动流程 4. `2925 号池` 现在是独立配置行;开启 `号池` 后可从下拉框中选择当前 2925 账号。若当前处于 `提供邮箱` 模式,这个账号也会同步作为别名基邮箱
7. 如果你同时开启了 `Auto` 的自动重试,当前尝试结束后会按现有逻辑自动进入下一次尝试,不需要再手动介入 5. 可先点 `使用此账号` 让当前 2925 账号切到这条记录,再点 `登录` 手动验证网页邮箱登录态
8. `Mail = 2925` 仍然走 Gmail / 2925 共用的别名邮箱链路;实际注册邮箱会基于当前 2925 账号邮箱生成,例如 `name@2925.com -> name123456@2925.com` 6. 只有在 `号池` 开关开启时,自动流程执行到 Step 4 / Step 8 前才会自动检查 2925 登录态;如果未登录,会先清理登录 cookie、等待 `3 秒`,再打开登录页,并在页面打开后再等待 `3 秒`,然后使用当前账号自动登录;点击登录后若 `20 秒`内仍未进入收件箱,则会判定当前登录失败
7. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间;如果还有下一个可用账号,就禁用当前账号 24 小时并自动切换登录;如果没有下一个可用账号,或当前未启用号池模式,则会直接复用现有“手动暂停 / 停止”逻辑终止自动流程
8. 如果你同时开启了 `Auto` 的自动重试,当前尝试结束后会按现有逻辑自动进入下一次尝试,不需要再手动介入
9. 只有 `Mail = 2925` 且模式为 `提供邮箱` 时,才会继续走 Gmail / 2925 共用的别名邮箱链路;例如 `name@2925.com -> name123456@2925.com`
## 侧边栏配置说明 ## 侧边栏配置说明
+22 -2
View File
@@ -2132,11 +2132,21 @@ function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) {
const provider = typeof stateOrProvider === 'string' const provider = typeof stateOrProvider === 'string'
? stateOrProvider ? stateOrProvider
: stateOrProvider?.mailProvider; : stateOrProvider?.mailProvider;
const resolvedMail2925Mode = mail2925Mode !== undefined
? normalizeMail2925Mode(mail2925Mode)
: getMail2925Mode(stateOrProvider);
const utils = (typeof self !== 'undefined' ? self : globalThis).MultiPageManagedAliasUtils || null; const utils = (typeof self !== 'undefined' ? self : globalThis).MultiPageManagedAliasUtils || null;
if (utils?.usesManagedAliasGeneration) {
return utils.usesManagedAliasGeneration(provider, { mail2925Mode: resolvedMail2925Mode });
}
if (utils?.isManagedAliasProvider) { if (utils?.isManagedAliasProvider) {
if (String(provider || '').trim().toLowerCase() === '2925') {
return utils.isManagedAliasProvider(provider) && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE;
}
return utils.isManagedAliasProvider(provider); return utils.isManagedAliasProvider(provider);
} }
return provider === GMAIL_PROVIDER || provider === '2925'; return provider === GMAIL_PROVIDER
|| (provider === '2925' && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE);
} }
function shouldUseCustomRegistrationEmail(state = {}) { function shouldUseCustomRegistrationEmail(state = {}) {
@@ -2279,11 +2289,21 @@ function isGeneratedAliasProvider(stateOrProvider, mail2925Mode = undefined) {
const provider = typeof stateOrProvider === 'string' const provider = typeof stateOrProvider === 'string'
? stateOrProvider ? stateOrProvider
: stateOrProvider?.mailProvider; : stateOrProvider?.mailProvider;
const resolvedMail2925Mode = mail2925Mode !== undefined
? normalizeMail2925Mode(mail2925Mode)
: getMail2925Mode(stateOrProvider);
const utils = getManagedAliasUtils(); const utils = getManagedAliasUtils();
if (utils?.usesManagedAliasGeneration) {
return utils.usesManagedAliasGeneration(provider, { mail2925Mode: resolvedMail2925Mode });
}
if (utils?.isManagedAliasProvider) { if (utils?.isManagedAliasProvider) {
if (String(provider || '').trim().toLowerCase() === '2925') {
return utils.isManagedAliasProvider(provider) && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE;
}
return utils.isManagedAliasProvider(provider); return utils.isManagedAliasProvider(provider);
} }
return provider === GMAIL_PROVIDER || provider === '2925'; return provider === GMAIL_PROVIDER
|| (provider === '2925' && resolvedMail2925Mode === MAIL_2925_MODE_PROVIDE);
} }
function shouldUseCustomRegistrationEmail(state = {}) { function shouldUseCustomRegistrationEmail(state = {}) {
+7 -1
View File
@@ -195,6 +195,9 @@
...(state || {}), ...(state || {}),
mailProvider: provider, mailProvider: provider,
}; };
if (options.mail2925Mode !== undefined) {
mergedState.mail2925Mode = String(options.mail2925Mode || '').trim();
}
if (options.gmailBaseEmail !== undefined) { if (options.gmailBaseEmail !== undefined) {
mergedState.gmailBaseEmail = String(options.gmailBaseEmail || '').trim(); mergedState.gmailBaseEmail = String(options.gmailBaseEmail || '').trim();
} }
@@ -227,7 +230,10 @@
async function fetchGeneratedEmail(state, options = {}) { async function fetchGeneratedEmail(state, options = {}) {
const currentState = state || await getState(); const currentState = state || await getState();
const provider = String(options.mailProvider || currentState.mailProvider || '').trim().toLowerCase(); const provider = String(options.mailProvider || currentState.mailProvider || '').trim().toLowerCase();
if (isGeneratedAliasProvider?.(provider)) { const mail2925Mode = options.mail2925Mode !== undefined
? options.mail2925Mode
: currentState.mail2925Mode;
if (isGeneratedAliasProvider?.(provider, mail2925Mode)) {
return fetchManagedAliasEmail(currentState, options); return fetchManagedAliasEmail(currentState, options);
} }
const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator); const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator);
+4
View File
@@ -113,12 +113,15 @@
function getVerificationPollPayload(step, state, overrides = {}) { function getVerificationPollPayload(step, state, overrides = {}) {
const is2925Provider = state?.mailProvider === '2925'; const is2925Provider = state?.mailProvider === '2925';
const mail2925MatchTargetEmail = is2925Provider
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive';
if (step === 4) { if (step === 4) {
return { return {
filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(4, state), filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(4, state),
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'], senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'], subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'],
targetEmail: state.email, targetEmail: state.email,
mail2925MatchTargetEmail,
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5, maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000, intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
...overrides, ...overrides,
@@ -130,6 +133,7 @@
senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'], senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'], subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
targetEmail: String(state?.step8VerificationTargetEmail || '').trim() || state.email, targetEmail: String(state?.step8VerificationTargetEmail || '').trim() || state.email,
mail2925MatchTargetEmail,
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5, maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000, intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
...overrides, ...overrides,
+47
View File
@@ -629,6 +629,39 @@ function extractVerificationCode(text, strictChatGPTCodeOnly = false) {
return null; return null;
} }
function extractEmails(text = '') {
const matches = String(text || '').match(/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/gi) || [];
return [...new Set(matches.map((item) => item.toLowerCase()))];
}
function emailMatchesTarget(candidate, targetEmail) {
const normalizedCandidate = String(candidate || '').trim().toLowerCase();
const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
return Boolean(normalizedCandidate && normalizedTarget && normalizedCandidate === normalizedTarget);
}
function getTargetEmailMatchState(text, targetEmail) {
const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
if (!normalizedTarget) {
return { matches: true, hasExplicitEmail: false };
}
const normalizedText = String(text || '').toLowerCase();
if (normalizedText.includes(normalizedTarget)) {
return { matches: true, hasExplicitEmail: true };
}
const extractedEmails = extractEmails(normalizedText);
if (!extractedEmails.length) {
return { matches: true, hasExplicitEmail: false };
}
return {
matches: extractedEmails.some((candidate) => emailMatchesTarget(candidate, normalizedTarget)),
hasExplicitEmail: true,
};
}
function normalizeMinuteTimestamp(timestamp) { function normalizeMinuteTimestamp(timestamp) {
if (!Number.isFinite(timestamp) || timestamp <= 0) return 0; if (!Number.isFinite(timestamp) || timestamp <= 0) return 0;
const date = new Date(timestamp); const date = new Date(timestamp);
@@ -930,6 +963,8 @@ async function handlePollEmail(step, payload) {
filterAfterTimestamp = 0, filterAfterTimestamp = 0,
excludeCodes = [], excludeCodes = [],
strictChatGPTCodeOnly = false, strictChatGPTCodeOnly = false,
targetEmail = '',
mail2925MatchTargetEmail = false,
} = payload || {}; } = payload || {};
const excludedCodeSet = new Set(excludeCodes.filter(Boolean)); const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0); const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
@@ -994,9 +1029,21 @@ async function handlePollEmail(step, payload) {
if (!matchesMailFilters(previewText, senderFilters, subjectFilters)) { if (!matchesMailFilters(previewText, senderFilters, subjectFilters)) {
continue; continue;
} }
const previewTargetState = mail2925MatchTargetEmail
? getTargetEmailMatchState(previewText, targetEmail)
: { matches: true, hasExplicitEmail: false };
if (mail2925MatchTargetEmail && previewTargetState.hasExplicitEmail && !previewTargetState.matches) {
continue;
}
const previewCode = extractVerificationCode(previewText, strictChatGPTCodeOnly); const previewCode = extractVerificationCode(previewText, strictChatGPTCodeOnly);
const openedText = await openMailAndDeleteAfterRead(item, step); const openedText = await openMailAndDeleteAfterRead(item, step);
const openedTargetState = mail2925MatchTargetEmail
? getTargetEmailMatchState(openedText, targetEmail)
: { matches: true, hasExplicitEmail: false };
if (mail2925MatchTargetEmail && openedTargetState.hasExplicitEmail && !openedTargetState.matches) {
continue;
}
const bodyCode = extractVerificationCode(openedText, strictChatGPTCodeOnly); const bodyCode = extractVerificationCode(openedText, strictChatGPTCodeOnly);
const candidateCode = bodyCode || previewCode; const candidateCode = bodyCode || previewCode;
+29
View File
@@ -3,6 +3,9 @@
})(typeof self !== 'undefined' ? self : globalThis, function createManagedAliasUtilsModule() { })(typeof self !== 'undefined' ? self : globalThis, function createManagedAliasUtilsModule() {
const GMAIL_PROVIDER = 'gmail'; const GMAIL_PROVIDER = 'gmail';
const MAIL_2925_PROVIDER = '2925'; const MAIL_2925_PROVIDER = '2925';
const MAIL_2925_MODE_PROVIDE = 'provide';
const MAIL_2925_MODE_RECEIVE = 'receive';
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
const PROVIDER_CONFIGS = { const PROVIDER_CONFIGS = {
[GMAIL_PROVIDER]: { [GMAIL_PROVIDER]: {
@@ -64,10 +67,31 @@
return PROVIDER_CONFIGS[String(provider || '').trim().toLowerCase()] || null; return PROVIDER_CONFIGS[String(provider || '').trim().toLowerCase()] || null;
} }
function normalizeMail2925Mode(value = '') {
return String(value || '').trim().toLowerCase() === MAIL_2925_MODE_RECEIVE
? MAIL_2925_MODE_RECEIVE
: DEFAULT_MAIL_2925_MODE;
}
function isManagedAliasProvider(provider = '') { function isManagedAliasProvider(provider = '') {
return Boolean(getManagedAliasProviderConfig(provider)); return Boolean(getManagedAliasProviderConfig(provider));
} }
function usesManagedAliasGeneration(provider = '', options = {}) {
const normalizedProvider = String(provider || '').trim().toLowerCase();
if (!isManagedAliasProvider(normalizedProvider)) {
return false;
}
if (normalizedProvider !== MAIL_2925_PROVIDER) {
return true;
}
const mail2925Mode = typeof options === 'string'
? options
: options?.mail2925Mode;
return normalizeMail2925Mode(mail2925Mode) === MAIL_2925_MODE_PROVIDE;
}
function parseEmailParts(rawValue = '') { function parseEmailParts(rawValue = '') {
const value = String(rawValue || '').trim().toLowerCase(); const value = String(rawValue || '').trim().toLowerCase();
const match = value.match(/^([^@\s]+)@([^@\s]+\.[^@\s]+)$/); const match = value.match(/^([^@\s]+)@([^@\s]+\.[^@\s]+)$/);
@@ -136,11 +160,16 @@
return { return {
buildManagedAliasEmail, buildManagedAliasEmail,
DEFAULT_MAIL_2925_MODE,
getManagedAliasProviderConfig, getManagedAliasProviderConfig,
getManagedAliasProviderUiCopy, getManagedAliasProviderUiCopy,
isManagedAliasEmail, isManagedAliasEmail,
isManagedAliasProvider, isManagedAliasProvider,
MAIL_2925_MODE_PROVIDE,
MAIL_2925_MODE_RECEIVE,
normalizeMail2925Mode,
parseEmailParts, parseEmailParts,
parseManagedAliasBaseEmail, parseManagedAliasBaseEmail,
usesManagedAliasGeneration,
}; };
}); });
+12 -7
View File
@@ -269,20 +269,25 @@
<button id="btn-cf-domain-mode" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button> <button id="btn-cf-domain-mode" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
</div> </div>
</div> </div>
<div class="data-row" id="row-email-prefix" style="display:none;"> <div class="data-row" id="row-mail2925-pool-settings" style="display:none;">
<span class="data-label" id="label-email-prefix">别名基邮箱</span> <span class="data-label">2925 号池</span>
<div class="data-inline mail2925-base-inline"> <div class="data-inline mail2925-base-inline">
<input type="text" id="input-email-prefix" class="data-input mail2925-base-input" placeholder="例如 yourname@example.com" /> <label class="toggle-switch mail2925-pool-toggle" id="label-mail2925-use-account-pool" for="input-mail2925-use-account-pool" style="display:none;" title="开启后启用 2925 账号池">
<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" /> <input type="checkbox" id="input-mail2925-use-account-pool" />
<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>
<span>号池</span> <span>号池</span>
</label> </label>
<select id="select-mail2925-pool-account" class="data-select mail2925-pool-select" style="display:none;">
<option value="">请选择号池邮箱</option>
</select>
</div>
</div>
<div class="data-row" id="row-email-prefix" style="display:none;">
<span class="data-label" id="label-email-prefix">别名基邮箱</span>
<div class="data-inline">
<input type="text" id="input-email-prefix" class="data-input mail2925-base-input" placeholder="例如 yourname@example.com" />
</div> </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;">
+33 -9
View File
@@ -98,6 +98,7 @@ const rowCustomPassword = document.getElementById('row-custom-password');
const selectMailProvider = document.getElementById('select-mail-provider'); const selectMailProvider = document.getElementById('select-mail-provider');
const btnMailLogin = document.getElementById('btn-mail-login'); const btnMailLogin = document.getElementById('btn-mail-login');
const rowMail2925Mode = document.getElementById('row-mail-2925-mode'); const rowMail2925Mode = document.getElementById('row-mail-2925-mode');
const rowMail2925PoolSettings = document.getElementById('row-mail2925-pool-settings');
const mail2925ModeButtons = Array.from(document.querySelectorAll('[data-mail2925-mode]')); const mail2925ModeButtons = Array.from(document.querySelectorAll('[data-mail2925-mode]'));
const rowEmailGenerator = document.getElementById('row-email-generator'); const rowEmailGenerator = document.getElementById('row-email-generator');
const selectEmailGenerator = document.getElementById('select-email-generator'); const selectEmailGenerator = document.getElementById('select-email-generator');
@@ -264,12 +265,24 @@ function getManagedAliasUtils() {
return window.MultiPageManagedAliasUtils || null; return window.MultiPageManagedAliasUtils || null;
} }
function isManagedAliasProvider(provider = selectMailProvider.value) { function isManagedAliasProvider(provider = selectMailProvider.value, mail2925Mode = getSelectedMail2925Mode()) {
const utils = getManagedAliasUtils(); const utils = getManagedAliasUtils();
if (utils?.usesManagedAliasGeneration) {
return utils.usesManagedAliasGeneration(provider, { mail2925Mode });
}
if (utils?.isManagedAliasProvider) { if (utils?.isManagedAliasProvider) {
const normalizedProvider = String(provider || '').trim().toLowerCase();
if (normalizedProvider === '2925') {
return utils.isManagedAliasProvider(provider)
&& normalizeMail2925Mode(mail2925Mode) === MAIL_2925_MODE_PROVIDE;
}
return utils.isManagedAliasProvider(provider); return utils.isManagedAliasProvider(provider);
} }
return [GMAIL_PROVIDER, '2925'].includes(String(provider || '').trim().toLowerCase()); const normalizedProvider = String(provider || '').trim().toLowerCase();
if (normalizedProvider === '2925') {
return normalizeMail2925Mode(mail2925Mode) === MAIL_2925_MODE_PROVIDE;
}
return normalizedProvider === GMAIL_PROVIDER;
} }
function parseManagedAliasBaseEmail(rawValue, provider = selectMailProvider.value) { function parseManagedAliasBaseEmail(rawValue, provider = selectMailProvider.value) {
@@ -288,7 +301,10 @@ function isManagedAliasEmail(value, baseEmail = '', provider = selectMailProvide
return false; return false;
} }
function getManagedAliasProviderUiCopy(provider = selectMailProvider.value) { function getManagedAliasProviderUiCopy(provider = selectMailProvider.value, mail2925Mode = getSelectedMail2925Mode()) {
if (!isManagedAliasProvider(provider, mail2925Mode)) {
return null;
}
const utils = getManagedAliasUtils(); const utils = getManagedAliasUtils();
if (utils?.getManagedAliasProviderUiCopy) { if (utils?.getManagedAliasProviderUiCopy) {
return utils.getManagedAliasProviderUiCopy(provider); return utils.getManagedAliasProviderUiCopy(provider);
@@ -592,7 +608,7 @@ const LOG_LEVEL_LABELS = {
}; };
function usesGeneratedAliasMailProvider(provider, mail2925Mode = getSelectedMail2925Mode()) { function usesGeneratedAliasMailProvider(provider, mail2925Mode = getSelectedMail2925Mode()) {
return isManagedAliasProvider(provider); return isManagedAliasProvider(provider, mail2925Mode);
} }
function parseGmailBaseEmail(rawValue = '') { function parseGmailBaseEmail(rawValue = '') {
@@ -2639,13 +2655,18 @@ function updateMailProviderUI() {
const useIcloudProvider = isIcloudMailProvider(); const useIcloudProvider = isIcloudMailProvider();
const useEmailGenerator = !useHotmail && !useLuckmail && !useGeneratedAlias && !useCustomEmail; const useEmailGenerator = !useHotmail && !useLuckmail && !useGeneratedAlias && !useCustomEmail;
const useCloudflareTempEmailProvider = selectMailProvider.value === 'cloudflare-temp-email'; const useCloudflareTempEmailProvider = selectMailProvider.value === 'cloudflare-temp-email';
const aliasUiCopy = useGeneratedAlias ? getManagedAliasProviderUiCopy(selectMailProvider.value) : null; const aliasUiCopy = useGeneratedAlias
? getManagedAliasProviderUiCopy(selectMailProvider.value, mail2925Mode)
: null;
const uiCopy = getCurrentRegistrationEmailUiCopy(); const uiCopy = getCurrentRegistrationEmailUiCopy();
updateMailLoginButtonState(); updateMailLoginButtonState();
if (rowMail2925Mode) { if (rowMail2925Mode) {
rowMail2925Mode.style.display = 'none'; rowMail2925Mode.style.display = use2925 ? '' : 'none';
} }
rowEmailPrefix.style.display = useGeneratedAlias ? '' : 'none'; if (rowMail2925PoolSettings) {
rowMail2925PoolSettings.style.display = useMail2925 ? '' : 'none';
}
rowEmailPrefix.style.display = useGeneratedAlias && !useMail2925AccountPool ? '' : 'none';
const hotmailServiceMode = getSelectedHotmailServiceMode(); const hotmailServiceMode = getSelectedHotmailServiceMode();
rowInbucketHost.style.display = useInbucket ? '' : 'none'; rowInbucketHost.style.display = useInbucket ? '' : 'none';
rowInbucketMailbox.style.display = useInbucket ? '' : 'none'; rowInbucketMailbox.style.display = useInbucket ? '' : 'none';
@@ -2708,8 +2729,8 @@ function updateMailProviderUI() {
selectMail2925PoolAccount.style.display = useMail2925AccountPool ? '' : 'none'; selectMail2925PoolAccount.style.display = useMail2925AccountPool ? '' : 'none';
selectMail2925PoolAccount.disabled = !useMail2925AccountPool || getMail2925Accounts().length === 0; selectMail2925PoolAccount.disabled = !useMail2925AccountPool || getMail2925Accounts().length === 0;
} }
inputEmailPrefix.style.display = useMail2925AccountPool ? 'none' : ''; inputEmailPrefix.style.display = '';
inputEmailPrefix.readOnly = useMail2925AccountPool; inputEmailPrefix.readOnly = false;
selectEmailGenerator.disabled = useHotmail || useLuckmail || useGeneratedAlias || useCustomEmail; selectEmailGenerator.disabled = useHotmail || useLuckmail || useGeneratedAlias || useCustomEmail;
if (useGmail) { if (useGmail) {
labelEmailPrefix.textContent = 'Gmail 原邮箱'; labelEmailPrefix.textContent = 'Gmail 原邮箱';
@@ -2760,7 +2781,9 @@ function updateMailProviderUI() {
} }
if (autoHintText && useMail2925AccountPool) { if (autoHintText && useMail2925AccountPool) {
autoHintText.textContent = getMail2925Accounts().length autoHintText.textContent = getMail2925Accounts().length
? (useGeneratedAlias
? '当前已启用 2925 号池模式,步骤 3 会基于下拉框选中的号池邮箱生成别名地址' ? '当前已启用 2925 号池模式,步骤 3 会基于下拉框选中的号池邮箱生成别名地址'
: '当前已启用 2925 号池模式,步骤 4 / 8 遇到登录页时会优先使用下拉框选中的账号自动登录')
: '当前已启用 2925 号池模式,请先在下方 2925 账号池中添加账号并选择邮箱'; : '当前已启用 2925 号池模式,请先在下方 2925 账号池中添加账号并选择邮箱';
} }
if (autoHintText && showCloudflareTempEmailReceiveMailbox) { if (autoHintText && showCloudflareTempEmailReceiveMailbox) {
@@ -3088,6 +3111,7 @@ async function fetchGeneratedEmail(options = {}) {
generateNew: true, generateNew: true,
generator: selectEmailGenerator.value, generator: selectEmailGenerator.value,
mailProvider: selectMailProvider.value, mailProvider: selectMailProvider.value,
mail2925Mode: getSelectedMail2925Mode(),
...buildManagedAliasBaseEmailPayload(), ...buildManagedAliasBaseEmailPayload(),
}, },
}); });
@@ -15,3 +15,64 @@ test('generated email helper module exposes a factory', () => {
assert.equal(typeof api?.createGeneratedEmailHelpers, 'function'); assert.equal(typeof api?.createGeneratedEmailHelpers, 'function');
}); });
test('generated email helper falls back to normal generator when 2925 is in receive mode', async () => {
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
const events = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build alias in receive mode');
},
buildCloudflareTempEmailHeaders: () => ({}),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
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: '' }),
getState: async () => ({
mailProvider: '2925',
mail2925Mode: 'receive',
emailGenerator: 'duck',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate 2925 account in receive mode');
},
joinCloudflareTempEmailUrl: () => '',
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: () => '',
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: (_provider, mail2925Mode) => mail2925Mode === 'provide',
reuseOrCreateTab: async () => {},
sendToContentScript: async (_source, message) => {
events.push(message.type);
return { email: 'duck@example.com', generated: true };
},
setEmailState: async (email) => {
events.push(['email', email]);
},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
mailProvider: '2925',
mail2925Mode: 'receive',
emailGenerator: 'duck',
}, {
mailProvider: '2925',
mail2925Mode: 'receive',
generator: 'duck',
});
assert.equal(email, 'duck@example.com');
assert.deepStrictEqual(events, [
'FETCH_DUCK_EMAIL',
['email', 'duck@example.com'],
]);
});
+92 -1
View File
@@ -164,7 +164,7 @@ return {
assert.deepEqual(api.getReadAndDeleteCalls(), ['baseline', 'new']); assert.deepEqual(api.getReadAndDeleteCalls(), ['baseline', 'new']);
}); });
test('handlePollEmail ignores targetEmail and still tests any matching ChatGPT mail', async () => { test('handlePollEmail keeps ignoring targetEmail when receive-mode matching is disabled', async () => {
const bundle = [ const bundle = [
extractFunction('normalizeMinuteTimestamp'), extractFunction('normalizeMinuteTimestamp'),
extractFunction('handlePollEmail'), extractFunction('handlePollEmail'),
@@ -242,12 +242,103 @@ return {
maxAttempts: 4, maxAttempts: 4,
intervalMs: 1, intervalMs: 1,
targetEmail: 'expected@example.com', targetEmail: 'expected@example.com',
mail2925MatchTargetEmail: false,
}); });
assert.equal(result.code, '112233'); assert.equal(result.code, '112233');
assert.deepEqual(api.getReadAndDeleteCalls(), ['mail-1']); assert.deepEqual(api.getReadAndDeleteCalls(), ['mail-1']);
}); });
test('handlePollEmail skips explicit mismatched target emails when receive-mode matching is enabled', async () => {
const bundle = [
extractFunction('extractEmails'),
extractFunction('emailMatchesTarget'),
extractFunction('getTargetEmailMatchState'),
extractFunction('normalizeMinuteTimestamp'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
let state = 'ready';
const seenCodes = new Set();
const readAndDeleteCalls = [];
const mismatchMail = {
id: 'mail-1',
text: 'ChatGPT verification code 112233 for another.user@example.com',
};
const targetMail = {
id: 'mail-2',
text: 'ChatGPT verification code 445566 for expected@example.com',
};
function findMailItems() {
return state === 'ready' ? [mismatchMail, targetMail] : [];
}
function getMailItemId(item) {
return item.id;
}
function getCurrentMailIds(items = []) {
return new Set(items.map((item) => item.id));
}
function parseMailItemTimestamp() {
return Date.now();
}
function matchesMailFilters(text) {
return /chatgpt|openai|verification/i.test(String(text || ''));
}
function getMailItemText(item) {
return item.text;
}
function extractVerificationCode(text) {
const match = String(text || '').match(/(\\d{6})/);
return match ? match[1] : null;
}
async function sleep() {}
async function sleepRandom() {}
async function returnToInbox() {
return true;
}
async function refreshInbox() {}
async function openMailAndDeleteAfterRead(item) {
readAndDeleteCalls.push(item.id);
return item.text;
}
async function ensureSeenCodesSession() {}
function persistSeenCodes() {}
function log() {}
${bundle}
return {
handlePollEmail,
getReadAndDeleteCalls() {
return readAndDeleteCalls.slice();
},
};
`)();
const result = await api.handlePollEmail(8, {
senderFilters: ['chatgpt'],
subjectFilters: ['verification'],
maxAttempts: 1,
intervalMs: 1,
targetEmail: 'expected@example.com',
mail2925MatchTargetEmail: true,
});
assert.equal(result.code, '445566');
assert.deepEqual(api.getReadAndDeleteCalls(), ['mail-2']);
});
test('handlePollEmail only accepts 2925 mails inside the fixed lookback window', async () => { test('handlePollEmail only accepts 2925 mails inside the fixed lookback window', async () => {
const bundle = [ const bundle = [
extractFunction('normalizeMinuteTimestamp'), extractFunction('normalizeMinuteTimestamp'),
+9
View File
@@ -26,3 +26,12 @@ test('managed alias utils validate provider email with or without configured bas
assert.equal(api.isManagedAliasEmail('manual@gmail.com', 'gmail', ''), true); assert.equal(api.isManagedAliasEmail('manual@gmail.com', 'gmail', ''), true);
assert.equal(api.isManagedAliasEmail('manual@qq.com', 'gmail', ''), false); assert.equal(api.isManagedAliasEmail('manual@qq.com', 'gmail', ''), false);
}); });
test('managed alias utils keep 2925 alias generation behind provide mode only', () => {
assert.equal(api.normalizeMail2925Mode('provide'), 'provide');
assert.equal(api.normalizeMail2925Mode('receive'), 'receive');
assert.equal(api.normalizeMail2925Mode('other'), 'provide');
assert.equal(api.usesManagedAliasGeneration('gmail'), true);
assert.equal(api.usesManagedAliasGeneration('2925', { mail2925Mode: 'provide' }), true);
assert.equal(api.usesManagedAliasGeneration('2925', { mail2925Mode: 'receive' }), false);
});
+109
View File
@@ -0,0 +1,109 @@
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 keeps 2925 mode row and standalone pool settings row', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="row-mail-2925-mode"/);
assert.match(html, /data-mail2925-mode="provide"/);
assert.match(html, /data-mail2925-mode="receive"/);
assert.match(html, /id="row-mail2925-pool-settings"/);
});
test('sidepanel only treats 2925 as generated alias provider in provide mode', () => {
const bundle = [
extractFunction('isManagedAliasProvider'),
extractFunction('usesGeneratedAliasMailProvider'),
].join('\n');
const api = new Function(`
const GMAIL_PROVIDER = 'gmail';
const MAIL_2925_MODE_PROVIDE = 'provide';
const MAIL_2925_MODE_RECEIVE = 'receive';
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
const selectMailProvider = { value: '2925' };
function normalizeMail2925Mode(value = '') {
return String(value || '').trim().toLowerCase() === MAIL_2925_MODE_RECEIVE
? MAIL_2925_MODE_RECEIVE
: DEFAULT_MAIL_2925_MODE;
}
function getSelectedMail2925Mode() {
return MAIL_2925_MODE_PROVIDE;
}
function getManagedAliasUtils() {
return {
usesManagedAliasGeneration(provider, options = {}) {
return String(provider || '').trim().toLowerCase() === 'gmail'
|| (String(provider || '').trim().toLowerCase() === '2925'
&& normalizeMail2925Mode(options.mail2925Mode) === MAIL_2925_MODE_PROVIDE);
},
};
}
${bundle}
return {
isManagedAliasProvider,
usesGeneratedAliasMailProvider,
};
`)();
assert.equal(api.isManagedAliasProvider('2925', 'provide'), true);
assert.equal(api.isManagedAliasProvider('2925', 'receive'), false);
assert.equal(api.usesGeneratedAliasMailProvider('2925', 'provide'), true);
assert.equal(api.usesGeneratedAliasMailProvider('2925', 'receive'), false);
assert.equal(api.usesGeneratedAliasMailProvider('gmail', 'receive'), true);
});
+43
View File
@@ -43,6 +43,49 @@ test('verification flow keeps 2925 polling cadence in the default payload', () =
assert.equal(step8Payload.intervalMs, 15000); assert.equal(step8Payload.intervalMs, 15000);
}); });
test('verification flow only enables 2925 target email matching in receive mode', () => {
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async () => ({}),
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
const providePayload = helpers.getVerificationPollPayload(4, {
email: 'user@example.com',
mailProvider: '2925',
mail2925Mode: 'provide',
});
const receivePayload = helpers.getVerificationPollPayload(4, {
email: 'user@example.com',
mailProvider: '2925',
mail2925Mode: 'receive',
});
assert.equal(providePayload.mail2925MatchTargetEmail, false);
assert.equal(receivePayload.mail2925MatchTargetEmail, true);
});
test('verification flow runs beforeSubmit hook before filling the code', async () => { test('verification flow runs beforeSubmit hook before filling the code', async () => {
const events = []; const events = [];
+19 -15
View File
@@ -336,7 +336,8 @@
- 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。 - 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。
- 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。 - 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。
- `2925` 内容脚本会把每一封实际打开检测的邮件立即删除;同一验证码步骤启动后,试过的验证码会按“步骤 ID + 启动时间”隔离缓存,不会在本次步骤里重复提交;如果再次遇到相同验证码,对应邮件也会在读取后立即删除,避免后续反复打开。 - `2925` 内容脚本会把每一封实际打开检测的邮件立即删除;同一验证码步骤启动后,试过的验证码会按“步骤 ID + 启动时间”隔离缓存,不会在本次步骤里重复提交;如果再次遇到相同验证码,对应邮件也会在读取后立即删除,避免后续反复打开。
- `2925` 当前不再对邮件里的收件邮箱做比对,只要邮件内容命中 ChatGPT / OpenAI 验证码过滤条件,就会尝试该邮件。 - `2925` 在 provide 模式下仍保持宽松匹配:只要邮件内容命中 ChatGPT / OpenAI 验证码过滤条件,就会尝试该邮件。
- `2925` 在 receive 模式下会恢复“弱目标邮箱匹配”:只有当邮件里显式出现了其他收件邮箱时才会跳过;如果邮件里没有明确写出邮箱,仍允许继续尝试该验证码。
- 当验证码最终提交成功后,后台会异步向 2925 邮箱页发送 `DELETE_ALL_EMAILS`,执行“全选 + 删除”清理剩余邮件,不阻塞主流程。 - 当验证码最终提交成功后,后台会异步向 2925 邮箱页发送 `DELETE_ALL_EMAILS`,执行“全选 + 删除”清理剩余邮件,不阻塞主流程。
- 如果 `2925` 邮箱页在轮询期间出现“子邮箱已达上限邮箱”,后台会记录当前时间,把当前 2925 账号禁用 24 小时,自动切到下一个可用账号并完成登录,然后直接报错结束当前尝试;如果 Auto 开启了自动重试,现有控制器会按原逻辑进入下一次尝试。 - 如果 `2925` 邮箱页在轮询期间出现“子邮箱已达上限邮箱”,后台会记录当前时间,把当前 2925 账号禁用 24 小时,自动切到下一个可用账号并完成登录,然后直接报错结束当前尝试;如果 Auto 开启了自动重试,现有控制器会按原逻辑进入下一次尝试。
@@ -409,7 +410,8 @@
补充: 补充:
-`2925` 而言,Step 8 不依赖验证码页显示邮箱做收件匹配,而是直接测试所有命中 ChatGPT / OpenAI 过滤条件的邮件。 -`2925 provide` 而言,Step 8 不依赖验证码页显示邮箱做收件匹配,而是直接测试所有命中 ChatGPT / OpenAI 过滤条件的邮件。
-`2925 receive` 而言,Step 8 会把当前目标注册邮箱一并传给 2925 内容脚本;只有当邮件里显式写出了其他邮箱时才会跳过,从而在不破坏历史兼容性的前提下,尽量降低误收验证码的概率。
### Step 9 ### Step 9
@@ -452,25 +454,26 @@
### 7.1 2026-04-17 补充:Gmail / 2925 统一别名邮箱链路 ### 7.1 2026-04-17 补充:Gmail / 2925 统一别名邮箱链路
本轮将 Gmail 与 2925 的注册邮箱逻辑统一收敛为“共享别名邮箱链路”: 本轮将 Gmail 与 `2925 + provide` 的注册邮箱逻辑统一收敛为“共享别名邮箱链路”:
- 两者都先填写“基邮箱” - 两者都先填写“基邮箱”
- Gmail`name@gmail.com` - Gmail`name@gmail.com`
- 2925`name@2925.com` - 2925(仅 provide 模式)`name@2925.com`
- 两者都允许两种入口: - 两者都允许两种入口:
- 点击侧边栏按钮自动生成完整注册邮箱 - 点击侧边栏按钮自动生成完整注册邮箱
- 直接在“注册邮箱”输入框中手动填写完整邮箱 - 直接在“注册邮箱”输入框中手动填写完整邮箱
-`mailProvider = 2925``mail2925Mode = receive` 时,不再走别名基邮箱链路,而是回退到普通“邮箱生成 / 手动填写注册邮箱”路线;2925 仅负责后续收信与登录态管理
当前行为约定: 当前行为约定:
1. sidepanel 展示“别名基邮箱”输入框,并根据当前 provider 显示对应文案 1. sidepanel 会根据当前 provider 与 `mail2925Mode` 决定是否展示“别名基邮箱”输入框
2. 点击 `获取 / 生成` 时: 2. 点击 `获取 / 生成` 时:
- Gmail 生成 `name+tag@gmail.com` - Gmail 生成 `name+tag@gmail.com`
- 2925 生成 `name123456@2925.com` - 2925 仅在 provide 模式下生成 `name123456@2925.com`
3. Step 2 / Step 3 进入注册流程前,会先判断当前 `state.email` 3. Step 2 / Step 3 进入注册流程前,会先判断当前 `state.email`
- 如果已经是与当前基邮箱兼容的完整邮箱,则直接复用 - 如果已经是与当前基邮箱兼容的完整邮箱,则直接复用
- 如果为空或不兼容,则按当前 provider 重新生成 - 如果为空或不兼容,则按当前 provider / mode 重新生成
4. 保存或执行 Step 3 时,如果手动填写的完整邮箱与当前 Gmail / 2925 基邮箱不兼容,sidepanel 会直接拦截 4. 保存或执行 Step 3 时,如果手动填写的完整邮箱与当前 Gmail / 2925 provide 基邮箱不兼容,sidepanel 会直接拦截`2925 receive` 不参与这条兼容性约束
5. auto-run fresh attempt reset 时,会保留: 5. auto-run fresh attempt reset 时,会保留:
- `gmailBaseEmail` - `gmailBaseEmail`
- `mail2925BaseEmail` - `mail2925BaseEmail`
@@ -554,13 +557,14 @@
链路: 链路:
1. 用户在 sidepanel 的 2925 账号池中保存 `email / password` 1. 用户在 sidepanel 的 2925 账号池中保存 `email / password`
2. 只有当 sidepanel 中`mail2925UseAccountPool` 开关开启时,别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail` 2. sidepanel 中会单独展示 `提供邮箱 / 接收邮箱` 模式切换,以及独立的 `2925 号池` 开关 / 当前账号下拉框;这样即使切到 receive 模式,账号池设置也不会被别名基邮箱行一起隐藏
3. 手动点击 `登录` 或自动流程进入 Step 4 前,后台会先打开当前 2925 邮箱页:如果仍停留在收件箱则直接复用;如果跳到登录页,则仅在号池模式开启时才自动登录,关闭号池时直接调用现有停止逻辑结束流程 3. 只有当 sidepanel 中的 `mail2925UseAccountPool` 开关开启时,provide 模式下的别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail`
4. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先判断是否启用了号池模式:若已启用且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录;若未启用,则直接调用现有停止逻辑结束流程 4. 手动点击 `登录` 或自动流程进入 Step 4 前,后台会先打开当前 2925 邮箱页:如果仍停留在收件箱则直接复用;如果跳到登录页,则仅在号池模式开启时才自动登录,关闭号池时直接调用现有停止逻辑结束流程
5. 如果登录提交后 20 秒内仍未进入收件箱,且当前正处于自动运行中,则后台会直接用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态;这类情况常见于图片验证、行为验证或其他阻断登录的中间页 5. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先判断是否启用了号池模式:若已启用且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录;若未启用,则直接用现有停止逻辑结束流程
6. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态 6. 如果登录提交后 20 秒内仍未进入收件箱,且当前正处于自动运行中,则后台会直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态;这类情况常见于图片验证、行为验证或其他阻断登录的中间页
7. sidepanel 中 2925 账号池的新增表单也走与 Hotmail 相同的共享交互:默认收起,头部按钮切换“添加账号 / 取消添加”,操作行右侧提供“批量导入”,保存成功后自动收起并清空。 7. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态
8. 2925 号池模式开启时,当前选中的号池邮箱会同步回写到同一个 `mail2925BaseEmail` 字段;因此用户切换号池账号后,即使再次关闭号池模式,也会直接沿用刚才选中的邮箱作为手动基邮箱,无需重新输入 8. sidepanel 中 2925 号池的新增表单也走与 Hotmail 相同的共享交互:默认收起,头部按钮切换“添加账号 / 取消添加”,操作行右侧提供“批量导入”,保存成功后自动收起并清空
9. 当 2925 号池模式开启时,当前选中的号池邮箱会同步回写到同一个 `mail2925BaseEmail` 字段;因此用户切换号池账号后,即使再次关闭号池模式,也会直接沿用刚才选中的邮箱作为手动基邮箱,无需重新输入。
### 7.5 iCloud ### 7.5 iCloud
+8 -7
View File
@@ -27,7 +27,7 @@
- `icloud-utils.js`:iCloud 隐私邮箱相关的纯工具函数,负责 host、别名列表、保留状态、已用状态等归一化。 - `icloud-utils.js`:iCloud 隐私邮箱相关的纯工具函数,负责 host、别名列表、保留状态、已用状态等归一化。
- `luckmail-utils.js`:LuckMail 相关的纯工具函数,负责邮箱购买记录、标签、邮件 cursor、验证码匹配等归一化。 - `luckmail-utils.js`:LuckMail 相关的纯工具函数,负责邮箱购买记录、标签、邮件 cursor、验证码匹配等归一化。
- `mail2925-utils.js`:2925 账号池相关的纯工具函数,负责账号归一化、冷却期判断、可用账号挑选、批量导入解析与列表更新。 - `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 文案;当前还统一承接 `mail2925Mode` 的归一化与“2925 仅在 provide 模式下参与别名生成”的共享判定
- `manifest.json`:Chrome 扩展清单,声明权限、背景脚本、侧边栏、内容脚本与规则集。 - `manifest.json`:Chrome 扩展清单,声明权限、背景脚本、侧边栏、内容脚本与规则集。
- `microsoft-email.js`Microsoft Graph / Outlook 邮件读取辅助模块,负责刷新令牌换 token、邮箱夹轮询和验证码提取。 - `microsoft-email.js`Microsoft Graph / Outlook 邮件读取辅助模块,负责刷新令牌换 token、邮箱夹轮询和验证码提取。
- `package.json`:仓库最小 Node 包配置,目前主要提供测试脚本定义。 - `package.json`:仓库最小 Node 包配置,目前主要提供测试脚本定义。
@@ -44,15 +44,15 @@
- `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` 与当前 2925 账号选择,避免自动流程重置后丢失别名基邮箱与 2925 切号上下文。 - `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 的别名邮箱生成入口;当 provider 为 2925 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱。 - `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口;当 provider 为 2925`mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路
- `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。 - `background/logging-status.js`:后台日志、步骤状态、错误信息和若干状态判断的公共工具层;当前额外承接 `add-phone / 手机号页` 这类认证 fatal 错误的共享判定。
- `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。 - `background/mail-2925-session.js`:2925 会话模块,负责 2925 账号池持久化、当前账号切换、cookie 清理登出、自动登录、命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号。
- `background/message-router.js`:后台消息路由层,负责处理 `chrome.runtime.onMessage` 进入的所有业务消息;当前额外接入 2925 账号池的新增、导入、切换、登录、禁用与删除消息。 - `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 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 时,会先确保账号池中已选中可用账号。 - `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。 - `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;当前等待标签完成、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。 - `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。
## `background/steps/` ## `background/steps/`
@@ -77,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 次的邮箱刷新轮询、命中邮件后立即删当前邮件,以及在成功后配合后台执行整箱清理;若页面出现“子邮箱已达上限邮箱”提示,会立即上报后台进入切号链路;当后台显式开启 `mail2925MatchTargetEmail` 时,会对邮件里显式出现的目标邮箱做弱匹配,避免 `receive` 模式误捞别人的验证码
- `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。
@@ -121,8 +121,8 @@
- `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。 - `sidepanel/contribution-content-update-service.js`:侧边栏贡献内容更新服务,负责拉取 `https://apikey.qzz.io/api/content-summary`、缓存公开公告/教程摘要,并输出可用于提示展示的 `promptVersion` 与最新更新时间。
- `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行的禁用与隐藏。 - `sidepanel/contribution-mode.js`:侧边栏贡献模式管理器,负责顶部“贡献”按钮、确认弹窗、贡献模式显隐、复用主自动流程启动、侧栏内贡献状态轮询、上传页跳转,以及贡献模式下对来源选择、配置入口、记录入口和敏感配置行的禁用与隐藏。
- `sidepanel/sidepanel.css`:侧边栏样式文件;当前额外提供 Hotmail / 2925 共用的号池表单容器、操作按钮行与统一的收起态列表高度样式。 - `sidepanel/sidepanel.css`:侧边栏样式文件;当前额外提供 Hotmail / 2925 共用的号池表单容器、操作按钮行与统一的收起态列表高度样式。
- `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,别名基邮箱行右侧会显示“号池”开关,开启后输入框切换为号池邮箱下拉选择,并显示 2925 账号池管理区Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。 - `sidepanel/sidepanel.html`:侧边栏页面结构;当前步骤列表已改为动态容器,日志区提供“记录”按钮并挂接邮箱记录覆盖层,顶部新增“贡献”按钮并在设置卡片中新增贡献模式主面板;贡献按钮下方额外挂接一个可关闭的轻提示气泡,用于提示公开公告 / 使用教程有更新;贡献面板内展示 `OAUTH / 回调 / 总状态` 三块真实运行态信息,同时把“本地同步”与“验证码重发”拆成独立行以避免特殊模式隐藏时互相影响;页面继续加载 `managed-alias-utils.js`,并把旧的“邮箱前缀”字段语义改为“别名基邮箱”;当 provider 为 2925 时,会额外显示 `提供邮箱 / 接收邮箱` 模式切换,并把“2925 号池”从别名基邮箱行拆成独立配置行,避免 receive 模式把账号池开关一起隐藏Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器。
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 2925 号池开关开启时,当前账号变化会同步映射到 2925 基邮箱下拉选择,关闭时则回退到原来的手填基邮箱路线Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;侧边栏初始化与点击“自动”前会刷新一次贡献站公开内容摘要,并按本地关闭版本决定是否展示轻提示。 - `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、独立本地同步配置、共享验证码自动重发次数配置与广播接收,并装配 Hotmail / 2925 / iCloud / LuckMail / 贡献模式 / 邮箱记录面板 / 贡献内容更新服务;当前贡献模式的“开始贡献”会直接复用主自动流程启动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显示并同时服务于 provide / receive 两种模式Hotmail / 2925 的新增表单显隐统一接到 `sidepanel/account-pool-ui.js` 这一层共享 helper;侧边栏初始化与点击“自动”前会刷新一次贡献站公开内容摘要,并按本地关闭版本决定是否展示轻提示。
- `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Pro` / `v` 双版本族排序、缓存读取与版本展示。 - `sidepanel/update-service.js`:侧边栏更新检查服务,负责 GitHub Releases 查询、`Pro` / `v` 双版本族排序、缓存读取与版本展示。
## `tests/` ## `tests/`
@@ -182,6 +182,7 @@
- `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/sidepanel-mail2925-manager.test.js`:测试侧边栏 2925 管理器模块接线、共享号池表单脚本加载顺序、显隐交互与空态渲染。
- `tests/sidepanel-mail2925-mode.test.js`:测试侧边栏保留 `2925 provide / receive` 模式行与独立的号池配置行,并验证 sidepanel 只在 provide 模式下把 2925 视为别名邮箱 provider。
- `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`:测试共享步骤定义模块及侧边栏脚本加载顺序。