fix: route SMS send rejection through phone restart helper

- Merge latest dev into the PR branch before applying the maintainer fix

- Expose the phone resend banned classifier from phone verification helpers

- Use the helper from auto-run step 4 and keep restart logs reason-specific
This commit is contained in:
QLHazyCoder
2026-05-08 18:17:18 +08:00
18 changed files with 1504 additions and 24 deletions
+6 -3
View File
@@ -2,7 +2,7 @@
一个用于批量跑通 ChatGPT OAuth 注册/登录流程的 Chrome 扩展。
当前版本基于侧边栏控制,支持单步执行、整套自动执行、停止当前流程、保存常用配置,以及通过 DuckDuckGo / QQ / 163 / 163 VIP / 126 / Inbucket / Hotmail 协助获取验证码。
当前版本基于侧边栏控制,支持单步执行、整套自动执行、停止当前流程、保存常用配置,以及通过 DuckDuckGo / QQ / 163 / 163 VIP / 126 / Inbucket / Hotmail / Cloud Mail 协助获取验证码。
## 插件效果
@@ -40,6 +40,7 @@
- 自动获取注册验证码与登录验证码
- 支持 `Hotmail`:继续使用 `邮箱 + 客户端 ID + 刷新令牌(refresh token`,并可在远程服务与本地助手两种模式间切换
- 支持 `2925`:新增多账号池、自动登录登出、Step 4 / Step 8 命中“子邮箱已达上限邮箱”后的 24 小时禁用与自动切号
- 支持 `Cloud Mail`:可通过 skymail.ink API 生成自定义域邮箱,也可作为转发收件通道轮询验证码
- 支持 `QQ Mail``163 Mail``163 VIP Mail``126 Mail``Inbucket mailbox`
- 支持从 DuckDuckGo Email Protection 自动生成新的 `@duck.com` 地址
- 支持基于 Cloudflare 自定义域名自动生成随机邮箱前缀
@@ -208,10 +209,11 @@ Step 1 和 Step 10 都依赖这个地址。
### `Mail`
支持种验证码来源:
支持种验证码来源:
- `Hotmail`
- `2925`
- `Cloud Mail`
- `163 Mail`
- `163 VIP Mail`
- `126 Mail`
@@ -222,6 +224,7 @@ Step 1 和 Step 10 都依赖这个地址。
- `Hotmail` 通过侧边栏里的 Hotmail 账号池选择账号,可切换为远程服务模式或本地助手模式
- `2925` 通过侧边栏里的 2925 账号池选择账号,并在 Step 4 / Step 8 前自动校验网页邮箱登录态
- `Cloud Mail` 通过侧边栏配置 API 地址、管理员账号、接收邮箱或生成域名,可直接生成邮箱或轮询转发收件箱
- `QQ``163``163 VIP``126` 用于直接轮询网页邮箱
- `Inbucket` 通过你在侧边栏里配置的 host 访问 `mailbox` 页面:`https://<your-inbucket-host>/m/<mailbox>/`
@@ -521,7 +524,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
1. Step 1 打开 `https://chatgpt.com/`
2. 根据 `Mail` 选择邮箱来源
3. 如果 `Mail = Hotmail`,会从账号池自动分配一个可用账号
4. 如果 `Mail = 自定义邮箱` 且配置了 `自定义号池`,会按号池顺序分配当前轮邮箱;否则如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取或分配邮箱(Duck / Cloudflare / iCloud / 自定义邮箱池等)
4. 如果 `Mail = 自定义邮箱` 且配置了 `自定义号池`,会按号池顺序分配当前轮邮箱;否则如果不是 Hotmail,则按当前“邮箱生成”配置尝试自动获取或分配邮箱(Duck / Cloudflare / Cloud Mail / iCloud / 自定义邮箱池等)
5. Step 2 点击注册、填写邮箱,并按真实落地页进入密码页或直接进入邮箱验证码页
6. 如果自动获取失败,暂停并等待你在侧边栏填写邮箱后点击 `Continue`
7. 继续执行 Step 3 ~ Step 10
+211 -8
View File
@@ -48,6 +48,8 @@ importScripts(
'microsoft-email.js',
'luckmail-utils.js',
'cloudflare-temp-email-utils.js',
'cloudmail-utils.js',
'background/cloudmail-provider.js',
'icloud-utils.js',
'mail-provider-utils.js',
'content/activation-utils.js'
@@ -168,6 +170,17 @@ const {
normalizeCloudflareTempEmailDomains,
normalizeCloudflareTempEmailMailApiMessages,
} = self.CloudflareTempEmailUtils;
const {
DEFAULT_MAIL_PAGE_SIZE: CLOUD_MAIL_DEFAULT_PAGE_SIZE,
buildCloudMailHeaders,
getCloudMailTokenFromResponse,
joinCloudMailUrl,
normalizeCloudMailAddress,
normalizeCloudMailBaseUrl,
normalizeCloudMailDomain,
normalizeCloudMailDomains,
normalizeCloudMailMailApiMessages,
} = self.CloudMailUtils;
const {
findIcloudAliasByEmail,
getConfiguredIcloudHostPreference,
@@ -224,6 +237,8 @@ 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 CLOUD_MAIL_PROVIDER = 'cloudmail';
const CLOUD_MAIL_GENERATOR = 'cloudmail';
const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool';
const HOTMAIL_MAILBOXES = ['INBOX', 'Junk'];
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
@@ -378,8 +393,18 @@ const FIVE_SIM_COUNTRY_ID = 'vietnam';
const FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const FIVE_SIM_SUPPORTED_COUNTRY_IDS = ['indonesia', 'thailand', 'vietnam'];
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(FIVE_SIM_SUPPORTED_COUNTRY_IDS);
const HERO_SMS_SUPPORTED_COUNTRY_IDS = [6, 52, 10];
const HERO_SMS_SUPPORTED_COUNTRY_IDS = [6, 52, 187, 16, 151, 43, 73, 10];
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(HERO_SMS_SUPPORTED_COUNTRY_IDS.map(String));
const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze([
{ prefix: '84', id: 10, label: 'Vietnam' },
{ prefix: '66', id: 52, label: 'Thailand' },
{ prefix: '62', id: 6, label: 'Indonesia' },
{ prefix: '44', id: 16, label: 'United Kingdom' },
{ prefix: '81', id: 151, label: 'Japan' },
{ prefix: '49', id: 43, label: 'Germany' },
{ prefix: '33', id: 73, label: 'France' },
{ prefix: '1', id: 187, label: 'USA' },
]);
const FIVE_SIM_OPERATOR = DEFAULT_FIVE_SIM_OPERATOR;
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
@@ -698,6 +723,13 @@ const PERSISTED_SETTING_DEFAULTS = {
cloudflareTempEmailUseRandomSubdomain: false,
cloudflareTempEmailDomain: '',
cloudflareTempEmailDomains: [],
cloudMailBaseUrl: '',
cloudMailAdminEmail: '',
cloudMailAdminPassword: '',
cloudMailToken: '',
cloudMailReceiveMailbox: '',
cloudMailDomain: '',
cloudMailDomains: [],
hotmailAccounts: [],
mail2925Accounts: [],
paypalAccounts: [],
@@ -1690,6 +1722,7 @@ function normalizeEmailGenerator(value = '') {
}
if (normalized === 'cloudflare') return 'cloudflare';
if (normalized === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return CLOUDFLARE_TEMP_EMAIL_GENERATOR;
if (normalized === 'cloudmail') return 'cloudmail';
return 'duck';
}
@@ -1923,6 +1956,7 @@ function normalizeMailProvider(value = '') {
case HOTMAIL_PROVIDER:
case LUCKMAIL_PROVIDER:
case CLOUDFLARE_TEMP_EMAIL_PROVIDER:
case CLOUD_MAIL_PROVIDER:
case '163':
case '163-vip':
case '126':
@@ -2118,6 +2152,34 @@ function resolveCloudflareTempEmailPollTargetEmail(state = {}, pollPayload = {},
return normalizeCloudflareTempEmailReceiveMailbox(state.email);
}
const cloudMailProvider = self.MultiPageBackgroundCloudMailProvider.createCloudMailProvider({
addLog,
buildCloudMailHeaders,
CLOUD_MAIL_DEFAULT_PAGE_SIZE,
CLOUD_MAIL_GENERATOR,
CLOUD_MAIL_PROVIDER,
getCloudMailTokenFromResponse,
getState,
joinCloudMailUrl,
normalizeCloudMailAddress,
normalizeCloudMailBaseUrl,
normalizeCloudMailDomain,
normalizeCloudMailDomains,
normalizeCloudMailMailApiMessages,
pickVerificationMessageWithTimeFallback,
setEmailState,
setPersistentSettings,
sleepWithStop,
throwIfStopped,
});
const {
getCloudMailConfig,
normalizeCloudMailReceiveMailbox,
fetchCloudMailAddress,
pollCloudMailVerificationCode,
resolveCloudMailPollTargetEmail,
} = cloudMailProvider;
function normalizeSub2ApiGroupNames(value = '') {
const source = Array.isArray(value)
? value
@@ -2454,6 +2516,19 @@ function normalizePersistentSettingValue(key, value) {
return normalizeCloudflareTempEmailDomain(value);
case 'cloudflareTempEmailDomains':
return normalizeCloudflareTempEmailDomains(value);
case 'cloudMailBaseUrl':
return normalizeCloudMailBaseUrl(value);
case 'cloudMailAdminEmail':
return String(value || '').trim();
case 'cloudMailAdminPassword':
case 'cloudMailToken':
return String(value || '');
case 'cloudMailReceiveMailbox':
return normalizeCloudMailReceiveMailbox(value);
case 'cloudMailDomain':
return normalizeCloudMailDomain(value);
case 'cloudMailDomains':
return normalizeCloudMailDomains(value);
case 'hotmailAccounts':
return normalizeHotmailAccounts(value);
case 'mail2925Accounts':
@@ -2563,6 +2638,13 @@ function buildPersistentSettingsPayload(input = {}, options = {}) {
}
payload.cloudflareTempEmailDomains = domains;
}
if (payload.cloudMailDomains) {
const domains = normalizeCloudMailDomains(payload.cloudMailDomains);
if (payload.cloudMailDomain && !domains.includes(payload.cloudMailDomain)) {
domains.unshift(payload.cloudMailDomain);
}
payload.cloudMailDomains = domains;
}
if (
Object.prototype.hasOwnProperty.call(payload, 'sub2apiGroupName')
|| Object.prototype.hasOwnProperty.call(payload, 'sub2apiGroupNames')
@@ -6708,19 +6790,117 @@ async function clearFreeReusablePhoneActivation() {
return { ok: true, freeReusablePhoneActivation: null };
}
function inferHeroSmsCountryFromPhoneNumber(phoneNumber = '') {
const digits = String(phoneNumber || '').replace(/\D+/g, '');
if (!digits) {
return null;
}
const match = HERO_SMS_COUNTRY_BY_PHONE_PREFIX.find((entry) => digits.startsWith(entry.prefix));
if (!match) {
return null;
}
return {
id: Math.max(1, Math.floor(Number(match.id) || 0)),
label: String(match.label || '').trim() || `Country #${match.id}`,
};
}
function normalizePhoneDigits(value = '') {
return String(value || '').replace(/\D+/g, '');
}
function phoneNumbersMatch(left = '', right = '') {
const leftDigits = normalizePhoneDigits(left);
const rightDigits = normalizePhoneDigits(right);
return Boolean(leftDigits && rightDigits && leftDigits === rightDigits);
}
function normalizeLocalHeroSmsActivation(record) {
if (!record || typeof record !== 'object' || Array.isArray(record)) {
return null;
}
const activationId = String(record.activationId ?? record.id ?? record.activation ?? '').trim();
const phoneNumber = String(record.phoneNumber ?? record.number ?? record.phone ?? '').trim();
if (!activationId || !phoneNumber) {
return null;
}
const rawProvider = String(record.provider ?? record.smsProvider ?? '').trim();
const provider = rawProvider ? normalizePhoneSmsProvider(rawProvider) : PHONE_SMS_PROVIDER_HERO;
if (provider !== PHONE_SMS_PROVIDER_HERO) {
return null;
}
const countryId = Math.max(
0,
Math.floor(Number(record.countryId ?? record.country ?? record.countryCode) || 0)
);
const countryLabel = String(record.countryLabel || record.label || '').trim();
const serviceCode = String(record.serviceCode || record.service || HERO_SMS_SERVICE_CODE).trim() || HERO_SMS_SERVICE_CODE;
return {
...record,
provider: PHONE_SMS_PROVIDER_HERO,
activationId,
phoneNumber,
serviceCode,
...(countryId > 0 ? { countryId } : {}),
...(countryLabel ? { countryLabel } : {}),
};
}
function findLocalHeroSmsActivationForPhone(state = {}, phoneNumber = '') {
const candidates = [
state.currentPhoneActivation,
state.reusablePhoneActivation,
state.pendingPhoneActivationConfirmation,
state.signupPhoneActivation,
state.signupPhoneCompletedActivation,
state.phonePreferredActivation,
state.freeReusablePhoneActivation,
];
if (Array.isArray(state.phoneReusableActivationPool)) {
candidates.push(...state.phoneReusableActivationPool);
}
for (const candidate of candidates) {
const normalized = normalizeLocalHeroSmsActivation(candidate);
if (normalized && phoneNumbersMatch(normalized.phoneNumber, phoneNumber)) {
return normalized;
}
}
return null;
}
async function setFreeReusablePhoneActivation(record = {}) {
const phoneNumber = String(record.phoneNumber || record.number || record.phone || '').trim();
if (!phoneNumber) {
throw new Error('请先填写白嫖复用手机号。');
}
const state = await getState();
const activationId = String(record.activationId || record.id || record.activation || '').trim();
const countryId = Math.max(1, Math.floor(Number(record.countryId) || Number(state.heroSmsCountryId) || HERO_SMS_COUNTRY_ID));
const localActivation = findLocalHeroSmsActivationForPhone(state, phoneNumber);
const activationId = String(
record.activationId
|| record.id
|| record.activation
|| localActivation?.activationId
|| ''
).trim();
const inferredCountry = inferHeroSmsCountryFromPhoneNumber(phoneNumber);
const hasExplicitCountry = Number.isFinite(Number(record.countryId)) && Number(record.countryId) > 0;
const countryId = Math.max(
1,
Math.floor(
Number(record.countryId)
|| Number(localActivation?.countryId)
|| Number(inferredCountry?.id)
|| Number(state.heroSmsCountryId)
|| HERO_SMS_COUNTRY_ID
)
);
const stateCountryLabel = Math.floor(Number(state.heroSmsCountryId) || 0) === countryId
? String(state.heroSmsCountryLabel || '').trim()
: '';
const countryLabel = String(
record.countryLabel
|| (Number(localActivation?.countryId) === countryId ? localActivation?.countryLabel : '')
|| (!hasExplicitCountry && inferredCountry?.id === countryId ? inferredCountry.label : '')
|| stateCountryLabel
|| (countryId === HERO_SMS_COUNTRY_ID ? HERO_SMS_COUNTRY_LABEL : `Country #${countryId}`)
).trim();
@@ -6728,7 +6908,7 @@ async function setFreeReusablePhoneActivation(record = {}) {
...(activationId ? { activationId } : {}),
phoneNumber,
provider: PHONE_SMS_PROVIDER_HERO,
serviceCode: HERO_SMS_SERVICE_CODE,
serviceCode: String(record.serviceCode || localActivation?.serviceCode || HERO_SMS_SERVICE_CODE).trim() || HERO_SMS_SERVICE_CODE,
countryId,
...(countryLabel ? { countryLabel } : {}),
successfulUses: Math.max(0, Math.floor(Number(record.successfulUses) || 0)),
@@ -7231,6 +7411,7 @@ function getSourceLabel(source) {
'hotmail-api': 'HotmailAPI对接/本地助手)',
'luckmail-api': 'LuckMailAPI 购邮)',
'cloudflare-temp-email': 'Cloudflare Temp Email',
'cloudmail': 'Cloud Mail',
'plus-checkout': 'Plus Checkout',
'paypal-flow': 'PayPal 授权页',
};
@@ -7495,12 +7676,20 @@ async function restartSignupPhonePasswordMismatchAttemptFromStep(step, restartCo
} = getSignupPhonePasswordMismatchRestartPayload(preservedState);
const emailSuffix = preservedEmail ? `当前邮箱:${preservedEmail}` : '';
const phoneSuffix = activeSignupPhoneNumber ? `当前手机号:${activeSignupPhoneNumber}` : '';
const errorMessage = getErrorMessage(error);
const reasonLabel = /PHONE_RESEND_BANNED_NUMBER::|无法向此(?:电话|手机)号码发送短信|无法发送短信到此(?:电话|手机)号码|unable\s+to\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number/i
.test(errorMessage)
? '当前注册手机号无法接收短信'
: (/与此(?:电话|手机)号码相关联的帐户已存在|account\s+associated\s+with\s+this\s+phone\s+number\s+already\s+exists/i
.test(errorMessage)
? '注册手机号异常'
: '手机号/密码不匹配');
await addLog(
`步骤 ${step}:检测到手机号/密码不匹配,准备丢弃当前注册手机号并回到步骤 1 重新开始(第 ${restartCount} 次重开)。${phoneSuffix}${emailSuffix}原因:${getErrorMessage(error)}`,
`步骤 ${step}:检测到${reasonLabel},准备丢弃当前注册手机号并回到步骤 1 重新开始(第 ${restartCount} 次重开)。${phoneSuffix}${emailSuffix}原因:${errorMessage}`,
'warn'
);
await invalidateDownstreamAfterStepRestart(1, {
logLabel: `步骤 ${step} 检测到手机号/密码不匹配后准备回到步骤 1 重新获取手机号重试(第 ${restartCount} 次重开)`,
logLabel: `步骤 ${step} 检测到${reasonLabel}后准备回到步骤 1 重新获取手机号重试(第 ${restartCount} 次重开)`,
});
if (shouldClearSignupPhoneRuntime) {
await addLog(`步骤 ${step}:已清空本轮注册手机号与接码订单,下一次重开将重新获取号码。`, 'warn');
@@ -9239,6 +9428,7 @@ function getEmailGeneratorLabel(generator) {
}
if (generator === 'cloudflare') return 'Cloudflare 邮箱';
if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email';
if (generator === CLOUD_MAIL_GENERATOR) return 'Cloud Mail';
return 'Duck 邮箱';
}
const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMail2925SessionManager({
@@ -9412,6 +9602,11 @@ async function fetchDuckEmail(options = {}) {
}
async function fetchGeneratedEmail(state, options = {}) {
const currentState = state || await getState();
const generator = normalizeEmailGenerator(options.generator ?? currentState.emailGenerator);
if (generator === CLOUD_MAIL_GENERATOR) {
return fetchCloudMailAddress(currentState, options);
}
return generatedEmailHelpers.fetchGeneratedEmail(state, options);
}
@@ -10317,8 +10512,9 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
throw err;
}
step4RestartCount += 1;
const isPhoneResendBanned = typeof isPhoneResendBannedNumberError === 'function'
&& isPhoneResendBannedNumberError(err);
const isPhoneResendBanned = typeof phoneVerificationHelpers !== 'undefined'
&& typeof phoneVerificationHelpers?.isPhoneResendBannedNumberError === 'function'
&& phoneVerificationHelpers.isPhoneResendBannedNumberError(err);
if (isSignupPhonePasswordMismatchFailure(err) || isPhoneResendBanned) {
await restartSignupPhonePasswordMismatchAttemptFromStep(4, step4RestartCount, err);
} else {
@@ -10554,6 +10750,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create
chrome,
closeConflictingTabsForSource,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER,
completeStepFromBackground,
confirmCustomVerificationStepBypassRequest: (step) => chrome.runtime.sendMessage({
type: 'REQUEST_CUSTOM_VERIFICATION_BYPASS_CONFIRMATION',
@@ -10572,6 +10769,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create
MAIL_2925_VERIFICATION_INTERVAL_MS,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
pollCloudflareTempEmailVerificationCode,
pollCloudMailVerificationCode,
pollHotmailVerificationCode,
pollLuckmailVerificationCode,
sendToContentScript,
@@ -10677,6 +10875,7 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({
isTabAlive,
LUCKMAIL_PROVIDER,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER,
resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep,
reuseOrCreateTab,
sendToContentScript,
@@ -10724,6 +10923,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
addLog,
chrome,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER,
completeStepFromBackground,
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
ensureMail2925MailboxSession,
@@ -11141,6 +11341,9 @@ function getMailConfig(state) {
if (provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
return { provider: CLOUDFLARE_TEMP_EMAIL_PROVIDER, label: 'Cloudflare Temp Email' };
}
if (provider === 'cloudmail') {
return { provider: 'cloudmail', label: 'Cloud Mail' };
}
if (provider === '163') {
return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 邮箱' };
}
+324
View File
@@ -0,0 +1,324 @@
(function cloudMailProviderModule(root, factory) {
root.MultiPageBackgroundCloudMailProvider = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createCloudMailProviderModule() {
function createCloudMailProvider(deps = {}) {
const {
addLog = async () => {},
buildCloudMailHeaders,
CLOUD_MAIL_DEFAULT_PAGE_SIZE = 20,
CLOUD_MAIL_GENERATOR = 'cloudmail',
CLOUD_MAIL_PROVIDER = 'cloudmail',
fetchImpl = typeof fetch === 'function' ? fetch.bind(globalThis) : null,
getCloudMailTokenFromResponse,
getState = async () => ({}),
joinCloudMailUrl,
normalizeCloudMailAddress,
normalizeCloudMailBaseUrl,
normalizeCloudMailDomain,
normalizeCloudMailDomains,
normalizeCloudMailMailApiMessages,
pickVerificationMessageWithTimeFallback,
setEmailState = async () => {},
setPersistentSettings = async () => {},
sleepWithStop = async () => {},
throwIfStopped = () => {},
} = deps;
function getCloudMailConfig(state = {}) {
return {
baseUrl: normalizeCloudMailBaseUrl(state.cloudMailBaseUrl),
adminEmail: String(state.cloudMailAdminEmail || '').trim(),
adminPassword: String(state.cloudMailAdminPassword || ''),
token: String(state.cloudMailToken || '').trim(),
receiveMailbox: normalizeCloudMailReceiveMailbox(state.cloudMailReceiveMailbox),
domain: normalizeCloudMailDomain(state.cloudMailDomain),
domains: normalizeCloudMailDomains(state.cloudMailDomains),
};
}
function normalizeCloudMailReceiveMailbox(value = '') {
const normalized = normalizeCloudMailAddress(value);
if (!normalized) return '';
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized) ? normalized : '';
}
function resolveCloudMailPollTargetEmail(state = {}, pollPayload = {}, config = getCloudMailConfig(state)) {
const configuredReceiveMailbox = normalizeCloudMailReceiveMailbox(config.receiveMailbox);
const mailProvider = String(state?.mailProvider || '').trim().toLowerCase();
const emailGenerator = String(state?.emailGenerator || '').trim().toLowerCase();
const shouldPreferConfiguredReceiveMailbox = mailProvider === CLOUD_MAIL_PROVIDER
&& emailGenerator !== CLOUD_MAIL_GENERATOR;
if (shouldPreferConfiguredReceiveMailbox && configuredReceiveMailbox) {
return configuredReceiveMailbox;
}
const requestedTarget = normalizeCloudMailReceiveMailbox(pollPayload.targetEmail);
if (requestedTarget) {
return requestedTarget;
}
return normalizeCloudMailReceiveMailbox(state.email);
}
function ensureCloudMailConfig(state, options = {}) {
const { requireToken = false, requireCredentials = false, requireDomain = false } = options;
const config = getCloudMailConfig(state);
if (!config.baseUrl) {
throw new Error('Cloud Mail 服务地址为空或格式无效。');
}
if (requireCredentials && (!config.adminEmail || !config.adminPassword)) {
throw new Error('Cloud Mail 缺少管理员邮箱或密码。');
}
if (requireToken && !config.token) {
throw new Error('Cloud Mail 尚未获取到身份令牌,请先生成 Token。');
}
if (requireDomain && !config.domain) {
throw new Error('Cloud Mail 域名为空或格式无效。');
}
return config;
}
async function requestCloudMailJson(config, path, options = {}) {
if (!fetchImpl) {
throw new Error('Cloud Mail 当前运行环境不支持 fetch。');
}
const {
method = 'POST',
payload,
timeoutMs = 20000,
requireToken = true,
} = options;
const url = joinCloudMailUrl(config.baseUrl, path);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
let response;
try {
response = await fetchImpl(url, {
method,
headers: buildCloudMailHeaders(config, {
json: payload !== undefined,
token: requireToken ? undefined : '',
}),
body: payload !== undefined ? JSON.stringify(payload) : undefined,
signal: controller.signal,
});
} catch (err) {
const errorMessage = err?.name === 'AbortError'
? `Cloud Mail 请求超时(>${Math.round(timeoutMs / 1000)} 秒)`
: `Cloud Mail 请求失败:${err.message}`;
throw new Error(errorMessage);
} finally {
clearTimeout(timeoutId);
}
const text = await response.text();
let parsed;
try {
parsed = text ? JSON.parse(text) : {};
} catch {
parsed = text;
}
if (!response.ok) {
const payloadError = typeof parsed === 'object' && parsed
? (parsed.message || parsed.error || parsed.msg)
: '';
throw new Error(`Cloud Mail 请求失败:${payloadError || text || `HTTP ${response.status}`}`);
}
if (parsed && typeof parsed === 'object' && 'code' in parsed && Number(parsed.code) !== 200) {
throw new Error(`Cloud Mail 业务错误:${parsed.message || parsed.msg || `code=${parsed.code}`}`);
}
return parsed;
}
async function ensureCloudMailToken(state, options = {}) {
const { forceRefresh = false } = options;
const latestState = state || await getState();
const config = ensureCloudMailConfig(latestState, { requireCredentials: true });
if (!forceRefresh && config.token) {
return { config, token: config.token };
}
const loginConfig = { ...config, token: '' };
const result = await requestCloudMailJson(loginConfig, '/api/public/genToken', {
method: 'POST',
payload: { email: config.adminEmail, password: config.adminPassword },
requireToken: false,
});
const token = getCloudMailTokenFromResponse(result);
if (!token) {
throw new Error('Cloud Mail 未返回可用 Token。');
}
await setPersistentSettings({ cloudMailToken: token });
return { config: { ...config, token }, token };
}
function generateCloudMailAliasLocalPart() {
const letters = 'abcdefghijklmnopqrstuvwxyz';
const digits = '0123456789';
const chars = [];
for (let i = 0; i < 6; i++) chars.push(letters[Math.floor(Math.random() * letters.length)]);
for (let i = 0; i < 4; i++) chars.push(digits[Math.floor(Math.random() * digits.length)]);
for (let i = chars.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[chars[i], chars[j]] = [chars[j], chars[i]];
}
return chars.join('');
}
async function fetchCloudMailAddress(state, options = {}) {
throwIfStopped();
const latestState = state || await getState();
const { config } = await ensureCloudMailToken(latestState);
const ensuredConfig = ensureCloudMailConfig({ ...latestState, cloudMailToken: config.token }, {
requireToken: true,
requireDomain: true,
});
const requestedLocal = String(options.localPart || options.name || '').trim().toLowerCase()
|| generateCloudMailAliasLocalPart();
const address = `${requestedLocal}@${ensuredConfig.domain}`.toLowerCase();
const payload = { list: [{ email: address }] };
try {
await requestCloudMailJson(ensuredConfig, '/api/public/addUser', { method: 'POST', payload });
} catch (err) {
if (/token|unauthor|401/i.test(String(err?.message || ''))) {
const refreshed = await ensureCloudMailToken(latestState, { forceRefresh: true });
await requestCloudMailJson(refreshed.config, '/api/public/addUser', { method: 'POST', payload });
} else {
throw err;
}
}
await setEmailState(address);
await addLog(`Cloud Mail:已生成 ${address}`, 'ok');
return address;
}
function summarizeCloudMailMessagesForLog(messages) {
return (messages || [])
.slice()
.sort((left, right) => {
const leftTime = Date.parse(left.receivedDateTime || '') || 0;
const rightTime = Date.parse(right.receivedDateTime || '') || 0;
return rightTime - leftTime;
})
.slice(0, 3)
.map((message) => {
const receivedAt = message?.receivedDateTime || '未知时间';
const sender = message?.from?.emailAddress?.address || '未知发件人';
const subject = message?.subject || '(无主题)';
const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80);
const address = message?.address || '未知地址';
return `[${address}] ${receivedAt} | ${sender} | ${subject} | ${preview}`;
})
.join(' || ');
}
async function listCloudMailMessages(state, options = {}) {
const latestState = state || await getState();
const { config } = await ensureCloudMailToken(latestState);
const address = normalizeCloudMailAddress(options.address);
const pageSize = Number(options.limit) || CLOUD_MAIL_DEFAULT_PAGE_SIZE;
const pageNum = Number(options.page) || 1;
const request = async (currentConfig) => requestCloudMailJson(currentConfig, '/api/public/emailList', {
method: 'POST',
payload: {
toEmail: address || undefined,
type: 0,
isDel: 0,
timeSort: 'desc',
num: pageNum,
size: pageSize,
},
});
let payload;
try {
payload = await request(config);
} catch (err) {
if (/token|unauthor|401/i.test(String(err?.message || ''))) {
const refreshed = await ensureCloudMailToken(latestState, { forceRefresh: true });
payload = await request(refreshed.config);
} else {
throw err;
}
}
const messages = normalizeCloudMailMailApiMessages(payload).filter((message) => {
if (!address) return true;
return !message.address || normalizeCloudMailAddress(message.address) === address;
});
return { config, messages };
}
async function pollCloudMailVerificationCode(step, state, pollPayload = {}) {
const latestState = state || await getState();
const config = ensureCloudMailConfig(latestState, { requireCredentials: true });
const targetEmail = resolveCloudMailPollTargetEmail(latestState, pollPayload, config);
const registrationEmail = normalizeCloudMailReceiveMailbox(latestState.email);
if (!targetEmail) {
throw new Error('Cloud Mail 轮询前缺少目标邮箱地址,请先填写注册邮箱或"邮件接收"邮箱。');
}
if (registrationEmail && registrationEmail !== targetEmail) {
await addLog(`步骤 ${step}:正在轮询 Cloud Mail 收件邮箱(${targetEmail}),注册邮箱为 ${registrationEmail}...`, 'info');
} else {
await addLog(`步骤 ${step}:正在轮询 Cloud Mail 邮件(${targetEmail}...`, 'info');
}
const maxAttempts = Number(pollPayload.maxAttempts) || 5;
const intervalMs = Number(pollPayload.intervalMs) || 3000;
let lastError = null;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
throwIfStopped();
try {
const { messages } = await listCloudMailMessages(latestState, {
address: targetEmail,
limit: pollPayload.limit || CLOUD_MAIL_DEFAULT_PAGE_SIZE,
page: pollPayload.page || 1,
});
const matchResult = pickVerificationMessageWithTimeFallback(messages, {
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
senderFilters: pollPayload.senderFilters || [],
subjectFilters: pollPayload.subjectFilters || [],
excludeCodes: pollPayload.excludeCodes || [],
});
const match = matchResult.match;
if (match?.code) {
if (matchResult.usedRelaxedFilters) {
const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配';
await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Cloud Mail 验证码。`, 'warn');
}
return {
ok: true,
code: match.code,
emailTimestamp: match.receivedAt || Date.now(),
mailId: match.message?.id || '',
};
}
lastError = new Error(`步骤 ${step}:暂未在 Cloud Mail 中找到匹配验证码(${attempt}/${maxAttempts})。`);
await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info');
const sample = summarizeCloudMailMessagesForLog(messages);
if (sample) {
await addLog(`步骤 ${step}:最近邮件样本:${sample}`, 'info');
}
} catch (err) {
lastError = err;
await addLog(`步骤 ${step}Cloud Mail 轮询失败:${err.message}`, 'warn');
}
if (attempt < maxAttempts) {
await sleepWithStop(intervalMs);
}
}
throw lastError || new Error(`步骤 ${step}:未在 Cloud Mail 中找到新的匹配验证码。`);
}
return {
ensureCloudMailConfig,
ensureCloudMailToken,
fetchCloudMailAddress,
getCloudMailConfig,
listCloudMailMessages,
normalizeCloudMailReceiveMailbox,
pollCloudMailVerificationCode,
requestCloudMailJson,
resolveCloudMailPollTargetEmail,
};
}
return {
createCloudMailProvider,
};
});
+1
View File
@@ -27,6 +27,7 @@
'hotmail-api': 'HotmailAPI对接/本地助手)',
'luckmail-api': 'LuckMailAPI 购邮)',
'cloudflare-temp-email': 'Cloudflare Temp Email',
'cloudmail': 'Cloud Mail',
};
return labels[source] || source || '未知来源';
}
+49 -5
View File
@@ -100,6 +100,16 @@
const FREE_PHONE_REUSE_PREPARE_MAX_ROUNDS = 10;
const PHONE_SMS_FAILURE_SKIP_THRESHOLD = 2;
const MAX_ACTIVATION_PRICE_HINTS = 256;
const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze([
{ prefix: '84', id: 10, label: 'Vietnam' },
{ prefix: '66', id: 52, label: 'Thailand' },
{ prefix: '62', id: 6, label: 'Indonesia' },
{ prefix: '44', id: 16, label: 'United Kingdom' },
{ prefix: '81', id: 151, label: 'Japan' },
{ prefix: '49', id: 43, label: 'Germany' },
{ prefix: '33', id: 73, label: 'France' },
{ prefix: '1', id: 187, label: 'USA' },
]);
const activationPriceHintsByKey = new Map();
let activePhoneVerificationLogStep = null;
let activePhoneVerificationLogStepKey = null;
@@ -422,6 +432,21 @@
return String(value || '').trim() || fallback;
}
function inferHeroSmsCountryFromPhoneNumber(phoneNumber = '') {
const digits = String(phoneNumber || '').replace(/\D+/g, '');
if (!digits) {
return null;
}
const match = HERO_SMS_COUNTRY_BY_PHONE_PREFIX.find((entry) => digits.startsWith(entry.prefix));
if (!match) {
return null;
}
return {
id: normalizeCountryId(match.id, 0),
label: normalizeCountryLabel(match.label, `Country #${match.id}`),
};
}
function normalizePhoneCodeWaitSeconds(value) {
const parsed = Math.floor(Number(value));
if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -1037,14 +1062,19 @@
const activationId = String(
record.activationId ?? record.id ?? record.activation ?? ''
).trim();
const countryLabel = String(record.countryLabel || '').trim();
const inferredCountry = inferHeroSmsCountryFromPhoneNumber(phoneNumber);
const countryId = normalizeCountryId(record.countryId, inferredCountry?.id || HERO_SMS_COUNTRY_ID);
const countryLabel = String(
record.countryLabel
|| (inferredCountry && inferredCountry.id === countryId ? inferredCountry.label : '')
).trim();
const statusAction = String(record.statusAction || '').trim();
return {
...(activationId ? { activationId } : {}),
phoneNumber,
provider: PHONE_SMS_PROVIDER_HERO,
serviceCode: String(record.serviceCode || HERO_SMS_SERVICE_CODE).trim() || HERO_SMS_SERVICE_CODE,
countryId: normalizeCountryId(record.countryId, HERO_SMS_COUNTRY_ID),
countryId,
...(countryLabel ? { countryLabel } : {}),
successfulUses: normalizeUseCount(record.successfulUses),
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)),
@@ -3935,7 +3965,16 @@
};
}
} else {
const countryId = normalizeCountryId(activation.countryId, 0);
const inferredCountry = inferHeroSmsCountryFromPhoneNumber(activation.phoneNumber);
const rawCountryId = normalizeCountryId(activation.countryId, 0);
const hasExplicitCountry = Object.prototype.hasOwnProperty.call(activation, 'countryId')
&& Number.isFinite(rawCountryId)
&& rawCountryId > 0
&& !(activation.manualOnly && rawCountryId === HERO_SMS_COUNTRY_ID && inferredCountry?.id && inferredCountry.id !== rawCountryId);
const countryId = hasExplicitCountry ? rawCountryId : normalizeCountryId(inferredCountry?.id, rawCountryId);
const countryLabel = hasExplicitCountry
? activation.countryLabel
: (inferredCountry?.label || activation.countryLabel);
if (countryId > 0) {
const matched = candidates.find((entry) => entry.id === countryId);
if (matched) {
@@ -3943,7 +3982,7 @@
}
return {
id: countryId,
label: normalizeCountryLabel(activation.countryLabel, `Country #${countryId}`),
label: normalizeCountryLabel(countryLabel, `Country #${countryId}`),
};
}
}
@@ -4286,7 +4325,11 @@
return null;
}
if (normalizeFreePhoneReuseAutoEnabled(state)) {
const canPrepareAutomaticFreeReuse = normalizeFreePhoneReuseAutoEnabled(state)
&& !freeReusableActivation.manualOnly
&& Boolean(String(freeReusableActivation.activationId || '').trim());
if (canPrepareAutomaticFreeReuse) {
await addLog(
`步骤 9:准备自动白嫖复用已保存手机号 ${freeReusableActivation.phoneNumber}${freeReusableActivation.successfulUses + 1}/${freeReusableActivation.maxUses})。`,
'info'
@@ -6245,6 +6288,7 @@
completeSignupPhoneVerificationFlow,
finalizeLoginPhoneActivationAfterSuccess,
finalizeSignupPhoneActivationAfterSuccess,
isPhoneResendBannedNumberError,
normalizeActivation,
pollPhoneActivationCode,
prepareLoginPhoneActivation,
+2
View File
@@ -10,6 +10,7 @@
addLog: rawAddLog = async () => {},
chrome,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER = 'cloudmail',
completeStepFromBackground,
confirmCustomVerificationStepBypass,
ensureMail2925MailboxSession,
@@ -484,6 +485,7 @@
mail.provider === HOTMAIL_PROVIDER
|| mail.provider === LUCKMAIL_PROVIDER
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
|| mail.provider === CLOUD_MAIL_PROVIDER
) {
await addLog(`步骤 ${visibleStep}:正在通过 ${mail.label} 轮询验证码...`);
} else {
+3
View File
@@ -19,6 +19,7 @@
isTabAlive,
LUCKMAIL_PROVIDER,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER = 'cloudmail',
resolveVerificationStep,
reuseOrCreateTab,
sendToContentScript,
@@ -238,6 +239,7 @@
mail.provider === HOTMAIL_PROVIDER
|| mail.provider === LUCKMAIL_PROVIDER
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
|| mail.provider === CLOUD_MAIL_PROVIDER
) {
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
} else if (mail.provider === '2925') {
@@ -263,6 +265,7 @@
HOTMAIL_PROVIDER,
LUCKMAIL_PROVIDER,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER,
].includes(mail.provider);
const signupProfile = buildSignupProfileForVerificationStep();
+9
View File
@@ -10,6 +10,7 @@
chrome,
closeConflictingTabsForSource,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER = 'cloudmail',
completeStepFromBackground,
confirmCustomVerificationStepBypassRequest,
getHotmailVerificationPollConfig,
@@ -24,6 +25,7 @@
MAIL_2925_VERIFICATION_INTERVAL_MS,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
pollCloudflareTempEmailVerificationCode,
pollCloudMailVerificationCode,
pollHotmailVerificationCode,
pollLuckmailVerificationCode,
sendToContentScript,
@@ -927,6 +929,13 @@
}, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
return pollCloudflareTempEmailVerificationCode(step, state, timedPoll.payload);
}
if (mail.provider === CLOUD_MAIL_PROVIDER) {
const timedPoll = await applyMailPollingTimeBudget(step, {
...getVerificationPollPayload(step, state),
...cleanPollOverrides,
}, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
return pollCloudMailVerificationCode(step, state, timedPoll.payload);
}
if (Number(pollOverrides.resendIntervalMs) > 0) {
return pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides);
+228
View File
@@ -0,0 +1,228 @@
(function cloudMailUtilsModule(root, factory) {
if (typeof module !== 'undefined' && module.exports) {
module.exports = factory();
return;
}
root.CloudMailUtils = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createCloudMailUtils() {
const DEFAULT_MAIL_PAGE_SIZE = 20;
function firstNonEmptyString(values) {
for (const value of values) {
if (value === undefined || value === null) continue;
const normalized = String(value).trim();
if (normalized) return normalized;
}
return '';
}
function normalizeCloudMailBaseUrl(rawValue = '') {
const value = String(rawValue || '').trim();
if (!value) return '';
const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`;
try {
const parsed = new URL(candidate);
parsed.hash = '';
parsed.search = '';
const pathname = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/+$/, '');
return `${parsed.origin}${pathname}`;
} catch {
return '';
}
}
function normalizeCloudMailDomain(rawValue = '') {
let value = String(rawValue || '').trim().toLowerCase();
if (!value) return '';
value = value.replace(/^@+/, '');
value = value.replace(/^https?:\/\//, '');
value = value.replace(/\/.*$/, '');
if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(value)) {
return '';
}
return value;
}
function normalizeCloudMailDomains(values) {
const domains = [];
const seen = new Set();
for (const value of Array.isArray(values) ? values : []) {
const normalized = normalizeCloudMailDomain(value);
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
domains.push(normalized);
}
return domains;
}
function buildCloudMailHeaders(config = {}, options = {}) {
const headers = {};
const token = firstNonEmptyString([
config.token,
config.cloudMailToken,
options.token,
]);
if (token) {
headers.Authorization = token;
}
if (options.json) {
headers['Content-Type'] = 'application/json';
}
if (options.acceptJson !== false) {
headers.Accept = 'application/json';
}
return headers;
}
function joinCloudMailUrl(baseUrl, path) {
const normalizedBase = normalizeCloudMailBaseUrl(baseUrl);
const normalizedPath = String(path || '').trim();
if (!normalizedBase || !normalizedPath) return normalizedBase || '';
return `${normalizedBase}${normalizedPath.startsWith('/') ? '' : '/'}${normalizedPath}`;
}
function getCloudMailMailRows(payload) {
if (Array.isArray(payload)) return payload;
if (!payload || typeof payload !== 'object') return [];
const candidates = [
payload.data,
payload.list,
payload.items,
payload.rows,
payload.records,
payload?.data?.list,
payload?.data?.records,
payload?.data?.rows,
];
for (const candidate of candidates) {
if (Array.isArray(candidate)) {
return candidate;
}
}
return [];
}
function normalizeCloudMailAddress(value) {
return String(value || '').trim().toLowerCase();
}
function stripHtmlTags(value = '') {
return String(value || '')
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.replace(/\s+/g, ' ')
.trim();
}
function parseCloudMailCreateTime(value) {
if (value === undefined || value === null || value === '') return '';
if (typeof value === 'number' && Number.isFinite(value)) {
return new Date(value).toISOString();
}
const source = String(value).trim();
if (!source) return '';
// Cloud Mail returns UTC time like "2099-12-30 23:59:59"; treat as UTC.
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:Z|([+-]\d{2}:?\d{2}))?$/);
if (match) {
const [, year, month, day, hour, minute, second, ms, offset] = match;
let iso = `${year}-${month}-${day}T${hour}:${minute}:${second}`;
if (ms) iso += `.${ms}`;
if (offset) {
iso += offset.includes(':') ? offset : `${offset.slice(0, 3)}:${offset.slice(3)}`;
} else {
iso += 'Z';
}
const parsed = Date.parse(iso);
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : iso;
}
const parsed = Date.parse(source);
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : source;
}
function normalizeCloudMailMessage(row = {}) {
if (!row || typeof row !== 'object') return null;
const address = normalizeCloudMailAddress(firstNonEmptyString([
row.toEmail,
row.to_email,
row.recipient,
row.address,
row.email,
]));
const subject = firstNonEmptyString([row.subject, row.title]);
const fromAddress = firstNonEmptyString([
row.sendEmail,
row.send_email,
row.from,
row.sender,
row.mailFrom,
]);
const htmlContent = firstNonEmptyString([row.content, row.html]);
const textContent = firstNonEmptyString([row.text, row.plainText, row.content_text]);
const bodyPreview = (textContent
|| stripHtmlTags(htmlContent)
|| '').replace(/\s+/g, ' ').trim();
return {
id: firstNonEmptyString([row.emailId, row.id, row.mailId, row.mail_id]),
address,
addressId: '',
subject,
from: {
emailAddress: {
address: fromAddress,
},
},
bodyPreview,
raw: htmlContent || textContent || '',
receivedDateTime: parseCloudMailCreateTime(firstNonEmptyString([
row.createTime,
row.create_time,
row.createdAt,
row.created_at,
row.receivedDateTime,
row.date,
])),
};
}
function normalizeCloudMailMailApiMessages(payload) {
return getCloudMailMailRows(payload)
.map((row) => normalizeCloudMailMessage(row))
.filter(Boolean);
}
function getCloudMailTokenFromResponse(payload = {}) {
return firstNonEmptyString([
payload?.data?.token,
payload?.token,
payload?.data?.accessToken,
payload?.accessToken,
]);
}
return {
DEFAULT_MAIL_PAGE_SIZE,
buildCloudMailHeaders,
getCloudMailTokenFromResponse,
joinCloudMailUrl,
normalizeCloudMailAddress,
normalizeCloudMailBaseUrl,
normalizeCloudMailDomain,
normalizeCloudMailDomains,
normalizeCloudMailMailApiMessages,
normalizeCloudMailMessage,
};
});
+37
View File
@@ -414,6 +414,7 @@
<option value="2925">2925 邮箱 (2925.com)</option>
<option value="gmail">Gmail 邮箱 (mail.google.com)</option>
<option value="cloudflare-temp-email">Cloudflare Temp Email</option>
<option value="cloudmail">Cloud Mail</option>
</select>
<button id="btn-mail-login" class="btn btn-outline btn-sm data-inline-btn" type="button" disabled>登录</button>
</div>
@@ -439,6 +440,7 @@
<option value="cloudflare">Cloudflare</option>
<option value="icloud">iCloud 隐私邮箱</option>
<option value="cloudflare-temp-email">Cloudflare Temp Email</option>
<option value="cloudmail">Cloud Mail</option>
</select>
</div>
<div class="data-row data-check-row" id="row-custom-email-pool" style="display:none;">
@@ -704,6 +706,41 @@
</div>
</div>
</div>
<div id="cloud-mail-section" class="data-card hotmail-card" style="display:none;">
<div class="section-mini-header">
<div class="section-mini-copy">
<span class="section-label">Cloud Mail</span>
<span class="data-value">用于生成邮箱或接收转发邮件(skymail.ink API</span>
</div>
</div>
<div class="data-row" id="row-cloud-mail-base-url" style="display:none;">
<span class="data-label">API 地址</span>
<input type="text" id="input-cloud-mail-base-url" class="data-input" placeholder="https://your-cloudmail-domain" />
</div>
<div class="data-row" id="row-cloud-mail-admin-email" style="display:none;">
<span class="data-label">管理员邮箱</span>
<input type="text" id="input-cloud-mail-admin-email" class="data-input" placeholder="admin@example.com" />
</div>
<div class="data-row" id="row-cloud-mail-admin-password" style="display:none;">
<span class="data-label">管理员密码</span>
<div class="input-with-icon">
<input type="password" id="input-cloud-mail-admin-password" class="data-input data-input-with-icon"
placeholder="Cloud Mail 管理员密码" />
<button id="btn-toggle-cloud-mail-admin-password" class="input-icon-btn" type="button"
data-password-toggle="input-cloud-mail-admin-password" data-show-label="显示 Cloud Mail 密码"
data-hide-label="隐藏 Cloud Mail 密码" aria-label="显示 Cloud Mail 密码" title="显示 Cloud Mail 密码"></button>
</div>
</div>
<div class="data-row" id="row-cloud-mail-receive-mailbox" style="display:none;">
<span class="data-label">邮件接收</span>
<input type="text" id="input-cloud-mail-receive-mailbox" class="data-input"
placeholder="用于接收转发邮件的邮箱,例如 1@email.example.com" />
</div>
<div class="data-row" id="row-cloud-mail-domain" style="display:none;">
<span class="data-label">域名</span>
<input type="text" id="input-cloud-mail-domain" class="data-input" placeholder="例如 mail.example.com" />
</div>
</div>
<div id="hotmail-section" class="data-card hotmail-card" style="display:none;">
<div class="section-mini-header">
<div class="section-mini-copy">
+98 -1
View File
@@ -262,6 +262,17 @@ const btnTempEmailDomainMode = document.getElementById('btn-temp-email-domain-mo
const cloudflareTempEmailSection = document.getElementById('cloudflare-temp-email-section');
const btnCloudflareTempEmailUsageGuide = document.getElementById('btn-cloudflare-temp-email-usage-guide');
const btnCloudflareTempEmailGithub = document.getElementById('btn-cloudflare-temp-email-github');
const cloudMailSection = document.getElementById('cloud-mail-section');
const rowCloudMailBaseUrl = document.getElementById('row-cloud-mail-base-url');
const rowCloudMailAdminEmail = document.getElementById('row-cloud-mail-admin-email');
const rowCloudMailAdminPassword = document.getElementById('row-cloud-mail-admin-password');
const rowCloudMailReceiveMailbox = document.getElementById('row-cloud-mail-receive-mailbox');
const rowCloudMailDomain = document.getElementById('row-cloud-mail-domain');
const inputCloudMailBaseUrl = document.getElementById('input-cloud-mail-base-url');
const inputCloudMailAdminEmail = document.getElementById('input-cloud-mail-admin-email');
const inputCloudMailAdminPassword = document.getElementById('input-cloud-mail-admin-password');
const inputCloudMailReceiveMailbox = document.getElementById('input-cloud-mail-receive-mailbox');
const inputCloudMailDomain = document.getElementById('input-cloud-mail-domain');
const hotmailSection = document.getElementById('hotmail-section');
const mail2925Section = document.getElementById('mail2925-section');
const luckmailSection = document.getElementById('luckmail-section');
@@ -2619,6 +2630,18 @@ function normalizeCloudflareTempEmailDomains(values = []) {
return domains;
}
function normalizeCloudMailBaseUrlValue(value = '') {
return normalizeCloudflareTempEmailBaseUrlValue(value);
}
function normalizeCloudMailReceiveMailboxValue(value = '') {
return normalizeCloudflareTempEmailReceiveMailboxValue(value);
}
function normalizeCloudMailDomainValue(value = '') {
return normalizeCloudflareDomainValue(value);
}
function getCloudflareDomainsFromState() {
const domains = normalizeCloudflareDomains(latestState?.cloudflareDomains || []);
const activeDomain = normalizeCloudflareDomainValue(latestState?.cloudflareDomain || '');
@@ -2695,6 +2718,24 @@ function applyCloudflareTempEmailSettingsState(state = {}) {
setCloudflareTempEmailDomainEditMode(false, { clearInput: true });
}
function applyCloudMailSettingsState(state = {}) {
if (inputCloudMailBaseUrl) {
inputCloudMailBaseUrl.value = state?.cloudMailBaseUrl || '';
}
if (inputCloudMailAdminEmail) {
inputCloudMailAdminEmail.value = state?.cloudMailAdminEmail || '';
}
if (inputCloudMailAdminPassword) {
inputCloudMailAdminPassword.value = state?.cloudMailAdminPassword || '';
}
if (inputCloudMailReceiveMailbox) {
inputCloudMailReceiveMailbox.value = state?.cloudMailReceiveMailbox || '';
}
if (inputCloudMailDomain) {
inputCloudMailDomain.value = state?.cloudMailDomain || '';
}
}
function collectSettingsPayload() {
const defaultGpcHelperApiUrl = typeof DEFAULT_GPC_HELPER_API_URL !== 'undefined'
? DEFAULT_GPC_HELPER_API_URL
@@ -2707,6 +2748,15 @@ function collectSettingsPayload() {
const selectedCloudflareTempEmailDomain = normalizeCloudflareTempEmailDomainValue(
!cloudflareTempEmailDomainEditMode ? selectTempEmailDomain.value : tempEmailActiveDomain
) || tempEmailActiveDomain;
const normalizeCloudMailBaseUrlInput = typeof normalizeCloudMailBaseUrlValue === 'function'
? normalizeCloudMailBaseUrlValue
: normalizeCloudflareTempEmailBaseUrlValue;
const normalizeCloudMailReceiveMailboxInput = typeof normalizeCloudMailReceiveMailboxValue === 'function'
? normalizeCloudMailReceiveMailboxValue
: normalizeCloudflareTempEmailReceiveMailboxValue;
const normalizeCloudMailDomainInput = typeof normalizeCloudMailDomainValue === 'function'
? normalizeCloudMailDomainValue
: normalizeCloudflareTempEmailDomainValue;
const contributionModeEnabled = Boolean(latestState?.contributionMode);
const icloudFetchModeRawValue = typeof selectIcloudFetchMode !== 'undefined'
? String(selectIcloudFetchMode?.value || '')
@@ -3357,6 +3407,11 @@ function collectSettingsPayload() {
cloudflareTempEmailUseRandomSubdomain: Boolean(inputTempEmailUseRandomSubdomain?.checked),
cloudflareTempEmailDomain: selectedCloudflareTempEmailDomain,
cloudflareTempEmailDomains: tempEmailDomains,
cloudMailBaseUrl: normalizeCloudMailBaseUrlInput((typeof inputCloudMailBaseUrl !== 'undefined' && inputCloudMailBaseUrl) ? inputCloudMailBaseUrl.value : ''),
cloudMailAdminEmail: ((typeof inputCloudMailAdminEmail !== 'undefined' && inputCloudMailAdminEmail) ? inputCloudMailAdminEmail.value : '').trim(),
cloudMailAdminPassword: (typeof inputCloudMailAdminPassword !== 'undefined' && inputCloudMailAdminPassword) ? inputCloudMailAdminPassword.value : '',
cloudMailReceiveMailbox: normalizeCloudMailReceiveMailboxInput((typeof inputCloudMailReceiveMailbox !== 'undefined' && inputCloudMailReceiveMailbox) ? inputCloudMailReceiveMailbox.value : ''),
cloudMailDomain: normalizeCloudMailDomainInput((typeof inputCloudMailDomain !== 'undefined' && inputCloudMailDomain) ? inputCloudMailDomain.value : ''),
autoRunSkipFailures: inputAutoSkipFailures.checked,
autoRunFallbackThreadIntervalMinutes: normalizeAutoRunThreadIntervalMinutes(inputAutoSkipFailuresThreadIntervalMinutes.value),
step6CookieCleanupEnabled: typeof inputStep6CookieCleanupEnabled !== 'undefined' && inputStep6CookieCleanupEnabled
@@ -8092,7 +8147,7 @@ function applySettingsState(state) {
inputCodex2ApiUrl.value = state?.codex2apiUrl || '';
inputCodex2ApiAdminKey.value = state?.codex2apiAdminKey || '';
const restoredMailProvider = isCustomMailProvider(state?.mailProvider)
|| [ICLOUD_PROVIDER, 'hotmail-api', GMAIL_PROVIDER, 'luckmail-api', '163', '163-vip', '126', 'qq', 'inbucket', '2925', 'cloudflare-temp-email'].includes(String(state?.mailProvider || '').trim())
|| [ICLOUD_PROVIDER, 'hotmail-api', GMAIL_PROVIDER, 'luckmail-api', '163', '163-vip', '126', 'qq', 'inbucket', '2925', 'cloudflare-temp-email', 'cloudmail'].includes(String(state?.mailProvider || '').trim())
? String(state?.mailProvider || '163').trim()
: (String(state?.emailGenerator || '').trim().toLowerCase() === 'custom'
|| String(state?.emailGenerator || '').trim().toLowerCase() === 'manual'
@@ -8114,6 +8169,8 @@ function applySettingsState(state) {
selectEmailGenerator.value = 'cloudflare';
} else if (restoredEmailGenerator === 'cloudflare-temp-email') {
selectEmailGenerator.value = 'cloudflare-temp-email';
} else if (restoredEmailGenerator === 'cloudmail') {
selectEmailGenerator.value = 'cloudmail';
} else {
selectEmailGenerator.value = 'duck';
}
@@ -8169,6 +8226,9 @@ function applySettingsState(state) {
selectLuckmailEmailType.value = normalizeLuckmailEmailType(state?.luckmailEmailType);
inputLuckmailDomain.value = state?.luckmailDomain || '';
applyCloudflareTempEmailSettingsState(state);
if (typeof applyCloudMailSettingsState === 'function') {
applyCloudMailSettingsState(state);
}
renderCloudflareDomainOptions(state?.cloudflareDomain || '');
setCloudflareDomainEditMode(false, { clearInput: true });
inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures);
@@ -8915,6 +8975,7 @@ function getSelectedEmailGenerator() {
}
if (generator === 'cloudflare') return 'cloudflare';
if (generator === 'cloudflare-temp-email') return 'cloudflare-temp-email';
if (generator === 'cloudmail') return 'cloudmail';
return 'duck';
}
@@ -8962,6 +9023,14 @@ function getEmailGeneratorUiCopy() {
label: 'Cloudflare Temp Email',
};
}
if (getSelectedEmailGenerator() === 'cloudmail') {
return {
buttonLabel: '生成',
placeholder: '点击生成 Cloud Mail 邮箱,或手动粘贴邮箱',
successVerb: '生成',
label: 'Cloud Mail',
};
}
return {
buttonLabel: '获取',
@@ -9361,6 +9430,7 @@ function updateMailProviderUI() {
const useIcloudProvider = isIcloudMailProvider();
const useEmailGenerator = !useHotmail && !useLuckmail && !useCustomEmail && (!useGeneratedAlias || useGmail);
const useCloudflareTempEmailProvider = selectMailProvider.value === 'cloudflare-temp-email';
const useCloudMailProvider = selectMailProvider.value === 'cloudmail';
const aliasUiCopy = useGeneratedAlias
? getManagedAliasProviderUiCopy(selectMailProvider.value, mail2925Mode)
: null;
@@ -9383,9 +9453,13 @@ function updateMailProviderUI() {
const useCloudflare = selectedGenerator === 'cloudflare';
const useIcloud = selectedGenerator === 'icloud';
const useCloudflareTempEmailGenerator = selectedGenerator === 'cloudflare-temp-email';
const useCloudMailGenerator = selectedGenerator === 'cloudmail';
const showCloudflareDomain = useEmailGenerator && useCloudflare;
const showCloudflareTempEmailSettings = useCloudflareTempEmailProvider || (useEmailGenerator && useCloudflareTempEmailGenerator);
const showCloudflareTempEmailReceiveMailbox = useCloudflareTempEmailProvider && !useCloudflareTempEmailGenerator;
const showCloudMailSettings = useCloudMailProvider || (useEmailGenerator && useCloudMailGenerator);
const showCloudMailReceiveMailbox = useCloudMailProvider && !useCloudMailGenerator;
const showCloudMailDomain = useEmailGenerator && useCloudMailGenerator;
const selectedIcloudHost = typeof getSelectedIcloudHostPreference === 'function'
? getSelectedIcloudHostPreference()
: (normalizeIcloudHostValue(icloudHostPreferenceValue || latestState?.icloudHostPreference || '')
@@ -9411,6 +9485,14 @@ function updateMailProviderUI() {
if (cloudflareTempEmailSection) {
cloudflareTempEmailSection.style.display = showCloudflareTempEmailSettings ? '' : 'none';
}
if (typeof cloudMailSection !== 'undefined' && cloudMailSection) {
cloudMailSection.style.display = showCloudMailSettings ? '' : 'none';
}
if (typeof rowCloudMailBaseUrl !== 'undefined' && rowCloudMailBaseUrl) rowCloudMailBaseUrl.style.display = showCloudMailSettings ? '' : 'none';
if (typeof rowCloudMailAdminEmail !== 'undefined' && rowCloudMailAdminEmail) rowCloudMailAdminEmail.style.display = showCloudMailSettings ? '' : 'none';
if (typeof rowCloudMailAdminPassword !== 'undefined' && rowCloudMailAdminPassword) rowCloudMailAdminPassword.style.display = showCloudMailSettings ? '' : 'none';
if (typeof rowCloudMailReceiveMailbox !== 'undefined' && rowCloudMailReceiveMailbox) rowCloudMailReceiveMailbox.style.display = showCloudMailReceiveMailbox ? '' : 'none';
if (typeof rowCloudMailDomain !== 'undefined' && rowCloudMailDomain) rowCloudMailDomain.style.display = showCloudMailDomain ? '' : 'none';
if (icloudSection) {
const showIcloudSection = (useEmailGenerator && useIcloud) || useIcloudProvider;
icloudSection.style.display = showIcloudSection ? '' : 'none';
@@ -13290,6 +13372,21 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
) {
updateMailProviderUI();
}
if (message.payload.cloudMailBaseUrl !== undefined && inputCloudMailBaseUrl) {
inputCloudMailBaseUrl.value = message.payload.cloudMailBaseUrl || '';
}
if (message.payload.cloudMailAdminEmail !== undefined && inputCloudMailAdminEmail) {
inputCloudMailAdminEmail.value = message.payload.cloudMailAdminEmail || '';
}
if (message.payload.cloudMailAdminPassword !== undefined && inputCloudMailAdminPassword) {
inputCloudMailAdminPassword.value = message.payload.cloudMailAdminPassword || '';
}
if (message.payload.cloudMailReceiveMailbox !== undefined && inputCloudMailReceiveMailbox) {
inputCloudMailReceiveMailbox.value = message.payload.cloudMailReceiveMailbox || '';
}
if (message.payload.cloudMailDomain !== undefined && inputCloudMailDomain) {
inputCloudMailDomain.value = message.payload.cloudMailDomain || '';
}
if (message.payload.plusModeEnabled !== undefined && inputPlusModeEnabled) {
inputPlusModeEnabled.checked = Boolean(message.payload.plusModeEnabled);
}
+6 -4
View File
@@ -742,9 +742,11 @@ function getErrorMessage(error) {
return error?.message || String(error || '');
}
function isPhoneResendBannedNumberError(error) {
return String(error?.message || error || '').startsWith('PHONE_RESEND_BANNED_NUMBER::');
}
const phoneVerificationHelpers = {
isPhoneResendBannedNumberError(error) {
return String(error?.message || error || '').startsWith('PHONE_RESEND_BANNED_NUMBER::');
},
};
async function getLoginAuthStateFromContent() {
return { state: 'password_page', url: 'https://auth.openai.com/log-in' };
@@ -771,7 +773,7 @@ return {
{
step: 1,
options: {
logLabel: '步骤 4 检测到手机号/密码不匹配后准备回到步骤 1 重新获取手机号重试(第 1 次重开)',
logLabel: '步骤 4 检测到当前注册手机号无法接收短信后准备回到步骤 1 重新获取手机号重试(第 1 次重开)',
},
},
]);
@@ -28,6 +28,47 @@ test('background free reusable phone setter does not depend on module-scoped pho
assert.match(setterBlock, /maxUses:\s*Math\.max\(1,\s*Math\.floor\(Number\(record\.maxUses\)\s*\|\|\s*3\)\)/);
});
test('background free reusable phone setter can recover local HeroSMS activation id by phone number', () => {
const source = fs.readFileSync('background.js', 'utf8');
const setterStart = source.indexOf('async function setFreeReusablePhoneActivation');
const setterEnd = source.indexOf('// ============================================================\n// Tab Registry', setterStart);
const setterBlock = source.slice(setterStart, setterEnd);
assert.match(source, /function findLocalHeroSmsActivationForPhone\(/);
assert.match(source, /state\.currentPhoneActivation/);
assert.match(source, /state\.reusablePhoneActivation/);
assert.match(source, /state\.signupPhoneActivation/);
assert.match(source, /state\.signupPhoneCompletedActivation/);
assert.match(source, /state\.phonePreferredActivation/);
assert.match(source, /state\.phoneReusableActivationPool/);
assert.match(setterBlock, /findLocalHeroSmsActivationForPhone\(state,\s*phoneNumber\)/);
assert.match(setterBlock, /activationId = String\(\s*record\.activationId[\s\S]*localActivation\?\.activationId/);
assert.match(setterBlock, /manualOnly:\s*!activationId/);
});
test('background HeroSMS phone prefix inference covers built-in major countries', () => {
const source = fs.readFileSync('background.js', 'utf8');
const supportedStart = source.indexOf('const HERO_SMS_SUPPORTED_COUNTRY_IDS = [');
const prefixStart = source.indexOf('const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze([');
const prefixEnd = source.indexOf(']);', prefixStart);
const supportedBlock = source.slice(supportedStart, source.indexOf('];', supportedStart));
const prefixBlock = source.slice(prefixStart, prefixEnd);
assert.match(supportedBlock, /\[6,\s*52,\s*187,\s*16,\s*151,\s*43,\s*73,\s*10/);
[
['84', 10, 'Vietnam'],
['66', 52, 'Thailand'],
['62', 6, 'Indonesia'],
['44', 16, 'United Kingdom'],
['81', 151, 'Japan'],
['49', 43, 'Germany'],
['33', 73, 'France'],
['1', 187, 'USA'],
].forEach(([prefix, id, label]) => {
assert.match(prefixBlock, new RegExp(`prefix:\\s*'${prefix}'[\\s\\S]*id:\\s*${id}[\\s\\S]*label:\\s*'${label}'`));
});
});
test('message router module exposes a factory', () => {
const source = fs.readFileSync('background/message-router.js', 'utf8');
const globalScope = {};
+156
View File
@@ -0,0 +1,156 @@
const test = require('node:test');
const assert = require('node:assert/strict');
require('../background/cloudmail-provider.js');
function createProviderApi(options = {}) {
const {
receiveMailbox = '',
messages = [{
id: 'mail-1',
address: 'user@example.com',
receivedDateTime: '2026-05-07T09:20:00.000Z',
subject: 'OpenAI verification code',
from: { emailAddress: { address: 'noreply@tm.openai.com' } },
bodyPreview: 'Your verification code is 123456.',
}],
} = options;
const logs = [];
const listCalls = [];
const fetchImpl = async (url, request = {}) => {
const payload = request.body ? JSON.parse(request.body) : {};
if (String(url).includes('/api/public/emailList')) {
listCalls.push(payload.toEmail || '');
return {
ok: true,
text: async () => JSON.stringify({ code: 200, data: { records: messages } }),
};
}
return {
ok: true,
text: async () => JSON.stringify({ code: 200, data: { token: 'token' } }),
};
};
const api = globalThis.MultiPageBackgroundCloudMailProvider.createCloudMailProvider({
addLog: async (message, level) => logs.push({ message, level }),
buildCloudMailHeaders: () => ({}),
CLOUD_MAIL_DEFAULT_PAGE_SIZE: 20,
CLOUD_MAIL_GENERATOR: 'cloudmail',
CLOUD_MAIL_PROVIDER: 'cloudmail',
fetchImpl,
getCloudMailTokenFromResponse: () => 'token',
getState: async () => ({}),
joinCloudMailUrl: (baseUrl, path) => `${baseUrl}${path}`,
normalizeCloudMailAddress: (value) => String(value || '').trim().toLowerCase(),
normalizeCloudMailBaseUrl: (value) => String(value || '').trim(),
normalizeCloudMailDomain: (value) => String(value || '').trim(),
normalizeCloudMailDomains: (values) => values || [],
normalizeCloudMailMailApiMessages: (payload) => payload?.data?.records || [],
pickVerificationMessageWithTimeFallback: (currentMessages) => ({
match: currentMessages[0]
? {
code: String(currentMessages[0].bodyPreview).match(/(\d{6})/)[1],
receivedAt: Date.parse(currentMessages[0].receivedDateTime),
message: currentMessages[0],
}
: null,
usedRelaxedFilters: false,
usedTimeFallback: false,
}),
setEmailState: async () => {},
setPersistentSettings: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
return {
...api,
snapshot() {
return { logs, listCalls };
},
};
}
test('pollCloudMailVerificationCode returns code for generated Cloud Mail address', async () => {
const api = createProviderApi();
const result = await api.pollCloudMailVerificationCode(4, {
email: 'user@example.com',
cloudMailBaseUrl: 'https://mail.example.com',
cloudMailAdminEmail: 'admin@example.com',
cloudMailAdminPassword: 'secret',
cloudMailToken: 'token',
}, {
targetEmail: 'user@example.com',
maxAttempts: 1,
intervalMs: 1,
});
assert.equal(result.code, '123456');
assert.deepEqual(api.snapshot().listCalls, ['user@example.com']);
});
test('pollCloudMailVerificationCode prefers configured receive mailbox over registration email', async () => {
const api = createProviderApi({
receiveMailbox: 'forward-box@example.com',
messages: [{
id: 'mail-2',
address: 'forward-box@example.com',
receivedDateTime: '2026-05-07T10:20:00.000Z',
subject: 'Login verification code',
from: { emailAddress: { address: 'noreply@tm.openai.com' } },
bodyPreview: 'Your verification code is 654321.',
}],
});
const result = await api.pollCloudMailVerificationCode(8, {
email: 'duck-forwarded@duck.com',
mailProvider: 'cloudmail',
emailGenerator: 'duck',
cloudMailBaseUrl: 'https://mail.example.com',
cloudMailReceiveMailbox: 'forward-box@example.com',
cloudMailAdminEmail: 'admin@example.com',
cloudMailAdminPassword: 'secret',
cloudMailToken: 'token',
}, {
targetEmail: 'duck-forwarded@duck.com',
maxAttempts: 1,
intervalMs: 1,
});
assert.equal(result.code, '654321');
assert.deepEqual(api.snapshot().listCalls, ['forward-box@example.com']);
});
test('pollCloudMailVerificationCode ignores stale receive mailbox when generator owns the address', async () => {
const api = createProviderApi({
receiveMailbox: 'forward-box@example.com',
messages: [{
id: 'mail-3',
address: 'generated@example.com',
receivedDateTime: '2026-05-07T11:20:00.000Z',
subject: 'Signup verification code',
from: { emailAddress: { address: 'noreply@tm.openai.com' } },
bodyPreview: 'Your verification code is 246810.',
}],
});
const result = await api.pollCloudMailVerificationCode(4, {
email: 'generated@example.com',
mailProvider: 'cloudmail',
emailGenerator: 'cloudmail',
cloudMailBaseUrl: 'https://mail.example.com',
cloudMailReceiveMailbox: 'forward-box@example.com',
cloudMailAdminEmail: 'admin@example.com',
cloudMailAdminPassword: 'secret',
cloudMailToken: 'token',
}, {
targetEmail: 'generated@example.com',
maxAttempts: 1,
intervalMs: 1,
});
assert.equal(result.code, '246810');
assert.deepEqual(api.snapshot().listCalls, ['generated@example.com']);
});
+73
View File
@@ -0,0 +1,73 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const {
buildCloudMailHeaders,
getCloudMailTokenFromResponse,
normalizeCloudMailBaseUrl,
normalizeCloudMailDomain,
normalizeCloudMailDomains,
normalizeCloudMailMailApiMessages,
} = require('../cloudmail-utils.js');
test('normalizeCloudMailBaseUrl normalizes host and preserves path', () => {
assert.equal(
normalizeCloudMailBaseUrl('mail.example.com/api/'),
'https://mail.example.com/api'
);
assert.equal(
normalizeCloudMailBaseUrl('http://127.0.0.1:8080'),
'http://127.0.0.1:8080'
);
assert.equal(normalizeCloudMailBaseUrl('::::'), '');
});
test('normalizeCloudMailDomain and domains de-duplicate valid entries', () => {
assert.equal(normalizeCloudMailDomain('@Mail.Example.com'), 'mail.example.com');
assert.equal(normalizeCloudMailDomain('not-a-domain'), '');
assert.deepEqual(
normalizeCloudMailDomains(['mail.example.com', 'MAIL.EXAMPLE.COM', 'bad-value']),
['mail.example.com']
);
});
test('buildCloudMailHeaders includes token and content type when needed', () => {
assert.deepEqual(
buildCloudMailHeaders({ token: 'token-value' }, { json: true }),
{
Authorization: 'token-value',
'Content-Type': 'application/json',
Accept: 'application/json',
}
);
});
test('normalizeCloudMailMailApiMessages extracts rows from nested Cloud Mail payloads', () => {
const messages = normalizeCloudMailMailApiMessages({
data: {
records: [
{
emailId: 'mail-1',
toEmail: 'User@Example.com',
sendEmail: 'noreply@tm.openai.com',
subject: 'OpenAI verification code',
content: '<p>Your code is <strong>654321</strong> &amp; ready.</p>',
createTime: '2026-05-07 10:20:00',
},
],
},
});
assert.equal(messages.length, 1);
assert.equal(messages[0].id, 'mail-1');
assert.equal(messages[0].address, 'user@example.com');
assert.equal(messages[0].from.emailAddress.address, 'noreply@tm.openai.com');
assert.match(messages[0].bodyPreview, /654321/);
assert.match(messages[0].bodyPreview, /& ready/);
assert.equal(messages[0].receivedDateTime, '2026-05-07T10:20:00.000Z');
});
test('getCloudMailTokenFromResponse supports direct and nested response shapes', () => {
assert.equal(getCloudMailTokenFromResponse({ token: 'one' }), 'one');
assert.equal(getCloudMailTokenFromResponse({ data: { accessToken: 'two' } }), 'two');
});
+232
View File
@@ -3569,6 +3569,238 @@ test('phone verification helper gives automatic free reuse priority over paid re
assert.equal(currentState.reusablePhoneActivation.activationId, 'paid-reuse');
});
test('phone verification helper hands off manual-only free reuse even when automatic free reuse is enabled', async () => {
const requests = [];
const messages = [];
const stops = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsReuseEnabled: true,
freePhoneReuseEnabled: true,
freePhoneReuseAutoEnabled: true,
verificationResendCount: 0,
currentPhoneActivation: null,
reusablePhoneActivation: {
activationId: 'paid-reuse',
phoneNumber: '66950003333',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 0,
maxUses: 3,
},
freeReusablePhoneActivation: {
phoneNumber: '84943328460',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
countryLabel: 'Thailand',
successfulUses: 1,
maxUses: 3,
source: 'free-manual-reuse',
manualOnly: true,
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
requests.push(parsedUrl);
throw new Error(`HeroSMS should not be called for manual-only free reuse: ${parsedUrl.searchParams.get('action')}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
requestStop: async (payload) => {
stops.push(payload);
},
sendToContentScriptResilient: async (_source, message) => {
messages.push(message);
if (message.type === 'SUBMIT_PHONE_NUMBER') {
assert.equal(message.payload.phoneNumber, '84943328460');
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await assert.rejects(
helpers.completePhoneVerificationFlow(1, {
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
}),
(error) => {
assert.match(error.message, /^PHONE_MANUAL_FREE_REUSE::/);
assert.equal(error.result?.manualFreePhoneReuse, true);
assert.equal(error.result?.phoneNumber, '84943328460');
assert.deepStrictEqual(error.result?.fillResult, {
phoneVerificationPage: true,
url: 'https://auth.openai.com/phone-verification',
});
return true;
}
);
assert.deepStrictEqual(messages.map((message) => message.type), ['SUBMIT_PHONE_NUMBER']);
assert.equal(stops.length, 1);
assert.match(stops[0].logMessage, /开始手动复用手机 84943328460/);
assert.deepStrictEqual(requests, []);
assert.equal(currentState.currentPhoneActivation, null);
assert.equal(currentState.reusablePhoneActivation.activationId, 'paid-reuse');
assert.equal(currentState.freeReusablePhoneActivation.phoneNumber, '84943328460');
});
test('phone verification helper infers manual free reuse country from phone prefix', async () => {
const messages = [];
const stops = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsCountryId: 52,
heroSmsCountryLabel: 'Thailand',
freePhoneReuseEnabled: true,
freePhoneReuseAutoEnabled: true,
verificationResendCount: 0,
currentPhoneActivation: null,
freeReusablePhoneActivation: {
phoneNumber: '84943328460',
provider: 'hero-sms',
serviceCode: 'dr',
successfulUses: 1,
maxUses: 3,
source: 'free-manual-reuse',
manualOnly: true,
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
throw new Error(`HeroSMS should not be called for manual-only free reuse: ${parsedUrl.searchParams.get('action')}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
requestStop: async (payload) => {
stops.push(payload);
},
sendToContentScriptResilient: async (_source, message) => {
messages.push(message);
if (message.type === 'SUBMIT_PHONE_NUMBER') {
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await assert.rejects(
helpers.completePhoneVerificationFlow(1, {
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
}),
(error) => {
assert.match(error.message, /^PHONE_MANUAL_FREE_REUSE::/);
return true;
}
);
assert.equal(stops.length, 1);
assert.deepStrictEqual(messages.map((message) => message.type), ['SUBMIT_PHONE_NUMBER']);
assert.equal(messages[0].payload.phoneNumber, '84943328460');
assert.equal(messages[0].payload.countryId, 10);
assert.equal(messages[0].payload.countryLabel, 'Vietnam');
});
test('phone verification helper infers major HeroSMS countries from phone prefixes', async () => {
const cases = [
{ phoneNumber: '12025550123', countryId: 187, countryLabel: 'USA' },
{ phoneNumber: '447911123456', countryId: 16, countryLabel: 'United Kingdom' },
{ phoneNumber: '819012345678', countryId: 151, countryLabel: 'Japan' },
{ phoneNumber: '4915112345678', countryId: 43, countryLabel: 'Germany' },
{ phoneNumber: '33612345678', countryId: 73, countryLabel: 'France' },
{ phoneNumber: '84943328460', countryId: 10, countryLabel: 'Vietnam' },
{ phoneNumber: '66950003333', countryId: 52, countryLabel: 'Thailand' },
{ phoneNumber: '628111111111', countryId: 6, countryLabel: 'Indonesia' },
];
for (const testCase of cases) {
const messages = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsCountryId: 52,
heroSmsCountryLabel: 'Thailand',
freePhoneReuseEnabled: true,
freePhoneReuseAutoEnabled: true,
verificationResendCount: 0,
currentPhoneActivation: null,
freeReusablePhoneActivation: {
phoneNumber: testCase.phoneNumber,
provider: 'hero-sms',
serviceCode: 'dr',
successfulUses: 1,
maxUses: 3,
source: 'free-manual-reuse',
manualOnly: true,
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
throw new Error(`HeroSMS should not be called for manual-only free reuse: ${parsedUrl.searchParams.get('action')}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
requestStop: async () => {},
sendToContentScriptResilient: async (_source, message) => {
messages.push(message);
if (message.type === 'SUBMIT_PHONE_NUMBER') {
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await assert.rejects(
helpers.completePhoneVerificationFlow(1, {
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
}),
(error) => {
assert.match(error.message, /^PHONE_MANUAL_FREE_REUSE::/);
return true;
}
);
assert.equal(messages.length, 1, testCase.phoneNumber);
assert.equal(messages[0].payload.phoneNumber, testCase.phoneNumber);
assert.equal(messages[0].payload.countryId, testCase.countryId, testCase.phoneNumber);
assert.equal(messages[0].payload.countryLabel, testCase.countryLabel, testCase.phoneNumber);
}
});
test('phone verification helper accepts HeroSMS WAIT_RETRY as free-reuse ready without using its suffix as code', async () => {
const requests = [];
const events = [];
+21
View File
@@ -186,6 +186,7 @@
- Plus 支付方式 `plusPaymentMethod`GoPay 配置 `gopayPhone / gopayPin`GPC helper 配置 `gopayHelperApiUrl / gopayHelperApiKey / gopayHelperPhoneNumber / gopayHelperOtpChannel / gopayHelperLocalSmsHelperEnabled / gopayHelperLocalSmsHelperUrl / gopayHelperPin`;其中 GPC API 地址固定归一为 `https://gpc.qlhazycoder.top`
- PayPal 账号池配置 `paypalAccounts / currentPayPalAccountId`,以及供后台步骤兼容读取的 `paypalEmail / paypalPassword`
- 邮箱 provider 配置
- Cloud Mail / SkyMail API 配置:`cloudMailBaseUrl / cloudMailAdminEmail / cloudMailAdminPassword / cloudMailToken / cloudMailReceiveMailbox / cloudMailDomain / cloudMailDomains`
- Hotmail 账号池
- 2925 账号池
- 2925 是否启用号池模式 `mail2925UseAccountPool`
@@ -388,6 +389,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
- 普通邮箱仍会携带 `filterAfterTimestamp` 做时间窗筛选;`2925` 在 Step 4 / Step 8 会固定使用“步骤开始时间向前回看 10 分钟”的时间窗,不再做“新旧邮件快照差集”比较,而是每次刷新后直接遍历当前列表中落在该固定时间窗内的匹配邮件。
- 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。
- `LuckMail``/code` 接口暂未拿到新验证码时,会先点击一次认证页“重新发送验证码”,再等待 15 秒后继续轮询 `/code`,避免只空等不触发新邮件。
- `Cloud Mail` 既可以作为邮箱生成器直接创建 `local@domain`,也可以作为收件 provider 轮询 `cloudMailReceiveMailbox`;当 `Mail = Cloud Mail` 且邮箱生成器不是 Cloud Mail 时,会优先轮询配置的接收邮箱,避免错误轮询注册邮箱本身。
- 验证码提交重试上限当前为 15 次;页面明确拒绝验证码时,会在上限内继续拉取新验证码并重提。
- `2925` 内容脚本会把每一封实际打开检测的邮件立即删除;同一验证码步骤启动后,试过的验证码会按“步骤 ID + 启动时间”隔离缓存,不会在本次步骤里重复提交;如果再次遇到相同验证码,对应邮件也会在读取后立即删除,避免后续反复打开。
- `2925` 在 provide 模式下仍保持宽松匹配:只要邮件内容命中 ChatGPT / OpenAI 验证码过滤条件,就会尝试该邮件。
@@ -656,9 +658,28 @@ Plus 模式可见步骤:
- Duck
- Cloudflare
- Cloudflare Temp Email
- Cloud Mail
- 自定义邮箱池
- iCloud 隐私邮箱
### 7.1.1 Cloud Mail
组成:
- [cloudmail-utils.js](c:/Users/projectf/Downloads/codex注册扩展/cloudmail-utils.js)
- [background/cloudmail-provider.js](c:/Users/projectf/Downloads/codex注册扩展/background/cloudmail-provider.js)
- [background/verification-flow.js](c:/Users/projectf/Downloads/codex注册扩展/background/verification-flow.js)
- [sidepanel/sidepanel.html](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.html)
- [sidepanel/sidepanel.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.js)
链路:
1. 侧栏展示 Cloud Mail 的 API 地址、管理员邮箱、管理员密码、接收邮箱与生成域名配置。
2. 当 `邮箱生成 = Cloud Mail` 时,后台用管理员账号获取 Token,并通过 `/api/public/addUser` 创建随机本地名前缀邮箱。
3. 当 `Mail = Cloud Mail` 且邮箱生成器不是 Cloud Mail 时,Step 4 / Step 8 优先轮询 `cloudMailReceiveMailbox`,用于接住 Duck / Cloudflare / iCloud 等转发来的验证码。
4. 邮件读取统一走 `/api/public/emailList`,返回数据先经 `cloudmail-utils.js` 归一化,再复用现有验证码匹配与时间窗过滤逻辑。
5. 如果 Token 失效,后台会尝试重新登录刷新 Token 后重试当前 API 请求。
### 7.2 Hotmail
组成:
+7 -3
View File
@@ -22,6 +22,7 @@
- `LICENSE`:项目许可证文件。
- `README.md`:面向使用者的项目介绍、安装说明、能力清单与操作指引。
- `background.js`:扩展后台 Service Worker 入口壳,负责模块装配、初始化、全局常量、少量保留的领域函数与运行入口;当前新增 `oauthFlowTimeoutEnabled` 持久化配置,并统一承接 OAuth 授权后链总预算开关与剩余预算计算。
- `cloudmail-utils.js`Cloud Mail / SkyMail 相关的纯工具函数,负责 API 地址、域名、鉴权头、邮件列表响应与邮件正文归一化。
- `cloudflare-temp-email-utils.js`Cloudflare Temp Email 相关的纯工具函数,负责 URL、域名、邮件内容与 MIME 数据归一化。
- `gopay-utils.js`GoPay / GPC Plus 支付相关纯工具函数,负责支付方式归一化、GoPay 手机号/OTP/PIN 归一化、GPC API 地址归一化、API Key 请求头、任务创建/查询/OTP/PIN/停止 URL 与余额响应解析。
- `hotmail-utils.js`:Hotmail 账号与验证码提取相关的纯工具函数,负责账号筛选、验证码匹配、第三方接口数据归一化。
@@ -47,6 +48,7 @@
- `background/account-run-history.js`:账号记录模块,负责以统一账号标识维护最新记录;邮箱注册以邮箱为主身份,手机号注册以手机号为主身份,同一轮后续绑定的手机号或邮箱会并入同一条记录并覆盖旧占位状态;模块统一归一化成功/失败/停止三态并落地到 `chrome.storage.local`,兼容 phone-only 与 email+phone 组合记录、空密码,并按默认本地 helper 地址自动尝试把完整快照同步到本地 helper;若 helper 未启动,则静默跳过。
- `background/auto-run-controller.js`:自动运行主控制器,封装多轮执行、重试、轮次摘要、线程间隔与倒计时恢复逻辑;当前自动流程会绑定 `autoRunSessionId`,手动停止后旧的倒计时计划、旧重试链路和旧恢复入口不会再复活已失效的自动运行;fresh-attempt reset 时会额外保留 Plus 模式与 PayPal 配置、`gmailBaseEmail``mail2925BaseEmail` 与当前 2925 账号选择,避免自动流程重置后丢失关键持久配置。
- `background/contribution-oauth.js`:贡献模式的公开 OAuth 流程模块,负责调用 `apikey.qzz.io` 的公开贡献接口、保存贡献会话运行态、在主 10 步流程里为步骤 7 提供贡献登录地址、在步骤 9/10 衔接 callback 捕获与兼容提交 `/oauth/api/submit-callback`,并把真实服务端状态映射回 sidepanel 运行态。
- `background/cloudmail-provider.js`Cloud Mail / SkyMail 后台 provider 模块,负责 Token 获取、邮箱创建、邮件列表读取、验证码轮询和生成/转发收件目标邮箱选择。
- `background/generated-email-helpers.js`:生成邮箱辅助层,除了 Duck / Cloudflare / iCloud / Cloudflare Temp Email,也统一承接 Gmail / 2925 的别名邮箱生成入口与自定义邮箱池读取;当 provider 为 2925 且 `mail2925Mode = provide` 时,会先确保当前账号池里已有可用账号,再基于该账号生成别名邮箱;若 `mail2925Mode = receive`,则会回退到普通邮箱生成链路;当生成方式为 iCloud 且 `icloudFetchMode = always_new` 时,会强制跳过未用别名复用并新建别名;当生成方式为 `custom-pool` 时,会按当前目标轮次读取邮箱池中的对应邮箱。
- `background/ip-proxy-core.js`:IP 代理核心模块,负责代理条目解析、账号/接口代理池运行态、PAC 应用与清除、代理鉴权回填、出口探测、失败时的目标站点 fail-close 防漏规则,以及自动运行成功阈值后的代理切换支撑。
- `background/ip-proxy-provider-711proxy.js`711Proxy provider 规则模块,负责从账号串中识别和写回 `region / session / sessTime` 等参数,并在固定账号模式下把侧栏配置转换为最终生效的代理账号。
@@ -59,7 +61,7 @@
- `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;内容脚本恢复等待日志支持 `logStep / logStepKey`,用于保持 Plus 复用步骤的日志标签正确;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置以及 2925 长轮询参数;日志步骤号使用 `completionStep`,因此 Plus 登录验证码会显示可见第 11 步而不是内部复用的 Step 8;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若登录验证码提交后页面转入 `add-phone / 手机号页`,则会把“后续需要手机号验证”的结果继续传给 OAuth 确认步骤,而不是误判为普通失败;若登录验证码提交后通信中断但认证页已进入 OAuth 同意页或手机号验证页,会按真实页面状态继续后续流程;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置、Cloud Mail API 轮询以及 2925 长轮询参数;日志步骤号使用 `completionStep`,因此 Plus 登录验证码会显示可见第 11 步而不是内部复用的 Step 8;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若登录验证码提交后页面转入 `add-phone / 手机号页`,则会把“后续需要手机号验证”的结果继续传给 OAuth 确认步骤,而不是误判为普通失败;若登录验证码提交后通信中断但认证页已进入 OAuth 同意页或手机号验证页,会按真实页面状态继续后续流程;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。
## `background/steps/`
@@ -150,7 +152,7 @@
名基邮箱”;当 provider 为 2925 时,会额外显示 `提供邮箱 / 接收邮箱` 模式切换,并把“2925 号池”从别名基邮箱行拆成独立配置行,避免
receive 模式把账号池开关一起隐藏;当前在 `邮箱生成` 区域新增 `自定义邮箱池` 选项和多行邮箱池输入框,并在 `邮箱服务 = 自定义邮箱`
额外显示 `自定义号池` 文本框;当邮箱服务为 iCloud 时,额外提供目标邮箱类型与转发邮箱 provider 配置,用于选择直接从 iCloud 收件箱收
码或从 QQ / 网易 / Gmail 转发目标邮箱收码;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密
码或从 QQ / 网易 / Gmail 转发目标邮箱收码;当邮箱服务或邮箱生成器为 Cloud Mail 时,额外显示 API 地址、管理员账号、接收邮箱和生成域名配置行;来源下拉框当前支持 `CPA / SUB2API / Codex2API`,其中 Codex2API 额外提供后台地址和管理密
钥配置行;设置卡片新增 `IP代理` 开关与代理配置折叠区,支持 711Proxy 账号密码模式、代理状态卡与出口检测按钮;接码卡内把注册方式、接码配置、接码订单运行态与“注册手机号”身份运行态分层展示,“注册手机号”位于订单运行状态下方,不嵌入当前分配/验证码网格;设置卡片新增 `Plus 模式` 开关与 PayPal 账号下拉框,右侧使用公共表单弹窗添加账号;GPC Plus 配置额外提供只读 API 地址、API Key、专用手机号、OTP 渠道、本地 OTP helper 开关、helper URL 与 PIN,并提供 `购买卡密``转换 API Key` 入口;Hotmail / 2925 两个账号池当前都使用统一的头部“添加账号/取消添加”按钮和共享表单容器;设置卡片还新增“授权总超时”开关,用于控制 Step 7 后链 5 分钟总预算。
- `sidepanel/paypal-manager.js`:侧边栏 PayPal 账号管理器,负责 Plus 模式下的账号下拉框渲染、添加账号弹窗、保存账号与切换当前账号。
- `sidepanel/sidepanel.js`:侧边栏主入口脚本,负责 UI 状态同步、动态步骤渲染、按钮交互、共享验证码自动重发次数配置与广播接收,并装
@@ -158,7 +160,7 @@
动逻辑,而独立 manager 负责贡献运行态展示与轮询,同时把 Gmail / 2925 的基邮箱输入、完整注册邮箱输入、自动生成按钮与兼容性校验统一接
到共享别名逻辑上;当 provider 为 2925 时,会根据 `mail2925Mode` 决定是否启用别名基邮箱链路,而 2925 号池开关与当前账号选择则独立显
示并同时服务于 provide / receive 两种模式;当 provider 为 iCloud 时,会保存并回显目标邮箱类型与转发邮箱 provider,相关 provider 选
项和 label 复用 `mail-provider-utils.js``自定义邮箱池` 模式会按邮箱池长度锁定自动轮数,并把当前输入的邮箱池配置参与自动启动前保
项和 label 复用 `mail-provider-utils.js`Cloud Mail 配置保存、回显和显隐由这里接入,可分别服务于邮箱生成器和转发收件 provider;`自定义邮箱池` 模式会按邮箱池长度锁定自动轮数,并把当前输入的邮箱池配置参与自动启动前保
存;点击“自动”时会先冻结当时的目标轮数,避免启动前的异步刷新、保存或后台旧状态回灌把输入框重置为 1;`邮箱服务 = 自定义邮箱` 时,如果配置了 `customMailProviderPool`,也会按号池长度锁定自动轮数并在 Auto 中按轮次分配注册邮箱,同时
在普通失败时继续复用当前邮箱,只有成功或出现手机号验证时才切换下一个邮箱;Plus 模式开启后,步骤列表会切换为 13 步,并显示 PayPal 账
号下拉框与添加入口;当前选中的 PayPal 账号会同步回兼容字段 `paypalEmail / paypalPassword` 供后台步骤复用;Step 8 的自定义邮箱确认弹
@@ -219,6 +221,8 @@
- `tests/plus-checkout-billing-tab-resolution.test.js`:测试 Plus 第 7 步可直接接管当前 checkout 标签页,并把 PayPal、账单地址、Google 地址推荐和订阅按钮操作路由到对应 Stripe iframe;当前也覆盖 GPC 队列任务轮询、本地 OTP 自动读取、手动 OTP 输入、PIN 提交与任务终止分支。
- `tests/cloudflare-temp-email-provider.test.js`:测试 Cloudflare Temp Email provider 的轮询与目标邮箱选择逻辑。
- `tests/cloudflare-temp-email-utils.test.js`:测试 Cloudflare Temp Email 工具层的 URL、域名、邮件解析逻辑。
- `tests/cloudmail-provider.test.js`:测试 Cloud Mail provider 的验证码轮询和生成/转发收件目标邮箱选择逻辑。
- `tests/cloudmail-utils.test.js`:测试 Cloud Mail 工具层的 URL、域名、鉴权头、Token 与邮件响应归一化逻辑。
- `tests/content-utils.test.js`:测试内容脚本公共工具层对 `mail.126.com` 等网页邮箱来源的识别逻辑。
- `tests/hotmail-api-mode.test.js`:测试 Hotmail API 模式相关文案和集成方式。
- `tests/hotmail-cors-headers.test.js`:测试 Microsoft token 请求相关 DNR 配置。