修复自定义邮箱收码模式默认手动
This commit is contained in:
+159
@@ -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,
|
||||||
|
|||||||
@@ -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} 邮箱轮询能力未接入,无法继续执行。`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 }),
|
||||||
|
/手动确认模式/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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/`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user