修复自定义邮箱收码模式默认手动

This commit is contained in:
QLHazyCoder
2026-05-28 06:14:04 +08:00
parent 1960b6e21a
commit 2992cc88ac
18 changed files with 1154 additions and 6 deletions
+159
View File
@@ -682,6 +682,10 @@ const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
const DEFAULT_HOTMAIL_REMOTE_BASE_URL = ''; const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373'; const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373';
const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = DEFAULT_HOTMAIL_LOCAL_BASE_URL; const DEFAULT_ACCOUNT_RUN_HISTORY_HELPER_BASE_URL = DEFAULT_HOTMAIL_LOCAL_BASE_URL;
const CUSTOM_MAIL_RECEIVE_MODE_MANUAL = 'manual';
const CUSTOM_MAIL_RECEIVE_MODE_HELPER = 'helper';
const DEFAULT_CUSTOM_MAIL_RECEIVE_MODE = CUSTOM_MAIL_RECEIVE_MODE_MANUAL;
const DEFAULT_CUSTOM_MAIL_HELPER_BASE_URL = 'http://127.0.0.1:17374';
const HOTMAIL_LOCAL_HELPER_TIMEOUT_MS = 45000; const HOTMAIL_LOCAL_HELPER_TIMEOUT_MS = 45000;
const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai'; const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
const DEFAULT_HERO_SMS_BASE_URL = 'https://hero-sms.com/stubs/handler_api.php'; const DEFAULT_HERO_SMS_BASE_URL = 'https://hero-sms.com/stubs/handler_api.php';
@@ -1396,6 +1400,8 @@ const PERSISTED_SETTING_DEFAULTS = {
mailProvider: '163', mailProvider: '163',
mail2925Mode: DEFAULT_MAIL_2925_MODE, mail2925Mode: DEFAULT_MAIL_2925_MODE,
mail2925UseAccountPool: false, mail2925UseAccountPool: false,
customMailReceiveMode: DEFAULT_CUSTOM_MAIL_RECEIVE_MODE,
customMailHelperBaseUrl: DEFAULT_CUSTOM_MAIL_HELPER_BASE_URL,
emailGenerator: 'duck', emailGenerator: 'duck',
customMailProviderPool: [], customMailProviderPool: [],
customEmailPool: [], customEmailPool: [],
@@ -1500,6 +1506,8 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([
'hostedCheckoutPhoneNumber', 'hostedCheckoutPhoneNumber',
'plusHostedCheckoutOauthDelaySeconds', 'plusHostedCheckoutOauthDelaySeconds',
'mailProvider', 'mailProvider',
'customMailReceiveMode',
'customMailHelperBaseUrl',
'ipProxyEnabled', 'ipProxyEnabled',
'ipProxyService', 'ipProxyService',
'ipProxyMode', 'ipProxyMode',
@@ -2798,6 +2806,35 @@ function normalizeMail2925Mode(value = '') {
: DEFAULT_MAIL_2925_MODE; : DEFAULT_MAIL_2925_MODE;
} }
function normalizeCustomMailReceiveMode(value = '') {
return String(value || '').trim().toLowerCase() === CUSTOM_MAIL_RECEIVE_MODE_HELPER
? CUSTOM_MAIL_RECEIVE_MODE_HELPER
: DEFAULT_CUSTOM_MAIL_RECEIVE_MODE;
}
function normalizeCustomMailHelperBaseUrl(value = '') {
const trimmed = String(value || '').trim();
const candidate = trimmed || DEFAULT_CUSTOM_MAIL_HELPER_BASE_URL;
try {
const parsed = new URL(candidate);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return DEFAULT_CUSTOM_MAIL_HELPER_BASE_URL;
}
parsed.hash = '';
parsed.search = '';
parsed.pathname = parsed.pathname.replace(/\/+$/, '');
const path = parsed.pathname === '/' ? '' : parsed.pathname;
return `${parsed.origin}${path}` || DEFAULT_CUSTOM_MAIL_HELPER_BASE_URL;
} catch {
return DEFAULT_CUSTOM_MAIL_HELPER_BASE_URL;
}
}
function shouldUseCustomMailHelper(state = {}) {
return isCustomMailProvider(state)
&& normalizeCustomMailReceiveMode(state?.customMailReceiveMode) === CUSTOM_MAIL_RECEIVE_MODE_HELPER;
}
function normalizeCloudflareTempEmailLookupMode(value = '') { function normalizeCloudflareTempEmailLookupMode(value = '') {
return String(value || '').trim().toLowerCase() === CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE_REGISTRATION_EMAIL return String(value || '').trim().toLowerCase() === CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE_REGISTRATION_EMAIL
? CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE_REGISTRATION_EMAIL ? CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE_REGISTRATION_EMAIL
@@ -3406,6 +3443,10 @@ function normalizePersistentSettingValue(key, value) {
return normalizeMail2925Mode(value); return normalizeMail2925Mode(value);
case 'mail2925UseAccountPool': case 'mail2925UseAccountPool':
return Boolean(value); return Boolean(value);
case 'customMailReceiveMode':
return normalizeCustomMailReceiveMode(value);
case 'customMailHelperBaseUrl':
return normalizeCustomMailHelperBaseUrl(value);
case 'emailGenerator': case 'emailGenerator':
return normalizeEmailGenerator(value); return normalizeEmailGenerator(value);
case 'customMailProviderPool': case 'customMailProviderPool':
@@ -3819,6 +3860,8 @@ function buildSettingsStatePatchFromFlatUpdates(updates = {}) {
assignIfUpdated('plusPaymentMethod', ['flows', 'openai', 'plus', 'plusPaymentMethod']); assignIfUpdated('plusPaymentMethod', ['flows', 'openai', 'plus', 'plusPaymentMethod']);
assignIfUpdated('plusAccountAccessStrategy', ['flows', 'openai', 'plus', 'plusAccountAccessStrategy']); assignIfUpdated('plusAccountAccessStrategy', ['flows', 'openai', 'plus', 'plusAccountAccessStrategy']);
assignIfUpdated('mailProvider', ['services', 'email', 'provider']); assignIfUpdated('mailProvider', ['services', 'email', 'provider']);
assignIfUpdated('customMailReceiveMode', ['services', 'email', 'customReceiveMode']);
assignIfUpdated('customMailHelperBaseUrl', ['services', 'email', 'customHelperBaseUrl']);
assignIfUpdated('ipProxyEnabled', ['services', 'proxy', 'enabled']); assignIfUpdated('ipProxyEnabled', ['services', 'proxy', 'enabled']);
assignIfUpdated('ipProxyService', ['services', 'proxy', 'provider']); assignIfUpdated('ipProxyService', ['services', 'proxy', 'provider']);
assignIfUpdated('ipProxyMode', ['services', 'proxy', 'mode']); assignIfUpdated('ipProxyMode', ['services', 'proxy', 'mode']);
@@ -5306,6 +5349,115 @@ function buildHotmailLocalEndpoint(baseUrl, path) {
return new URL(path, `${normalizedBaseUrl}/`).toString(); return new URL(path, `${normalizedBaseUrl}/`).toString();
} }
function buildCustomMailLocalEndpoint(baseUrl, path) {
const normalizedBaseUrl = normalizeCustomMailHelperBaseUrl(baseUrl);
return new URL(path, `${normalizedBaseUrl}/`).toString();
}
function getCustomMailHelperBaseUrlForState(state = {}) {
return normalizeCustomMailHelperBaseUrl(state?.customMailHelperBaseUrl);
}
async function requestCustomMailLocalCode(state = {}, pollPayload = {}) {
const requestTimeoutMs = HOTMAIL_LOCAL_HELPER_TIMEOUT_MS;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), requestTimeoutMs);
let response;
try {
response = await fetch(buildCustomMailLocalEndpoint(getCustomMailHelperBaseUrlForState(state), '/code'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
top: pollPayload.top || 20,
targetEmail: pollPayload.targetEmail || '',
senderFilters: pollPayload.senderFilters || [],
subjectFilters: pollPayload.subjectFilters || [],
requiredKeywords: pollPayload.requiredKeywords || [],
codePatterns: pollPayload.codePatterns || [],
excludeCodes: pollPayload.excludeCodes || [],
filterAfterTimestamp: Number(pollPayload.filterAfterTimestamp || 0) || 0,
}),
signal: controller.signal,
});
} catch (err) {
if (err?.name === 'AbortError') {
throw new Error(`自定义邮箱本地助手请求超时(>${Math.round(requestTimeoutMs / 1000)} 秒)`);
}
throw new Error(`自定义邮箱本地助手请求失败:${err.message}`);
} finally {
clearTimeout(timeoutId);
}
const text = await response.text();
let payload = {};
try {
payload = text ? JSON.parse(text) : {};
} catch {
payload = { raw: text };
}
if (!response.ok || payload?.ok === false) {
const errorText = payload?.error || payload?.message || text || `HTTP ${response.status}`;
throw new Error(`自定义邮箱本地助手返回失败:${errorText}`);
}
return {
code: String(payload?.code || '').trim(),
message: payload?.message || null,
usedTimeFallback: Boolean(payload?.usedTimeFallback),
};
}
async function pollCustomMailVerificationCode(step, state, pollPayload = {}) {
if (!shouldUseCustomMailHelper(state)) {
throw new Error(`步骤 ${step}:自定义邮箱当前为手动确认模式,未启用本地助手自动收码。`);
}
const maxAttempts = Math.max(1, Math.floor(Number(pollPayload.maxAttempts) || 5));
const intervalMs = Math.max(1000, Number(pollPayload.intervalMs) || 3000);
let lastError = null;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
throwIfStopped();
try {
await addLog(`步骤 ${step}:正在通过自定义邮箱本地助手轮询验证码(${attempt}/${maxAttempts}...`, 'info');
const fetchResult = await requestCustomMailLocalCode(state, {
...pollPayload,
targetEmail: pollPayload.targetEmail || state?.email || '',
});
if (fetchResult.code) {
if (fetchResult.usedTimeFallback) {
await addLog(`步骤 ${step}:自定义邮箱本地助手使用时间回退后命中验证码。`, 'warn');
}
await addLog(`步骤 ${step}:已通过自定义邮箱本地助手找到验证码:${fetchResult.code}`, 'ok');
return {
ok: true,
code: fetchResult.code,
emailTimestamp: Number(fetchResult.message?.receivedTimestamp) || Date.now(),
mailId: fetchResult.message?.id || '',
};
}
lastError = new Error(`步骤 ${step}:自定义邮箱本地助手暂未返回匹配验证码(${attempt}/${maxAttempts})。`);
await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info');
} catch (err) {
lastError = err;
await addLog(`步骤 ${step}:自定义邮箱本地助手轮询失败:${err.message}`, 'warn');
}
if (attempt < maxAttempts) {
await sleepWithStop(intervalMs);
}
}
throw lastError || new Error(`步骤 ${step}:自定义邮箱本地助手未返回新的匹配验证码。`);
}
async function requestHotmailRemoteMailbox(account, mailbox = 'INBOX') { async function requestHotmailRemoteMailbox(account, mailbox = 'INBOX') {
if (!account?.email) { if (!account?.email) {
throw new Error('Hotmail 账号缺少邮箱地址。'); throw new Error('Hotmail 账号缺少邮箱地址。');
@@ -13461,6 +13613,7 @@ const flowMailPollingService = self.MultiPageBackgroundFlowMailPolling?.createFl
chrome, chrome,
CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER, CLOUD_MAIL_PROVIDER,
CUSTOM_MAIL_PROVIDER: 'custom',
ensureIcloudMailSession: ensureIcloudMailSessionForVerification, ensureIcloudMailSession: ensureIcloudMailSessionForVerification,
ensureMail2925MailboxSession, ensureMail2925MailboxSession,
getMailConfig, getMailConfig,
@@ -13473,11 +13626,13 @@ const flowMailPollingService = self.MultiPageBackgroundFlowMailPolling?.createFl
LUCKMAIL_PROVIDER, LUCKMAIL_PROVIDER,
pollCloudflareTempEmailVerificationCode, pollCloudflareTempEmailVerificationCode,
pollCloudMailVerificationCode, pollCloudMailVerificationCode,
pollCustomMailVerificationCode,
pollHotmailVerificationCode, pollHotmailVerificationCode,
pollLuckmailVerificationCode, pollLuckmailVerificationCode,
pollYydsMailVerificationCode, pollYydsMailVerificationCode,
reuseOrCreateTab, reuseOrCreateTab,
sendToMailContentScriptResilient, sendToMailContentScriptResilient,
shouldUseCustomMailHelper,
throwIfStopped, throwIfStopped,
YYDS_MAIL_PROVIDER, YYDS_MAIL_PROVIDER,
}); });
@@ -13488,6 +13643,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create
closeConflictingTabsForSource, closeConflictingTabsForSource,
CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER, CLOUD_MAIL_PROVIDER,
CUSTOM_MAIL_PROVIDER: 'custom',
completeNodeFromBackground, completeNodeFromBackground,
confirmCustomVerificationStepBypassRequest: (step) => chrome.runtime.sendMessage({ confirmCustomVerificationStepBypassRequest: (step) => chrome.runtime.sendMessage({
type: 'REQUEST_CUSTOM_VERIFICATION_BYPASS_CONFIRMATION', type: 'REQUEST_CUSTOM_VERIFICATION_BYPASS_CONFIRMATION',
@@ -13509,6 +13665,7 @@ const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.create
MAIL_2925_VERIFICATION_MAX_ATTEMPTS, MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
pollCloudflareTempEmailVerificationCode, pollCloudflareTempEmailVerificationCode,
pollCloudMailVerificationCode, pollCloudMailVerificationCode,
pollCustomMailVerificationCode,
pollHotmailVerificationCode, pollHotmailVerificationCode,
pollLuckmailVerificationCode, pollLuckmailVerificationCode,
pollYydsMailVerificationCode, pollYydsMailVerificationCode,
@@ -13646,6 +13803,7 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({
sendToContentScriptResilient, sendToContentScriptResilient,
isRetryableContentScriptTransportError, isRetryableContentScriptTransportError,
shouldUseCustomRegistrationEmail, shouldUseCustomRegistrationEmail,
shouldUseCustomMailHelper,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
throwIfStopped, throwIfStopped,
waitForTabStableComplete, waitForTabStableComplete,
@@ -13716,6 +13874,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
sendToContentScriptResilient, sendToContentScriptResilient,
setState, setState,
shouldUseCustomRegistrationEmail, shouldUseCustomRegistrationEmail,
shouldUseCustomMailHelper,
sleepWithStop, sleepWithStop,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS, STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
+22
View File
@@ -69,6 +69,7 @@
chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null), chrome = (typeof globalThis !== 'undefined' ? globalThis.chrome : null),
CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email', CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email',
CLOUD_MAIL_PROVIDER = 'cloudmail', CLOUD_MAIL_PROVIDER = 'cloudmail',
CUSTOM_MAIL_PROVIDER = 'custom',
ensureIcloudMailSession = null, ensureIcloudMailSession = null,
ensureMail2925MailboxSession = null, ensureMail2925MailboxSession = null,
getMailConfig = null, getMailConfig = null,
@@ -81,11 +82,13 @@
LUCKMAIL_PROVIDER = 'luckmail-api', LUCKMAIL_PROVIDER = 'luckmail-api',
pollCloudflareTempEmailVerificationCode = null, pollCloudflareTempEmailVerificationCode = null,
pollCloudMailVerificationCode = null, pollCloudMailVerificationCode = null,
pollCustomMailVerificationCode = null,
pollHotmailVerificationCode = null, pollHotmailVerificationCode = null,
pollLuckmailVerificationCode = null, pollLuckmailVerificationCode = null,
pollYydsMailVerificationCode = null, pollYydsMailVerificationCode = null,
reuseOrCreateTab = async () => null, reuseOrCreateTab = async () => null,
sendToMailContentScriptResilient = null, sendToMailContentScriptResilient = null,
shouldUseCustomMailHelper = null,
throwIfStopped = () => {}, throwIfStopped = () => {},
YYDS_MAIL_PROVIDER = 'yyds-mail', YYDS_MAIL_PROVIDER = 'yyds-mail',
} = deps; } = deps;
@@ -107,6 +110,10 @@
label: 'Cloud Mail', label: 'Cloud Mail',
poll: pollCloudMailVerificationCode, poll: pollCloudMailVerificationCode,
}], }],
[normalizeProviderId(CUSTOM_MAIL_PROVIDER), {
label: '自定义邮箱本地助手',
poll: pollCustomMailVerificationCode,
}],
[normalizeProviderId(YYDS_MAIL_PROVIDER), { [normalizeProviderId(YYDS_MAIL_PROVIDER), {
label: 'YYDS Mail', label: 'YYDS Mail',
poll: pollYydsMailVerificationCode, poll: pollYydsMailVerificationCode,
@@ -167,11 +174,26 @@
return normalizeProviderId(mail?.provider) === '2925'; return normalizeProviderId(mail?.provider) === '2925';
} }
function isCustomMailProvider(mail = {}) {
return normalizeProviderId(mail?.provider) === normalizeProviderId(CUSTOM_MAIL_PROVIDER);
}
function canUseCustomMailHelper(state = {}) {
if (typeof shouldUseCustomMailHelper === 'function') {
return Boolean(shouldUseCustomMailHelper(state));
}
return normalizeProviderId(state?.mailProvider) === normalizeProviderId(CUSTOM_MAIL_PROVIDER)
&& normalizeProviderId(state?.customMailReceiveMode) === 'helper';
}
async function pollThroughApiProvider(providerId, step, state, pollPayload, mail, options) { async function pollThroughApiProvider(providerId, step, state, pollPayload, mail, options) {
const handler = apiProviderHandlers.get(providerId); const handler = apiProviderHandlers.get(providerId);
if (!handler) { if (!handler) {
return null; return null;
} }
if (isCustomMailProvider(mail) && !canUseCustomMailHelper(state)) {
throw new Error('自定义邮箱当前为手动确认模式,未启用本地 helper 自动收码。');
}
if (typeof handler.poll !== 'function') { if (typeof handler.poll !== 'function') {
throw new Error(`${handler.label} 邮箱轮询能力未接入,无法继续执行。`); throw new Error(`${handler.label} 邮箱轮询能力未接入,无法继续执行。`);
} }
+9
View File
@@ -12,6 +12,7 @@
closeConflictingTabsForSource, closeConflictingTabsForSource,
CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER = 'cloudmail', CLOUD_MAIL_PROVIDER = 'cloudmail',
CUSTOM_MAIL_PROVIDER = 'custom',
completeNodeFromBackground, completeNodeFromBackground,
confirmCustomVerificationStepBypassRequest, confirmCustomVerificationStepBypassRequest,
getNodeIdByStepForState, getNodeIdByStepForState,
@@ -29,6 +30,7 @@
MAIL_2925_VERIFICATION_MAX_ATTEMPTS, MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
pollCloudflareTempEmailVerificationCode, pollCloudflareTempEmailVerificationCode,
pollCloudMailVerificationCode, pollCloudMailVerificationCode,
pollCustomMailVerificationCode,
pollHotmailVerificationCode, pollHotmailVerificationCode,
pollLuckmailVerificationCode, pollLuckmailVerificationCode,
pollYydsMailVerificationCode, pollYydsMailVerificationCode,
@@ -987,6 +989,13 @@
}, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`); }, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
return pollCloudMailVerificationCode(step, state, timedPoll.payload); return pollCloudMailVerificationCode(step, state, timedPoll.payload);
} }
if (mail.provider === CUSTOM_MAIL_PROVIDER && typeof pollCustomMailVerificationCode === 'function') {
const timedPoll = await applyMailPollingTimeBudget(step, {
...getVerificationPollPayload(step, state),
...cleanPollOverrides,
}, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
return pollCustomMailVerificationCode(step, state, timedPoll.payload);
}
if (mail.provider === YYDS_MAIL_PROVIDER) { if (mail.provider === YYDS_MAIL_PROVIDER) {
const timedPoll = await applyMailPollingTimeBudget(step, { const timedPoll = await applyMailPollingTimeBudget(step, {
...getVerificationPollPayload(step, state), ...getVerificationPollPayload(step, state),
+33
View File
@@ -88,6 +88,25 @@
return 'oauth'; return 'oauth';
}; };
const normalizeCustomMailHelperBaseUrl = (value = '') => {
const fallback = 'http://127.0.0.1:17374';
const trimmed = String(value || '').trim();
const candidate = trimmed || fallback;
try {
const parsed = new URL(candidate);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return fallback;
}
parsed.hash = '';
parsed.search = '';
parsed.pathname = parsed.pathname.replace(/\/+$/, '');
const path = parsed.pathname === '/' ? '' : parsed.pathname;
return `${parsed.origin}${path}` || fallback;
} catch {
return fallback;
}
};
function getCanonicalFlowIds() { function getCanonicalFlowIds() {
const ids = Array.isArray(getRegisteredFlowIds()) const ids = Array.isArray(getRegisteredFlowIds())
? getRegisteredFlowIds() ? getRegisteredFlowIds()
@@ -184,6 +203,8 @@
}, },
email: { email: {
provider: '163', provider: '163',
customReceiveMode: 'manual',
customHelperBaseUrl: 'http://127.0.0.1:17374',
}, },
proxy: { proxy: {
enabled: false, enabled: false,
@@ -477,6 +498,16 @@
?? input?.mailProvider ?? input?.mailProvider
?? defaults.services.email.provider ?? defaults.services.email.provider
).trim() || defaults.services.email.provider, ).trim() || defaults.services.email.provider,
customReceiveMode: String(
nested?.services?.email?.customReceiveMode
?? input?.customMailReceiveMode
?? defaults.services.email.customReceiveMode
).trim().toLowerCase() === 'helper' ? 'helper' : defaults.services.email.customReceiveMode,
customHelperBaseUrl: normalizeCustomMailHelperBaseUrl(
nested?.services?.email?.customHelperBaseUrl
?? input?.customMailHelperBaseUrl
?? defaults.services.email.customHelperBaseUrl
),
}, },
proxy: { proxy: {
enabled: Boolean( enabled: Boolean(
@@ -608,6 +639,8 @@
next.hostedCheckoutPhoneNumber = openaiState.plus?.hostedCheckoutPhoneNumber || ''; next.hostedCheckoutPhoneNumber = openaiState.plus?.hostedCheckoutPhoneNumber || '';
next.plusHostedCheckoutOauthDelaySeconds = openaiState.plus?.plusHostedCheckoutOauthDelaySeconds ?? 3; next.plusHostedCheckoutOauthDelaySeconds = openaiState.plus?.plusHostedCheckoutOauthDelaySeconds ?? 3;
next.mailProvider = normalizedState.services.email.provider; next.mailProvider = normalizedState.services.email.provider;
next.customMailReceiveMode = normalizedState.services.email.customReceiveMode;
next.customMailHelperBaseUrl = normalizedState.services.email.customHelperBaseUrl;
next.ipProxyEnabled = normalizedState.services.proxy.enabled; next.ipProxyEnabled = normalizedState.services.proxy.enabled;
next.ipProxyService = normalizedState.services.proxy.provider; next.ipProxyService = normalizedState.services.proxy.provider;
next.ipProxyMode = normalizedState.services.proxy.mode; next.ipProxyMode = normalizedState.services.proxy.mode;
@@ -32,6 +32,7 @@
phoneVerificationHelpers = null, phoneVerificationHelpers = null,
setState, setState,
shouldUseCustomRegistrationEmail, shouldUseCustomRegistrationEmail,
shouldUseCustomMailHelper = () => false,
sleepWithStop, sleepWithStop,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS, STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
@@ -594,7 +595,7 @@
await addLog(`步骤 ${visibleStep}:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info'); await addLog(`步骤 ${visibleStep}:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info');
} }
if (shouldUseCustomRegistrationEmail(preparedState)) { if (shouldUseCustomRegistrationEmail(preparedState) && !shouldUseCustomMailHelper(preparedState)) {
await confirmCustomVerificationStepBypass(8, { await confirmCustomVerificationStepBypass(8, {
completionStep: visibleStep, completionStep: visibleStep,
promptStep: visibleStep, promptStep: visibleStep,
@@ -617,6 +618,7 @@
|| mail.provider === LUCKMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
|| mail.provider === CLOUD_MAIL_PROVIDER || mail.provider === CLOUD_MAIL_PROVIDER
|| shouldUseCustomMailHelper(preparedState)
) { ) {
await addLog(`步骤 ${visibleStep}:正在通过 ${mail.label} 轮询验证码...`); await addLog(`步骤 ${visibleStep}:正在通过 ${mail.label} 轮询验证码...`);
} else { } else {
@@ -26,6 +26,7 @@
sendToContentScriptResilient, sendToContentScriptResilient,
isRetryableContentScriptTransportError = () => false, isRetryableContentScriptTransportError = () => false,
shouldUseCustomRegistrationEmail, shouldUseCustomRegistrationEmail,
shouldUseCustomMailHelper = () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
throwIfStopped, throwIfStopped,
waitForTabStableComplete = null, waitForTabStableComplete = null,
@@ -93,14 +94,14 @@
} }
async function executeSignupEmailVerificationStep(state, stepStartedAt, verificationSessionKey) { async function executeSignupEmailVerificationStep(state, stepStartedAt, verificationSessionKey) {
if (shouldUseCustomRegistrationEmail(state)) { const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
if (shouldUseCustomRegistrationEmail(state) && !shouldUseCustomMailHelper(state)) {
await confirmCustomVerificationStepBypass(4); await confirmCustomVerificationStepBypass(4);
return; return;
} }
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const verificationFilterAfterTimestamp = mail.provider === '2925' const verificationFilterAfterTimestamp = mail.provider === '2925'
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS) ? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: stepStartedAt; : stepStartedAt;
@@ -120,6 +121,7 @@
|| mail.provider === LUCKMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
|| mail.provider === CLOUD_MAIL_PROVIDER || mail.provider === CLOUD_MAIL_PROVIDER
|| shouldUseCustomMailHelper(state)
) { ) {
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
} else if (mail.provider === '2925') { } else if (mail.provider === '2925') {
@@ -146,7 +148,7 @@
LUCKMAIL_PROVIDER, LUCKMAIL_PROVIDER,
CLOUDFLARE_TEMP_EMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER, CLOUD_MAIL_PROVIDER,
].includes(mail.provider); ].includes(mail.provider) && !shouldUseCustomMailHelper(state);
const signupProfile = buildSignupProfileForVerificationStep(); const signupProfile = buildSignupProfileForVerificationStep();
await resolveVerificationStep(4, state, mail, { await resolveVerificationStep(4, state, mail, {
+428
View File
@@ -0,0 +1,428 @@
import email
import html
import imaplib
import json
import os
import re
import secrets
import sqlite3
import ssl
import string
import traceback
from datetime import datetime, timezone
from email.header import decode_header
from email.utils import getaddresses, parseaddr, parsedate_to_datetime
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
def load_dotenv_file(path):
if not os.path.exists(path):
return
with open(path, "r", encoding="utf-8") as env_file:
for raw_line in env_file:
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
if not key or key in os.environ:
continue
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
value = value[1:-1]
os.environ[key] = value
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
load_dotenv_file(os.path.join(PROJECT_ROOT, ".env"))
HOST = "127.0.0.1"
PORT = int(os.environ.get("FLOWPILOT_CUSTOM_MAIL_HELPER_PORT", "17374"))
IMAP_HOST = os.environ.get("FLOWPILOT_CUSTOM_IMAP_HOST", "imap.mxhichina.com")
IMAP_PORT = int(os.environ.get("FLOWPILOT_CUSTOM_IMAP_PORT", "993"))
IMAP_USER = os.environ.get("FLOWPILOT_CUSTOM_IMAP_USER", "")
IMAP_PASS = os.environ.get("FLOWPILOT_CUSTOM_IMAP_PASS", "")
IMAP_MAILBOX = os.environ.get("FLOWPILOT_CUSTOM_IMAP_MAILBOX", "INBOX")
REQUEST_TIMEOUT_SECONDS = int(os.environ.get("FLOWPILOT_CUSTOM_IMAP_TIMEOUT", "45"))
DEFAULT_TOP = 20
RANDOM_EMAIL_DB_PATH = os.environ.get(
"FLOWPILOT_RANDOM_EMAIL_DB_PATH",
os.path.join(PROJECT_ROOT, "data", "custom-mail-helper.sqlite3"),
)
RANDOM_EMAIL_MAX_COUNT = int(os.environ.get("FLOWPILOT_RANDOM_EMAIL_MAX_COUNT", "20"))
PUBLIC_ENV_KEYS = ["FLOWPILOT_SUB2API_REDIRECT_URI"]
DEFAULT_MAIL_FROM_ALLOW = [
"no-reply@codeium.com",
"noreply@codeium.com",
"no-reply@windsurf.com",
"noreply@windsurf.com",
"noreply@tm.openai.com",
"noreply@tm1.openai.com",
]
def json_response(handler, status, payload):
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
handler.send_response(status)
handler.send_header("Content-Type", "application/json; charset=utf-8")
handler.send_header("Content-Length", str(len(body)))
handler.send_header("Access-Control-Allow-Origin", "*")
handler.send_header("Access-Control-Allow-Headers", "Content-Type")
handler.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
handler.end_headers()
handler.wfile.write(body)
def read_json_payload(handler):
length = int(handler.headers.get("Content-Length", "0") or 0)
raw = handler.rfile.read(length) if length > 0 else b"{}"
try:
return json.loads(raw.decode("utf-8"))
except Exception as exc:
raise RuntimeError(f"Invalid JSON payload: {exc}") from exc
def get_public_env_payload():
return {key: os.environ.get(key, "") for key in PUBLIC_ENV_KEYS}
def decode_mime_header(value):
if not value:
return ""
parts = []
for chunk, charset in decode_header(value):
if isinstance(chunk, bytes):
parts.append(chunk.decode(charset or "utf-8", errors="ignore"))
else:
parts.append(str(chunk))
return "".join(parts).strip()
def extract_text_part(message):
if message.is_multipart():
html_text = ""
for part in message.walk():
if part.get_content_maintype() == "multipart":
continue
if "attachment" in str(part.get("Content-Disposition") or "").lower():
continue
payload = part.get_payload(decode=True) or b""
charset = part.get_content_charset() or "utf-8"
text = payload.decode(charset, errors="ignore").strip()
if part.get_content_type() == "text/plain" and text:
return text
if part.get_content_type() == "text/html" and text and not html_text:
html_text = re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", html.unescape(text))).strip()
return html_text
payload = message.get_payload(decode=True) or b""
charset = message.get_content_charset() or "utf-8"
text = payload.decode(charset, errors="ignore").strip()
if message.get_content_type() == "text/html":
return re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", html.unescape(text))).strip()
return text
def to_timestamp_ms(raw_date):
if not raw_date:
return 0
try:
parsed = parsedate_to_datetime(raw_date)
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return int(parsed.timestamp() * 1000)
except Exception:
return 0
def to_iso_string(timestamp_ms):
if not timestamp_ms:
return ""
return datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc).isoformat().replace("+00:00", "Z")
def parse_addresses(value):
return [addr.strip().lower() for _, addr in getaddresses([str(value or "")]) if addr.strip()]
def normalize_domain(value):
domain = str(value or "").strip().lower()
if domain.startswith("@"):
domain = domain[1:]
if not re.fullmatch(r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+", domain):
raise RuntimeError("Invalid domain")
return domain
def parse_generation_count(value):
if value in (None, ""):
return 1
try:
count = int(value)
except Exception as exc:
raise RuntimeError("n must be an integer") from exc
if count < 1:
raise RuntimeError("n must be >= 1")
if count > RANDOM_EMAIL_MAX_COUNT:
raise RuntimeError(f"n must be <= {RANDOM_EMAIL_MAX_COUNT}")
return count
def ensure_random_email_db():
db_dir = os.path.dirname(RANDOM_EMAIL_DB_PATH)
if db_dir:
os.makedirs(db_dir, exist_ok=True)
connection = sqlite3.connect(RANDOM_EMAIL_DB_PATH)
try:
connection.execute("""
CREATE TABLE IF NOT EXISTS generated_emails (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
prefix TEXT NOT NULL,
domain TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)
""")
connection.execute("CREATE INDEX IF NOT EXISTS idx_generated_emails_domain ON generated_emails(domain)")
connection.commit()
return connection
except Exception:
connection.close()
raise
def random_email_prefix():
length = secrets.randbelow(5) + 8
return "".join(secrets.choice(string.ascii_lowercase) for _ in range(length))
def generate_random_emails(payload):
domain = normalize_domain((payload or {}).get("domain"))
count = parse_generation_count((payload or {}).get("n"))
emails = []
connection = ensure_random_email_db()
try:
attempts = 0
max_attempts = max(100, count * 20)
while len(emails) < count and attempts < max_attempts:
attempts += 1
prefix = random_email_prefix()
email_address = f"{prefix}@{domain}"
try:
connection.execute(
"INSERT INTO generated_emails(email, prefix, domain) VALUES (?, ?, ?)",
(email_address, prefix, domain),
)
emails.append(email_address)
except sqlite3.IntegrityError:
continue
if len(emails) != count:
connection.rollback()
raise RuntimeError("Unable to generate enough unique emails")
connection.commit()
return {"email": emails[0] if emails else "", "emails": emails, "domain": domain, "count": len(emails)}
finally:
connection.close()
def normalize_message(message_id, raw_bytes):
parsed = email.message_from_bytes(raw_bytes)
sender_name, sender_addr = parseaddr(parsed.get("From", ""))
subject = decode_mime_header(parsed.get("Subject", ""))
body = extract_text_part(parsed)
timestamp_ms = to_timestamp_ms(parsed.get("Date"))
return {
"id": str(message_id),
"mailbox": IMAP_MAILBOX,
"subject": subject,
"from": {
"emailAddress": {
"address": sender_addr.strip().lower(),
"name": sender_name.strip(),
}
},
"to": parse_addresses(parsed.get("To", "")),
"cc": parse_addresses(parsed.get("Cc", "")),
"deliveredTo": parse_addresses(parsed.get("Delivered-To", "")),
"bodyPreview": body[:500],
"body": {"content": body},
"receivedDateTime": to_iso_string(timestamp_ms),
"receivedTimestamp": timestamp_ms,
}
def fetch_recent_messages(top=DEFAULT_TOP):
if not IMAP_USER or not IMAP_PASS:
raise RuntimeError("Missing FLOWPILOT_CUSTOM_IMAP_USER/FLOWPILOT_CUSTOM_IMAP_PASS")
context = ssl.create_default_context()
client = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT, ssl_context=context, timeout=REQUEST_TIMEOUT_SECONDS)
try:
client.login(IMAP_USER, IMAP_PASS)
status, _ = client.select(IMAP_MAILBOX)
if status != "OK":
raise RuntimeError(f"Mailbox not found: {IMAP_MAILBOX}")
status, data = client.search(None, "ALL")
if status != "OK" or not data or not data[0]:
return []
message_ids = data[0].split()
selected_ids = list(reversed(message_ids[-max(1, min(int(top or DEFAULT_TOP), 50)):]))
messages = []
for message_id in selected_ids:
fetch_status, fetch_data = client.fetch(message_id, "(RFC822)")
if fetch_status != "OK" or not fetch_data:
continue
raw_bytes = b""
for item in fetch_data:
if isinstance(item, tuple) and len(item) >= 2:
raw_bytes = item[1]
break
if raw_bytes:
messages.append(normalize_message(message_id.decode("utf-8", errors="ignore"), raw_bytes))
messages.sort(key=lambda item: int(item.get("receivedTimestamp") or 0), reverse=True)
return messages
finally:
try:
client.logout()
except Exception:
pass
def extract_code(text, code_patterns=None):
source = str(text or "")
for pattern in code_patterns or []:
try:
source_pattern = str((pattern or {}).get("source") or "").strip()
if not source_pattern:
continue
flags = str((pattern or {}).get("flags") or "").lower()
re_flags = 0
if "i" in flags:
re_flags |= re.IGNORECASE
if "m" in flags:
re_flags |= re.MULTILINE
if "s" in flags:
re_flags |= re.DOTALL
match = re.search(source_pattern, source, flags=re_flags)
if match:
groups = [str(match.group(i) or "").strip() for i in range(1, (match.lastindex or 0) + 1)]
return next((item for item in groups if item), str(match.group(0) or "").strip())
except re.error:
continue
for pattern in [
r"(?:代码为|验证码[^0-9]*?)[\s:]*(\d{6})",
r"(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})",
r"code(?:\s+is|[\s:])+(\d{6})",
r"\b(\d{6})\b",
]:
match = re.search(pattern, source, flags=re.IGNORECASE)
if match:
return match.group(1)
return ""
def message_matches_target(message, target_email):
target = str(target_email or "").strip().lower()
if not target:
return True
recipients = set(message.get("to") or []) | set(message.get("cc") or []) | set(message.get("deliveredTo") or [])
return target in recipients
def select_latest_code(messages, payload):
target_email = str(payload.get("targetEmail") or "").strip().lower()
filter_after_timestamp = int(payload.get("filterAfterTimestamp") or 0)
excluded = {str(item).strip() for item in payload.get("excludeCodes") or [] if str(item).strip()}
sender_filters = [str(item).strip().lower() for item in payload.get("senderFilters") or [] if str(item).strip()]
if not sender_filters:
sender_filters = DEFAULT_MAIL_FROM_ALLOW
subject_filters = [str(item).strip().lower() for item in payload.get("subjectFilters") or [] if str(item).strip()]
required_keywords = [str(item).strip().lower() for item in payload.get("requiredKeywords") or [] if str(item).strip()]
def candidate(message, apply_time_filter):
timestamp = int(message.get("receivedTimestamp") or 0)
if apply_time_filter and filter_after_timestamp and timestamp and timestamp < filter_after_timestamp:
return None
if not message_matches_target(message, target_email):
return None
sender = str(message.get("from", {}).get("emailAddress", {}).get("address", "")).lower()
subject = str(message.get("subject") or "")
preview = str(message.get("bodyPreview") or "")
body = str((message.get("body") or {}).get("content") or "")
combined = " ".join([sender, subject, preview, body]).lower()
if sender_filters and sender not in sender_filters and not any(item in combined for item in sender_filters):
return None
if subject_filters and not any(item in combined for item in subject_filters):
return None
if required_keywords and not any(item in combined for item in required_keywords):
return None
code = extract_code("\n".join([subject, preview, body, sender]), payload.get("codePatterns") or [])
if not code or code in excluded:
return None
return {"code": code, "message": message}
for use_time_fallback in [False, True]:
matches = [item for item in (candidate(message, not use_time_fallback) for message in messages) if item]
if matches:
matches.sort(key=lambda item: int(item["message"].get("receivedTimestamp") or 0), reverse=True)
best = matches[0]
return {"code": best["code"], "message": best["message"], "usedTimeFallback": use_time_fallback}
return {"code": "", "message": None, "usedTimeFallback": False}
class CustomMailHelperHandler(BaseHTTPRequestHandler):
def do_OPTIONS(self):
self.send_response(204)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
self.end_headers()
def do_POST(self):
try:
payload = read_json_payload(self)
if self.path == "/messages":
messages = fetch_recent_messages(payload.get("top") or DEFAULT_TOP)
json_response(self, 200, {"ok": True, "messages": messages})
return
if self.path == "/code":
messages = fetch_recent_messages(payload.get("top") or DEFAULT_TOP)
selected = select_latest_code(messages, payload)
json_response(self, 200, {
"ok": True,
"code": selected["code"],
"message": selected["message"],
"usedTimeFallback": selected["usedTimeFallback"],
})
return
if self.path == "/health":
json_response(self, 200, {"ok": True})
return
if self.path == "/env":
json_response(self, 200, {"ok": True, "env": get_public_env_payload()})
return
if self.path == "/random-email":
result = generate_random_emails(payload)
json_response(self, 200, {"ok": True, **result})
return
json_response(self, 404, {"ok": False, "error": f"Unsupported path: {self.path}"})
except Exception as exc:
traceback.print_exc()
json_response(self, 500, {"ok": False, "error": str(exc)})
def main():
server = ThreadingHTTPServer((HOST, PORT), CustomMailHelperHandler)
print(f"Custom mail helper listening on http://{HOST}:{PORT}", flush=True)
print(f"IMAP host={IMAP_HOST}:{IMAP_PORT} user={IMAP_USER or '(unset)'} mailbox={IMAP_MAILBOX}", flush=True)
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.server_close()
if __name__ == "__main__":
main()
+12
View File
@@ -553,6 +553,18 @@
<button id="btn-mail-login" class="btn btn-outline btn-sm data-inline-btn" type="button" disabled>登录</button> <button id="btn-mail-login" class="btn btn-outline btn-sm data-inline-btn" type="button" disabled>登录</button>
</div> </div>
</div> </div>
<div class="data-row" id="row-custom-mail-receive-mode" style="display:none;">
<span class="data-label">收码方式</span>
<select id="select-custom-mail-receive-mode" class="data-select">
<option value="manual">手动确认</option>
<option value="helper">本地 helper</option>
</select>
</div>
<div class="data-row" id="row-custom-mail-helper-base-url" style="display:none;">
<span class="data-label">helper 地址</span>
<input type="text" id="input-custom-mail-helper-base-url" class="data-input"
placeholder="http://127.0.0.1:17374" />
</div>
<div class="data-row data-check-row" id="row-custom-mail-provider-pool" style="display:none;"> <div class="data-row data-check-row" id="row-custom-mail-provider-pool" style="display:none;">
<span class="data-label">自定义号池</span> <span class="data-label">自定义号池</span>
<textarea id="input-custom-mail-provider-pool" class="data-textarea" <textarea id="input-custom-mail-provider-pool" class="data-textarea"
+86
View File
@@ -262,6 +262,10 @@ const rowGoPayPin = document.getElementById('row-gopay-pin');
const inputGoPayPin = document.getElementById('input-gopay-pin'); const inputGoPayPin = document.getElementById('input-gopay-pin');
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 rowCustomMailReceiveMode = document.getElementById('row-custom-mail-receive-mode');
const selectCustomMailReceiveMode = document.getElementById('select-custom-mail-receive-mode');
const rowCustomMailHelperBaseUrl = document.getElementById('row-custom-mail-helper-base-url');
const inputCustomMailHelperBaseUrl = document.getElementById('input-custom-mail-helper-base-url');
const rowCustomMailProviderPool = document.getElementById('row-custom-mail-provider-pool'); const rowCustomMailProviderPool = document.getElementById('row-custom-mail-provider-pool');
const inputCustomMailProviderPool = document.getElementById('input-custom-mail-provider-pool'); const inputCustomMailProviderPool = document.getElementById('input-custom-mail-provider-pool');
const rowMail2925Mode = document.getElementById('row-mail-2925-mode'); const rowMail2925Mode = document.getElementById('row-mail-2925-mode');
@@ -830,6 +834,10 @@ const DEFAULT_CPA_CALLBACK_MODE = 'step8';
const MAIL_2925_MODE_PROVIDE = 'provide'; const MAIL_2925_MODE_PROVIDE = 'provide';
const MAIL_2925_MODE_RECEIVE = 'receive'; const MAIL_2925_MODE_RECEIVE = 'receive';
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE; const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
const CUSTOM_MAIL_RECEIVE_MODE_MANUAL = 'manual';
const CUSTOM_MAIL_RECEIVE_MODE_HELPER = 'helper';
const DEFAULT_CUSTOM_MAIL_RECEIVE_MODE = CUSTOM_MAIL_RECEIVE_MODE_MANUAL;
const DEFAULT_CUSTOM_MAIL_HELPER_BASE_URL = 'http://127.0.0.1:17374';
const CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE_RECEIVE_MAILBOX = 'receive-mailbox'; const CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE_RECEIVE_MAILBOX = 'receive-mailbox';
const CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE_REGISTRATION_EMAIL = 'registration-email'; const CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE_REGISTRATION_EMAIL = 'registration-email';
const DEFAULT_CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE = CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE_RECEIVE_MAILBOX; const DEFAULT_CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE = CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE_RECEIVE_MAILBOX;
@@ -5161,6 +5169,14 @@ function collectSettingsPayload() {
mailProvider: selectMailProvider.value, mailProvider: selectMailProvider.value,
mail2925Mode: getSelectedMail2925Mode(), mail2925Mode: getSelectedMail2925Mode(),
mail2925UseAccountPool, mail2925UseAccountPool,
customMailReceiveMode: typeof getSelectedCustomMailReceiveMode === 'function'
? getSelectedCustomMailReceiveMode()
: 'manual',
customMailHelperBaseUrl: typeof normalizeCustomMailHelperBaseUrl === 'function'
? normalizeCustomMailHelperBaseUrl(typeof inputCustomMailHelperBaseUrl !== 'undefined' && inputCustomMailHelperBaseUrl
? inputCustomMailHelperBaseUrl.value
: '')
: 'http://127.0.0.1:17374',
currentMail2925AccountId: String(latestState?.currentMail2925AccountId || '').trim(), currentMail2925AccountId: String(latestState?.currentMail2925AccountId || '').trim(),
emailGenerator: selectEmailGenerator.value, emailGenerator: selectEmailGenerator.value,
customMailProviderPool: typeof normalizeCustomEmailPoolEntries === 'function' customMailProviderPool: typeof normalizeCustomEmailPoolEntries === 'function'
@@ -5279,6 +5295,34 @@ function normalizeMail2925Mode(value = '') {
: DEFAULT_MAIL_2925_MODE; : DEFAULT_MAIL_2925_MODE;
} }
function normalizeCustomMailReceiveMode(value = '') {
return String(value || '').trim().toLowerCase() === CUSTOM_MAIL_RECEIVE_MODE_HELPER
? CUSTOM_MAIL_RECEIVE_MODE_HELPER
: DEFAULT_CUSTOM_MAIL_RECEIVE_MODE;
}
function getSelectedCustomMailReceiveMode() {
return normalizeCustomMailReceiveMode(selectCustomMailReceiveMode?.value);
}
function normalizeCustomMailHelperBaseUrl(value = '') {
const trimmed = String(value || '').trim();
const candidate = trimmed || DEFAULT_CUSTOM_MAIL_HELPER_BASE_URL;
try {
const parsed = new URL(candidate);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return DEFAULT_CUSTOM_MAIL_HELPER_BASE_URL;
}
parsed.hash = '';
parsed.search = '';
parsed.pathname = parsed.pathname.replace(/\/+$/, '');
const path = parsed.pathname === '/' ? '' : parsed.pathname;
return `${parsed.origin}${path}` || DEFAULT_CUSTOM_MAIL_HELPER_BASE_URL;
} catch {
return DEFAULT_CUSTOM_MAIL_HELPER_BASE_URL;
}
}
function normalizeCloudflareTempEmailLookupMode(value = '') { function normalizeCloudflareTempEmailLookupMode(value = '') {
return String(value || '').trim().toLowerCase() === CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE_REGISTRATION_EMAIL return String(value || '').trim().toLowerCase() === CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE_REGISTRATION_EMAIL
? CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE_REGISTRATION_EMAIL ? CLOUDFLARE_TEMP_EMAIL_LOOKUP_MODE_REGISTRATION_EMAIL
@@ -11442,6 +11486,16 @@ function applySettingsState(state) {
? 'custom' ? 'custom'
: '163'); : '163');
selectMailProvider.value = restoredMailProvider; selectMailProvider.value = restoredMailProvider;
if (typeof selectCustomMailReceiveMode !== 'undefined' && selectCustomMailReceiveMode) {
selectCustomMailReceiveMode.value = typeof normalizeCustomMailReceiveMode === 'function'
? normalizeCustomMailReceiveMode(state?.customMailReceiveMode)
: (String(state?.customMailReceiveMode || '').trim().toLowerCase() === 'helper' ? 'helper' : 'manual');
}
if (typeof inputCustomMailHelperBaseUrl !== 'undefined' && inputCustomMailHelperBaseUrl) {
inputCustomMailHelperBaseUrl.value = typeof normalizeCustomMailHelperBaseUrl === 'function'
? normalizeCustomMailHelperBaseUrl(state?.customMailHelperBaseUrl)
: (String(state?.customMailHelperBaseUrl || '').trim() || 'http://127.0.0.1:17374');
}
setMail2925Mode(state?.mail2925Mode); setMail2925Mode(state?.mail2925Mode);
{ {
const restoredEmailGenerator = String(state?.emailGenerator || '').trim().toLowerCase(); const restoredEmailGenerator = String(state?.emailGenerator || '').trim().toLowerCase();
@@ -13042,6 +13096,12 @@ function updateMailProviderUI() {
if (typeof rowCustomMailProviderPool !== 'undefined' && rowCustomMailProviderPool) { if (typeof rowCustomMailProviderPool !== 'undefined' && rowCustomMailProviderPool) {
rowCustomMailProviderPool.style.display = useCustomEmail ? '' : 'none'; rowCustomMailProviderPool.style.display = useCustomEmail ? '' : 'none';
} }
if (typeof rowCustomMailReceiveMode !== 'undefined' && rowCustomMailReceiveMode) {
rowCustomMailReceiveMode.style.display = useCustomEmail ? '' : 'none';
}
if (typeof rowCustomMailHelperBaseUrl !== 'undefined' && rowCustomMailHelperBaseUrl) {
rowCustomMailHelperBaseUrl.style.display = useCustomEmail && getSelectedCustomMailReceiveMode() === CUSTOM_MAIL_RECEIVE_MODE_HELPER ? '' : 'none';
}
rowEmailPrefix.style.display = useGeneratedAlias && !useMail2925AccountPool ? '' : 'none'; rowEmailPrefix.style.display = useGeneratedAlias && !useMail2925AccountPool ? '' : 'none';
const hotmailServiceMode = getSelectedHotmailServiceMode(); const hotmailServiceMode = getSelectedHotmailServiceMode();
rowInbucketHost.style.display = useInbucket ? '' : 'none'; rowInbucketHost.style.display = useInbucket ? '' : 'none';
@@ -15759,6 +15819,32 @@ selectMailProvider.addEventListener('change', async () => {
saveSettings({ silent: true }).catch(() => { }); saveSettings({ silent: true }).catch(() => { });
}); });
[
selectCustomMailReceiveMode,
inputCustomMailHelperBaseUrl,
].forEach((input) => {
input?.addEventListener('input', () => {
if (input === selectCustomMailReceiveMode) {
updateMailProviderUI();
}
markSettingsDirty(true);
scheduleSettingsAutoSave();
});
input?.addEventListener('change', () => {
if (input === selectCustomMailReceiveMode) {
updateMailProviderUI();
}
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
});
input?.addEventListener('blur', () => {
if (input === inputCustomMailHelperBaseUrl) {
inputCustomMailHelperBaseUrl.value = normalizeCustomMailHelperBaseUrl(inputCustomMailHelperBaseUrl.value);
}
saveSettings({ silent: true }).catch(() => { });
});
});
mail2925ModeButtons.forEach((button) => { mail2925ModeButtons.forEach((button) => {
button.addEventListener('click', async () => { button.addEventListener('click', async () => {
const nextMode = normalizeMail2925Mode(button.dataset.mail2925Mode); const nextMode = normalizeMail2925Mode(button.dataset.mail2925Mode);
+19
View File
@@ -0,0 +1,19 @@
@echo off
setlocal
cd /d "%~dp0"
where py >nul 2>nul
if %errorlevel%==0 (
py -3 scripts\custom_mail_helper.py
goto :eof
)
where python >nul 2>nul
if %errorlevel%==0 (
python scripts\custom_mail_helper.py
goto :eof
)
echo Python 3 not found. Please install Python 3.10+ and try again.
pause
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
if command -v python3 >/dev/null 2>&1; then
exec python3 scripts/custom_mail_helper.py
fi
if command -v python >/dev/null 2>&1; then
exec python scripts/custom_mail_helper.py
fi
echo "Python 3 not found. Please install Python 3.10+ and try again."
read -r -p "Press Enter to exit..."
@@ -54,6 +54,84 @@ test('flow mail polling service dispatches API mail providers through shared hel
assert.equal(logs.some((entry) => entry.message.includes('Hotmail')), true); assert.equal(logs.some((entry) => entry.message.includes('Hotmail')), true);
}); });
test('flow mail polling service dispatches custom helper when custom provider is in helper mode', async () => {
const api = loadFlowMailPollingApi();
let customCall = null;
const service = api.createFlowMailPollingService({
addLog: async () => {},
buildVerificationPollPayloadForNode: (nodeId, state, overrides) => ({
flowId: state.activeFlowId,
nodeId,
step: 4,
targetEmail: 'custom@example.com',
maxAttempts: 1,
intervalMs: 100,
...overrides,
}),
CUSTOM_MAIL_PROVIDER: 'custom',
getMailConfig: () => ({ provider: 'custom', label: '自定义邮箱' }),
pollCustomMailVerificationCode: async (step, state, payload) => {
customCall = { step, state, payload };
return { code: '654321', emailTimestamp: 123 };
},
});
const result = await service.pollFlowVerificationCode({
flowId: 'kiro',
nodeId: 'kiro-submit-verification-code',
state: {
activeFlowId: 'kiro',
mailProvider: 'custom',
customMailReceiveMode: 'helper',
email: 'custom@example.com',
},
step: 4,
});
assert.equal(result.code, '654321');
assert.equal(customCall.step, 4);
assert.equal(customCall.payload.targetEmail, 'custom@example.com');
});
test('flow mail polling service rejects custom manual mode before helper polling', async () => {
const api = loadFlowMailPollingApi();
let customCallCount = 0;
const service = api.createFlowMailPollingService({
addLog: async () => {},
buildVerificationPollPayloadForNode: (nodeId, state, overrides) => ({
flowId: state.activeFlowId,
nodeId,
step: 4,
targetEmail: 'custom@example.com',
maxAttempts: 1,
intervalMs: 100,
...overrides,
}),
CUSTOM_MAIL_PROVIDER: 'custom',
getMailConfig: () => ({ provider: 'custom', label: '自定义邮箱' }),
pollCustomMailVerificationCode: async () => {
customCallCount += 1;
return { code: '654321', emailTimestamp: 123 };
},
});
await assert.rejects(
() => service.pollFlowVerificationCode({
flowId: 'kiro',
nodeId: 'kiro-submit-verification-code',
state: {
activeFlowId: 'kiro',
mailProvider: 'custom',
customMailReceiveMode: 'manual',
email: 'custom@example.com',
},
step: 4,
}),
/手动确认模式/
);
assert.equal(customCallCount, 0);
});
test('flow mail polling service prepares browser mail provider sessions and payload timeouts', async () => { test('flow mail polling service prepares browser mail provider sessions and payload timeouts', async () => {
const api = loadFlowMailPollingApi(); const api = loadFlowMailPollingApi();
let ensured2925 = null; let ensured2925 = null;
@@ -81,6 +81,8 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([
'plusPaymentMethod', 'plusPaymentMethod',
'plusAccountAccessStrategy', 'plusAccountAccessStrategy',
'mailProvider', 'mailProvider',
'customMailReceiveMode',
'customMailHelperBaseUrl',
'ipProxyEnabled', 'ipProxyEnabled',
'ipProxyService', 'ipProxyService',
'ipProxyMode', 'ipProxyMode',
@@ -98,6 +100,8 @@ const PERSISTED_SETTING_DEFAULTS = {
plusAccountAccessStrategy: 'oauth', plusAccountAccessStrategy: 'oauth',
phoneVerificationEnabled: false, phoneVerificationEnabled: false,
mailProvider: '163', mailProvider: '163',
customMailReceiveMode: 'manual',
customMailHelperBaseUrl: 'http://127.0.0.1:17374',
ipProxyEnabled: false, ipProxyEnabled: false,
ipProxyService: '711proxy', ipProxyService: '711proxy',
ipProxyMode: 'account', ipProxyMode: 'account',
@@ -135,6 +139,24 @@ function normalizeCloudflareDomains(value) { return Array.isArray(value) ? value
function normalizeCloudflareTempEmailDomains(value) { return Array.isArray(value) ? value : []; } function normalizeCloudflareTempEmailDomains(value) { return Array.isArray(value) ? value : []; }
function normalizeCloudMailDomains(value) { return Array.isArray(value) ? value : []; } function normalizeCloudMailDomains(value) { return Array.isArray(value) ? value : []; }
function normalizeMailProvider(value = '') { return String(value || '163').trim().toLowerCase() || '163'; } function normalizeMailProvider(value = '') { return String(value || '163').trim().toLowerCase() || '163'; }
function normalizeCustomMailReceiveMode(value = '') { return String(value || '').trim().toLowerCase() === 'helper' ? 'helper' : 'manual'; }
function normalizeCustomMailHelperBaseUrl(value = '') {
const trimmed = String(value || '').trim();
const candidate = trimmed || 'http://127.0.0.1:17374';
try {
const parsed = new URL(candidate);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return 'http://127.0.0.1:17374';
}
parsed.hash = '';
parsed.search = '';
parsed.pathname = parsed.pathname.replace(/[/]+$/, '');
const path = parsed.pathname === '/' ? '' : parsed.pathname;
return (parsed.origin + path) || 'http://127.0.0.1:17374';
} catch {
return 'http://127.0.0.1:17374';
}
}
function normalizeStepExecutionRangeByFlow(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; } function normalizeStepExecutionRangeByFlow(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
function normalizeIpProxyProviderValue(value) { return String(value || '711proxy').trim() || '711proxy'; } function normalizeIpProxyProviderValue(value) { return String(value || '711proxy').trim() || '711proxy'; }
function normalizeIpProxyMode(value) { return String(value || 'account').trim() || 'account'; } function normalizeIpProxyMode(value) { return String(value || 'account').trim() || 'account'; }
@@ -564,6 +586,49 @@ function getRemovedKeys() {
assert.equal(Object.prototype.hasOwnProperty.call(write, 'mailProvider'), false); assert.equal(Object.prototype.hasOwnProperty.call(write, 'mailProvider'), false);
}); });
test('setPersistentSettings mirrors custom mail helper mode into canonical email settings', async () => {
const api = buildHarness(`
const persistedWrites = [];
const removedKeys = [];
const chrome = {
storage: {
local: {
async get() {
return {};
},
async remove(keys) {
removedKeys.push(...(Array.isArray(keys) ? keys : [keys]));
},
async set(payload) {
persistedWrites.push(JSON.parse(JSON.stringify(payload)));
},
},
},
};
function getPersistedWrites() {
return persistedWrites;
}
function getRemovedKeys() {
return removedKeys;
}
`);
const persisted = await api.setPersistentSettings({
mailProvider: 'custom',
customMailReceiveMode: 'helper',
customMailHelperBaseUrl: 'http://127.0.0.1:17374/',
});
const write = api.getPersistedWrites().at(-1);
assert.equal(persisted.mailProvider, 'custom');
assert.equal(persisted.customMailReceiveMode, 'helper');
assert.equal(persisted.customMailHelperBaseUrl, 'http://127.0.0.1:17374');
assert.equal(write.settingsState.services.email.provider, 'custom');
assert.equal(write.settingsState.services.email.customReceiveMode, 'helper');
assert.equal(write.settingsState.services.email.customHelperBaseUrl, 'http://127.0.0.1:17374');
assert.equal(Object.prototype.hasOwnProperty.call(write, 'customMailReceiveMode'), false);
});
test('setPersistentSettings mirrors flat schema updates without resetting other canonical settings', async () => { test('setPersistentSettings mirrors flat schema updates without resetting other canonical settings', async () => {
const api = buildHarness(` const api = buildHarness(`
const persistedWrites = []; const persistedWrites = [];
@@ -0,0 +1,91 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('flows/openai/background/steps/fetch-login-code.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep8;`)(globalScope);
function createExecutorHarness(overrides = {}) {
let bypassCalls = 0;
let capturedMail = null;
let capturedState = null;
let capturedOptions = null;
const executor = api.createStep8Executor({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
completeNodeFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => { bypassCalls += 1; },
ensureStep8VerificationPageReady: async () => ({
state: 'verification_page',
displayedEmail: 'target@example.com',
url: 'https://auth.openai.com/verify',
}),
getMailConfig: () => ({ provider: 'custom', label: '自定义邮箱' }),
getState: async () => ({ mailProvider: 'custom', email: 'target@example.com' }),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => false,
isVerificationMailPollingError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
CLOUD_MAIL_PROVIDER: 'cloudmail',
resolveVerificationStep: async (_step, state, mail, options) => {
capturedState = state;
capturedMail = mail;
capturedOptions = options;
},
rerunStep7ForStep8Recovery: async () => {},
resolveSignupEmailForFlow: async () => 'target@example.com',
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async () => ({}),
setState: async () => {},
shouldUseCustomRegistrationEmail: () => true,
shouldUseCustomMailHelper: () => false,
sleepWithStop: async () => {},
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 1,
throwIfStopped: () => {},
...overrides,
});
return {
executor,
getBypassCalls: () => bypassCalls,
getCapturedMail: () => capturedMail,
getCapturedState: () => capturedState,
getCapturedOptions: () => capturedOptions,
};
}
test('step 8 keeps custom mail provider on manual confirmation by default', async () => {
const harness = createExecutorHarness();
await harness.executor.executeStep8({
mailProvider: 'custom',
customMailReceiveMode: 'manual',
email: 'target@example.com',
oauthUrl: 'https://auth.openai.com/oauth',
});
assert.equal(harness.getBypassCalls(), 1);
assert.equal(harness.getCapturedMail(), null);
});
test('step 8 routes custom mail provider through resolver only when helper mode is enabled', async () => {
const harness = createExecutorHarness({
shouldUseCustomMailHelper: () => true,
});
await harness.executor.executeStep8({
mailProvider: 'custom',
customMailReceiveMode: 'helper',
email: 'target@example.com',
oauthUrl: 'https://auth.openai.com/oauth',
});
assert.equal(harness.getBypassCalls(), 0);
assert.equal(harness.getCapturedMail().provider, 'custom');
assert.equal(harness.getCapturedState().step8VerificationTargetEmail, 'target@example.com');
assert.equal(harness.getCapturedOptions().targetEmail, 'target@example.com');
});
@@ -50,3 +50,108 @@ test('verification flow routes YYDS Mail provider to background poller', async (
assert.equal(pollCalls[0].step, 4); assert.equal(pollCalls[0].step, 4);
assert.equal(pollCalls[0].payload.maxAttempts, 1); assert.equal(pollCalls[0].payload.maxAttempts, 1);
}); });
test('verification flow routes custom mail provider to local helper poller', async () => {
const source = fs.readFileSync('background/verification-flow.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundVerificationFlow;`)(globalScope);
const pollCalls = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
buildVerificationPollPayload: () => ({ maxAttempts: 1, intervalMs: 1, targetEmail: 'target@example.com' }),
CUSTOM_MAIL_PROVIDER: 'custom',
getState: async () => ({}),
getTabId: async () => 1,
isStopError: () => false,
pollCustomMailVerificationCode: async (step, state, payload) => {
pollCalls.push({ step, state, payload });
return { ok: true, code: '654321', emailTimestamp: 2, mailId: 'custom-msg-1' };
},
sendToContentScript: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const result = await helpers.pollFreshVerificationCode(
4,
{ mailProvider: 'custom', customMailReceiveMode: 'helper', email: 'target@example.com' },
{ provider: 'custom', label: '自定义邮箱' },
{ disableTimeBudgetCap: true }
);
assert.equal(result.code, '654321');
assert.equal(pollCalls.length, 1);
assert.equal(pollCalls[0].step, 4);
assert.equal(pollCalls[0].payload.targetEmail, 'target@example.com');
});
test('background custom mail poller rejects manual mode before calling local helper', async () => {
const source = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') parenDepth += 1;
if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) signatureEnded = true;
}
if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
const api = new Function(`
const CUSTOM_MAIL_RECEIVE_MODE_HELPER = 'helper';
const DEFAULT_CUSTOM_MAIL_RECEIVE_MODE = 'manual';
function isCustomMailProvider(stateOrProvider) {
const provider = typeof stateOrProvider === 'string' ? stateOrProvider : stateOrProvider?.mailProvider;
return provider === 'custom';
}
${extractFunction('normalizeCustomMailReceiveMode')}
${extractFunction('shouldUseCustomMailHelper')}
async function addLog() {}
async function sleepWithStop() {}
function throwIfStopped() {}
async function requestCustomMailLocalCode() {
throw new Error('should not request helper in manual mode');
}
${extractFunction('pollCustomMailVerificationCode')}
return { pollCustomMailVerificationCode };
`)();
await assert.rejects(
() => api.pollCustomMailVerificationCode(4, {
mailProvider: 'custom',
customMailReceiveMode: 'manual',
}, { maxAttempts: 1, intervalMs: 1 }),
/手动确认模式/
);
});
+17
View File
@@ -60,6 +60,10 @@ test('sidepanel html exposes custom email pool generator option and input row',
assert.match(html, /id="input-custom-email-pool-import"/); assert.match(html, /id="input-custom-email-pool-import"/);
assert.match(html, /id="custom-email-pool-list"/); assert.match(html, /id="custom-email-pool-list"/);
assert.match(html, /id="btn-custom-email-pool-bulk-used"/); assert.match(html, /id="btn-custom-email-pool-bulk-used"/);
assert.match(html, /id="row-custom-mail-receive-mode"/);
assert.match(html, /id="select-custom-mail-receive-mode"/);
assert.match(html, /id="row-custom-mail-helper-base-url"/);
assert.match(html, /id="input-custom-mail-helper-base-url"/);
assert.match(html, /id="row-custom-mail-provider-pool"/); assert.match(html, /id="row-custom-mail-provider-pool"/);
assert.match(html, /id="input-custom-mail-provider-pool"/); assert.match(html, /id="input-custom-mail-provider-pool"/);
}); });
@@ -187,6 +191,19 @@ test('sidepanel queues custom email pool refresh when the pool row is visible',
); );
}); });
test('sidepanel only shows custom mail helper url when helper receive mode is selected', () => {
const source = extractFunction('updateMailProviderUI');
assert.match(
source,
/rowCustomMailReceiveMode\.style\.display = useCustomEmail \? '' : 'none'/
);
assert.match(
source,
/rowCustomMailHelperBaseUrl\.style\.display = useCustomEmail && getSelectedCustomMailReceiveMode\(\) === CUSTOM_MAIL_RECEIVE_MODE_HELPER \? '' : 'none'/
);
});
test('sidepanel custom verification dialog exposes add-phone action for step 8', async () => { test('sidepanel custom verification dialog exposes add-phone action for step 8', async () => {
const bundle = [ const bundle = [
extractFunction('getCustomVerificationPromptCopy'), extractFunction('getCustomVerificationPromptCopy'),
+2
View File
@@ -856,6 +856,8 @@ Grok flow 的目标是完成 Grok / xAI 注册页自动化,并把本轮提取
7. 如果当前目标轮次超出了号池数量,后台会直接报错提示数量不一致 7. 如果当前目标轮次超出了号池数量,后台会直接报错提示数量不一致
8. 这条链路只影响“注册邮箱分配”;Step 4 / Step 8 仍然走 `custom` provider 既有的手动验证码确认逻辑 8. 这条链路只影响“注册邮箱分配”;Step 4 / Step 8 仍然走 `custom` provider 既有的手动验证码确认逻辑
`mailProvider = custom` 默认收码方式是 `manual`,也就是继续弹出手动确认,不会自动读取邮箱。只有在侧边栏把 `收码方式` 显式切到 `本地 helper` 后,后台才会通过 `customMailHelperBaseUrl`(默认 `http://127.0.0.1:17374`)调用 [scripts/custom_mail_helper.py](./scripts/custom_mail_helper.py) 的 `/code` 接口轮询验证码;此时才会跳过手动确认弹窗。
### 7.2 共享模块分工 ### 7.2 共享模块分工
- `managed-alias-utils.js` - `managed-alias-utils.js`
+2
View File
@@ -39,6 +39,7 @@
- `rules.json`:静态 DNR 规则,主要处理 iCloud 相关请求头。 - `rules.json`:静态 DNR 规则,主要处理 iCloud 相关请求头。
- `start-hotmail-helper.bat`Windows 下启动本地 helper 的脚本;当前既服务于 Hotmail 本地收信,也服务于账号记录快照同步。 - `start-hotmail-helper.bat`Windows 下启动本地 helper 的脚本;当前既服务于 Hotmail 本地收信,也服务于账号记录快照同步。
- `start-hotmail-helper.command`macOS 下启动本地 helper 的脚本;当前既服务于 Hotmail 本地收信,也服务于账号记录快照同步。 - `start-hotmail-helper.command`macOS 下启动本地 helper 的脚本;当前既服务于 Hotmail 本地收信,也服务于账号记录快照同步。
- `start-custom-mail-helper.bat` / `start-custom-mail-helper.command`:启动自定义邮箱本地 helper;只有侧栏 `邮箱服务 = 自定义邮箱``收码方式 = 本地 helper` 时,扩展才会调用该 helper 自动轮询验证码。
- `THIRD_PARTY_NOTICES.md`:项目来源、共同历史、原仓库地址与许可说明,用于补充当前仓库与历史上游代码的来源关系。 - `THIRD_PARTY_NOTICES.md`:项目来源、共同历史、原仓库地址与许可说明,用于补充当前仓库与历史上游代码的来源关系。
- `开发者AI开发与PR提交流程.md`:仓库现有的 AI 开发与 PR 提交流程说明。 - `开发者AI开发与PR提交流程.md`:仓库现有的 AI 开发与 PR 提交流程说明。
- `项目文件结构说明.md`:当前文件,维护整个仓库非忽略文件的结构与职责索引。 - `项目文件结构说明.md`:当前文件,维护整个仓库非忽略文件的结构与职责索引。
@@ -183,6 +184,7 @@
- `scripts/gpc_sms_helper_macos.py`GPC Plus helper 的 macOS 本地短信验证码 helper,读取本机 Messages 数据库中的 GoPay/OpenAI 短信 OTP,并通过 `http://127.0.0.1:18767/latest-otp?phone=...&consume=1` 提供给扩展按手机号自动轮询;`/otp` 仍保留兼容,`consume=1` 会消费本次返回的验证码记录,避免下次重复读取。 - `scripts/gpc_sms_helper_macos.py`GPC Plus helper 的 macOS 本地短信验证码 helper,读取本机 Messages 数据库中的 GoPay/OpenAI 短信 OTP,并通过 `http://127.0.0.1:18767/latest-otp?phone=...&consume=1` 提供给扩展按手机号自动轮询;`/otp` 仍保留兼容,`consume=1` 会消费本次返回的验证码记录,避免下次重复读取。
- `scripts/hotmail_helper.py`:本地 helper 服务,负责通过本地接口协助 Hotmail 获取邮件和验证码,并提供账号记录 JSON 快照同步接口;旧的文本追加接口仍保留作兼容。 - `scripts/hotmail_helper.py`:本地 helper 服务,负责通过本地接口协助 Hotmail 获取邮件和验证码,并提供账号记录 JSON 快照同步接口;旧的文本追加接口仍保留作兼容。
- `scripts/custom_mail_helper.py`:自定义邮箱可选本地 IMAP helper,读取 `.env` 中的 `FLOWPILOT_CUSTOM_IMAP_*` 配置并提供 `/code``/messages` 等本地接口;默认自定义邮箱仍走手动确认,只有用户显式选择本地 helper 模式才会调用。
## `shared/` ## `shared/`