feat:新增CloudflareTempEmail
This commit is contained in:
+293
-10
@@ -1,6 +1,6 @@
|
||||
// background.js — Service Worker: orchestration, state, tab management, message routing
|
||||
|
||||
importScripts('data/names.js', 'hotmail-utils.js', 'content/activation-utils.js');
|
||||
importScripts('data/names.js', 'hotmail-utils.js', 'cloudflare-temp-email-utils.js', 'content/activation-utils.js');
|
||||
|
||||
const {
|
||||
buildHotmailMailApiLatestUrl,
|
||||
@@ -17,6 +17,17 @@ const {
|
||||
pickVerificationMessageWithTimeFallback,
|
||||
shouldClearHotmailCurrentSelection,
|
||||
} = self.HotmailUtils;
|
||||
const {
|
||||
DEFAULT_MAIL_PAGE_SIZE: CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE,
|
||||
buildCloudflareTempEmailHeaders,
|
||||
getCloudflareTempEmailAddressFromResponse,
|
||||
joinCloudflareTempEmailUrl,
|
||||
normalizeCloudflareTempEmailAddress,
|
||||
normalizeCloudflareTempEmailBaseUrl,
|
||||
normalizeCloudflareTempEmailDomain,
|
||||
normalizeCloudflareTempEmailDomains,
|
||||
normalizeCloudflareTempEmailMailApiMessages,
|
||||
} = self.CloudflareTempEmailUtils;
|
||||
const {
|
||||
isRecoverableStep9AuthFailure,
|
||||
} = self.MultiPageActivationUtils;
|
||||
@@ -24,6 +35,8 @@ const {
|
||||
const LOG_PREFIX = '[MultiPage:bg]';
|
||||
const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill';
|
||||
const HOTMAIL_PROVIDER = 'hotmail-api';
|
||||
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
|
||||
const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
|
||||
const HOTMAIL_MAILBOXES = ['INBOX', 'Junk'];
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
const HUMAN_STEP_DELAY_MIN = 700;
|
||||
@@ -81,6 +94,11 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
hotmailLocalBaseUrl: DEFAULT_HOTMAIL_LOCAL_BASE_URL,
|
||||
cloudflareDomain: '',
|
||||
cloudflareDomains: [],
|
||||
cloudflareTempEmailBaseUrl: '',
|
||||
cloudflareTempEmailAdminAuth: '',
|
||||
cloudflareTempEmailCustomAuth: '',
|
||||
cloudflareTempEmailDomain: '',
|
||||
cloudflareTempEmailDomains: [],
|
||||
hotmailAccounts: [],
|
||||
};
|
||||
|
||||
@@ -218,9 +236,8 @@ function normalizeEmailGenerator(value = '') {
|
||||
if (normalized === 'custom' || normalized === 'manual') {
|
||||
return 'custom';
|
||||
}
|
||||
if (normalized === 'cloudflare') {
|
||||
return 'cloudflare';
|
||||
}
|
||||
if (normalized === 'cloudflare') return 'cloudflare';
|
||||
if (normalized === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return CLOUDFLARE_TEMP_EMAIL_GENERATOR;
|
||||
return 'duck';
|
||||
}
|
||||
|
||||
@@ -233,6 +250,7 @@ function normalizeMailProvider(value = '') {
|
||||
switch (normalized) {
|
||||
case 'custom':
|
||||
case HOTMAIL_PROVIDER:
|
||||
case CLOUDFLARE_TEMP_EMAIL_PROVIDER:
|
||||
case '163':
|
||||
case '163-vip':
|
||||
case 'qq':
|
||||
@@ -330,6 +348,16 @@ function getHotmailServiceSettings(state = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function getCloudflareTempEmailConfig(state = {}) {
|
||||
return {
|
||||
baseUrl: normalizeCloudflareTempEmailBaseUrl(state.cloudflareTempEmailBaseUrl),
|
||||
adminAuth: String(state.cloudflareTempEmailAdminAuth || ''),
|
||||
customAuth: String(state.cloudflareTempEmailCustomAuth || ''),
|
||||
domain: normalizeCloudflareTempEmailDomain(state.cloudflareTempEmailDomain),
|
||||
domains: normalizeCloudflareTempEmailDomains(state.cloudflareTempEmailDomains),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePersistentSettingValue(key, value) {
|
||||
switch (key) {
|
||||
case 'panelMode':
|
||||
@@ -379,6 +407,15 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return normalizeCloudflareDomain(value);
|
||||
case 'cloudflareDomains':
|
||||
return normalizeCloudflareDomains(value);
|
||||
case 'cloudflareTempEmailBaseUrl':
|
||||
return normalizeCloudflareTempEmailBaseUrl(value);
|
||||
case 'cloudflareTempEmailAdminAuth':
|
||||
case 'cloudflareTempEmailCustomAuth':
|
||||
return String(value || '');
|
||||
case 'cloudflareTempEmailDomain':
|
||||
return normalizeCloudflareTempEmailDomain(value);
|
||||
case 'cloudflareTempEmailDomains':
|
||||
return normalizeCloudflareTempEmailDomains(value);
|
||||
case 'hotmailAccounts':
|
||||
return normalizeHotmailAccounts(value);
|
||||
default:
|
||||
@@ -422,6 +459,13 @@ function buildPersistentSettingsPayload(input = {}, options = {}) {
|
||||
}
|
||||
payload.cloudflareDomains = domains;
|
||||
}
|
||||
if (payload.cloudflareTempEmailDomains) {
|
||||
const domains = normalizeCloudflareTempEmailDomains(payload.cloudflareTempEmailDomains);
|
||||
if (payload.cloudflareTempEmailDomain && !domains.includes(payload.cloudflareTempEmailDomain)) {
|
||||
domains.unshift(payload.cloudflareTempEmailDomain);
|
||||
}
|
||||
payload.cloudflareTempEmailDomains = domains;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
@@ -1302,6 +1346,120 @@ function buildGeneratedAliasEmail(state) {
|
||||
throw new Error(`未支持的别名邮箱类型:${provider}`);
|
||||
}
|
||||
|
||||
function summarizeCloudflareTempEmailMessagesForLog(messages) {
|
||||
return (messages || [])
|
||||
.slice()
|
||||
.sort((left, right) => {
|
||||
const leftTime = Date.parse(left.receivedDateTime || '') || 0;
|
||||
const rightTime = Date.parse(right.receivedDateTime || '') || 0;
|
||||
return rightTime - leftTime;
|
||||
})
|
||||
.slice(0, 3)
|
||||
.map((message) => {
|
||||
const receivedAt = message?.receivedDateTime || '未知时间';
|
||||
const sender = message?.from?.emailAddress?.address || '未知发件人';
|
||||
const subject = message?.subject || '(无主题)';
|
||||
const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80);
|
||||
const address = message?.address || '未知地址';
|
||||
return `[${address}] ${receivedAt} | ${sender} | ${subject} | ${preview}`;
|
||||
})
|
||||
.join(' || ');
|
||||
}
|
||||
|
||||
async function deleteCloudflareTempEmailMail(config, mailId) {
|
||||
const normalizedMailId = String(mailId || '').trim();
|
||||
if (!normalizedMailId) return false;
|
||||
|
||||
await requestCloudflareTempEmailJson(config, `/admin/mails/${encodeURIComponent(normalizedMailId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async function listCloudflareTempEmailMessages(state, options = {}) {
|
||||
const config = ensureCloudflareTempEmailConfig(state, { requireAdminAuth: true });
|
||||
const address = normalizeCloudflareTempEmailAddress(options.address);
|
||||
const payload = await requestCloudflareTempEmailJson(config, '/admin/mails', {
|
||||
method: 'GET',
|
||||
searchParams: {
|
||||
limit: Number(options.limit) || CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE,
|
||||
offset: Number(options.offset) || 0,
|
||||
address,
|
||||
},
|
||||
});
|
||||
|
||||
const messages = normalizeCloudflareTempEmailMailApiMessages(payload).filter((message) => {
|
||||
if (!address) return true;
|
||||
return !message.address || normalizeCloudflareTempEmailAddress(message.address) === address;
|
||||
});
|
||||
|
||||
return { config, messages };
|
||||
}
|
||||
|
||||
async function pollCloudflareTempEmailVerificationCode(step, state, pollPayload = {}) {
|
||||
const targetEmail = normalizeCloudflareTempEmailAddress(pollPayload.targetEmail || state.email);
|
||||
if (!targetEmail) {
|
||||
throw new Error('Cloudflare Temp Email 轮询前缺少目标邮箱地址。');
|
||||
}
|
||||
|
||||
await addLog(`步骤 ${step}:正在轮询 Cloudflare Temp Email 邮件(${targetEmail})...`, 'info');
|
||||
const maxAttempts = Number(pollPayload.maxAttempts) || 5;
|
||||
const intervalMs = Number(pollPayload.intervalMs) || 3000;
|
||||
let lastError = null;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
throwIfStopped();
|
||||
try {
|
||||
const { config, messages } = await listCloudflareTempEmailMessages(state, {
|
||||
address: targetEmail,
|
||||
limit: pollPayload.limit || CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE,
|
||||
offset: pollPayload.offset || 0,
|
||||
});
|
||||
const matchResult = pickVerificationMessageWithTimeFallback(messages, {
|
||||
afterTimestamp: pollPayload.filterAfterTimestamp || 0,
|
||||
senderFilters: pollPayload.senderFilters || [],
|
||||
subjectFilters: pollPayload.subjectFilters || [],
|
||||
excludeCodes: pollPayload.excludeCodes || [],
|
||||
});
|
||||
const match = matchResult.match;
|
||||
|
||||
if (match?.code) {
|
||||
if (matchResult.usedRelaxedFilters) {
|
||||
const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配';
|
||||
await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Cloudflare Temp Email 验证码。`, 'warn');
|
||||
}
|
||||
try {
|
||||
await deleteCloudflareTempEmailMail(config, match.message?.id);
|
||||
} catch (err) {
|
||||
await addLog(`步骤 ${step}:删除 Cloudflare Temp Email 邮件失败:${err.message}`, 'warn');
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
code: match.code,
|
||||
emailTimestamp: match.receivedAt || Date.now(),
|
||||
mailId: match.message?.id || '',
|
||||
};
|
||||
}
|
||||
|
||||
lastError = new Error(`步骤 ${step}:暂未在 Cloudflare Temp Email 中找到匹配验证码(${attempt}/${maxAttempts})。`);
|
||||
await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info');
|
||||
const sample = summarizeCloudflareTempEmailMessagesForLog(messages);
|
||||
if (sample) {
|
||||
await addLog(`步骤 ${step}:最近邮件样本:${sample}`, 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
await addLog(`步骤 ${step}:Cloudflare Temp Email 轮询失败:${err.message}`, 'warn');
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
await sleepWithStop(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error(`步骤 ${step}:未在 Cloudflare Temp Email 中找到新的匹配验证码。`);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Tab Registry
|
||||
// ============================================================
|
||||
@@ -1532,11 +1690,14 @@ function buildLocalhostCleanupPrefix(rawUrl) {
|
||||
async function closeTabsByUrlPrefix(prefix, options = {}) {
|
||||
if (!prefix) return 0;
|
||||
|
||||
const { excludeTabIds = [] } = options;
|
||||
const { excludeTabIds = [], excludeUrls = [], excludeLocalhostCallbacks = false } = options;
|
||||
const excluded = new Set(excludeTabIds.filter(id => Number.isInteger(id)));
|
||||
const excludedUrls = new Set((Array.isArray(excludeUrls) ? excludeUrls : []).filter(Boolean));
|
||||
const tabs = await chrome.tabs.query({});
|
||||
const matchedIds = tabs
|
||||
.filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
|
||||
.filter((tab) => typeof tab.url === 'string' && !excludedUrls.has(tab.url))
|
||||
.filter((tab) => !(excludeLocalhostCallbacks && isLocalhostOAuthCallbackUrl(tab.url)))
|
||||
.filter((tab) => typeof tab.url === 'string' && tab.url.startsWith(prefix))
|
||||
.map((tab) => tab.id);
|
||||
|
||||
@@ -2124,6 +2285,7 @@ function getSourceLabel(source) {
|
||||
'inbucket-mail': 'Inbucket 邮箱',
|
||||
'duck-mail': 'Duck 邮箱',
|
||||
'hotmail-api': 'Hotmail(远程/本地)',
|
||||
'cloudflare-temp-email': 'Cloudflare Temp Email',
|
||||
};
|
||||
return labels[source] || source || '未知来源';
|
||||
}
|
||||
@@ -3141,7 +3303,10 @@ async function handleStepData(step, payload) {
|
||||
}
|
||||
const localhostPrefix = buildLocalhostCleanupPrefix(payload.localhostUrl);
|
||||
if (localhostPrefix) {
|
||||
await closeTabsByUrlPrefix(localhostPrefix);
|
||||
await closeTabsByUrlPrefix(localhostPrefix, {
|
||||
excludeUrls: [payload.localhostUrl],
|
||||
excludeLocalhostCallbacks: true,
|
||||
});
|
||||
}
|
||||
if (shouldUseCustomRegistrationEmail(latestState) && latestState.email) {
|
||||
await setEmailStateSilently(null);
|
||||
@@ -3434,7 +3599,9 @@ function getEmailGeneratorLabel(generator) {
|
||||
if (generator === 'custom') {
|
||||
return '自定义邮箱';
|
||||
}
|
||||
return generator === 'cloudflare' ? 'Cloudflare 邮箱' : 'Duck 邮箱';
|
||||
if (generator === 'cloudflare') return 'Cloudflare 邮箱';
|
||||
if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email';
|
||||
return 'Duck 邮箱';
|
||||
}
|
||||
|
||||
function generateCloudflareAliasLocalPart() {
|
||||
@@ -3474,6 +3641,107 @@ async function fetchCloudflareEmail(state, options = {}) {
|
||||
return aliasEmail;
|
||||
}
|
||||
|
||||
function ensureCloudflareTempEmailConfig(state, options = {}) {
|
||||
const {
|
||||
requireAdminAuth = false,
|
||||
requireDomain = false,
|
||||
} = options;
|
||||
const config = getCloudflareTempEmailConfig(state);
|
||||
if (!config.baseUrl) {
|
||||
throw new Error('Cloudflare Temp Email 服务地址为空或格式无效。');
|
||||
}
|
||||
if (requireAdminAuth && !config.adminAuth) {
|
||||
throw new Error('Cloudflare Temp Email 缺少 Admin Auth。');
|
||||
}
|
||||
if (requireDomain && !config.domain) {
|
||||
throw new Error('Cloudflare Temp Email 域名为空或格式无效。');
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
async function requestCloudflareTempEmailJson(config, path, options = {}) {
|
||||
const {
|
||||
method = 'GET',
|
||||
payload,
|
||||
searchParams,
|
||||
timeoutMs = 20000,
|
||||
} = options;
|
||||
|
||||
const url = new URL(joinCloudflareTempEmailUrl(config.baseUrl, path));
|
||||
if (searchParams && typeof searchParams === 'object') {
|
||||
for (const [key, value] of Object.entries(searchParams)) {
|
||||
if (value === undefined || value === null || value === '') continue;
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(url.toString(), {
|
||||
method,
|
||||
headers: buildCloudflareTempEmailHeaders(config, {
|
||||
json: payload !== undefined,
|
||||
}),
|
||||
body: payload !== undefined ? JSON.stringify(payload) : undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMessage = err?.name === 'AbortError'
|
||||
? `Cloudflare Temp Email 请求超时(>${Math.round(timeoutMs / 1000)} 秒)`
|
||||
: `Cloudflare Temp Email 请求失败:${err.message}`;
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
let parsed;
|
||||
try {
|
||||
parsed = text ? JSON.parse(text) : {};
|
||||
} catch {
|
||||
parsed = text;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const payloadError = typeof parsed === 'object' && parsed
|
||||
? (parsed.message || parsed.error || parsed.msg)
|
||||
: '';
|
||||
throw new Error(`Cloudflare Temp Email 请求失败:${payloadError || text || `HTTP ${response.status}`}`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function fetchCloudflareTempEmailAddress(state, options = {}) {
|
||||
throwIfStopped();
|
||||
const latestState = state || await getState();
|
||||
const config = ensureCloudflareTempEmailConfig(latestState, {
|
||||
requireAdminAuth: true,
|
||||
requireDomain: true,
|
||||
});
|
||||
const requestedName = String(options.localPart || options.name || '').trim().toLowerCase() || generateCloudflareAliasLocalPart();
|
||||
const payload = {
|
||||
enablePrefix: true,
|
||||
name: requestedName,
|
||||
domain: config.domain,
|
||||
};
|
||||
const result = await requestCloudflareTempEmailJson(config, '/admin/new_address', {
|
||||
method: 'POST',
|
||||
payload,
|
||||
});
|
||||
const address = normalizeCloudflareTempEmailAddress(getCloudflareTempEmailAddressFromResponse(result));
|
||||
if (!address) {
|
||||
throw new Error('Cloudflare Temp Email 未返回可用邮箱地址。');
|
||||
}
|
||||
|
||||
await setEmailState(address);
|
||||
await addLog(`Cloudflare Temp Email:已生成 ${address}`, 'ok');
|
||||
return address;
|
||||
}
|
||||
|
||||
async function fetchDuckEmail(options = {}) {
|
||||
throwIfStopped();
|
||||
const { generateNew = true } = options;
|
||||
@@ -3508,6 +3776,9 @@ async function fetchGeneratedEmail(state, options = {}) {
|
||||
if (generator === 'cloudflare') {
|
||||
return fetchCloudflareEmail(currentState, options);
|
||||
}
|
||||
if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) {
|
||||
return fetchCloudflareTempEmailAddress(currentState, options);
|
||||
}
|
||||
return fetchDuckEmail(options);
|
||||
}
|
||||
|
||||
@@ -3610,7 +3881,10 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
await addLog(`${generatorLabel}自动获取失败(${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS}):${err.message}`, 'warn');
|
||||
if (generator === 'cloudflare' && /域名/.test(String(err.message || ''))) {
|
||||
if (
|
||||
(generator === 'cloudflare' && /域名/.test(String(err.message || '')))
|
||||
|| (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR && /(服务地址|Admin Auth|域名)/.test(String(err.message || '')))
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -4732,6 +5006,9 @@ function getMailConfig(state) {
|
||||
if (provider === HOTMAIL_PROVIDER) {
|
||||
return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(远程/本地)' };
|
||||
}
|
||||
if (provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
|
||||
return { provider: CLOUDFLARE_TEMP_EMAIL_PROVIDER, label: 'Cloudflare Temp Email' };
|
||||
}
|
||||
if (provider === '163') {
|
||||
return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 邮箱' };
|
||||
}
|
||||
@@ -4896,6 +5173,12 @@ async function pollFreshVerificationCode(step, state, mail, pollOverrides = {})
|
||||
...pollOverrides,
|
||||
});
|
||||
}
|
||||
if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
|
||||
return pollCloudflareTempEmailVerificationCode(step, state, {
|
||||
...getVerificationPollPayload(step, state),
|
||||
...pollOverrides,
|
||||
});
|
||||
}
|
||||
|
||||
const stateKey = getVerificationCodeStateKey(step);
|
||||
const rejectedCodes = new Set();
|
||||
@@ -5111,7 +5394,7 @@ async function executeStep4(state) {
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
if (mail.provider === HOTMAIL_PROVIDER) {
|
||||
if (mail.provider === HOTMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
|
||||
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
|
||||
} else {
|
||||
await addLog(`步骤 4:正在打开${mail.label}...`);
|
||||
@@ -5246,7 +5529,7 @@ async function runStep7Attempt(state) {
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
if (mail.provider === HOTMAIL_PROVIDER) {
|
||||
if (mail.provider === HOTMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
|
||||
await addLog(`步骤 7:正在通过 ${mail.label} 轮询验证码...`);
|
||||
} else {
|
||||
await addLog(`步骤 7:正在打开${mail.label}...`);
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
(function cloudflareTempEmailUtilsModule(root, factory) {
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = factory();
|
||||
return;
|
||||
}
|
||||
|
||||
root.CloudflareTempEmailUtils = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createCloudflareTempEmailUtils() {
|
||||
const DEFAULT_MAIL_PAGE_SIZE = 20;
|
||||
|
||||
function firstNonEmptyString(values) {
|
||||
for (const value of values) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const normalized = String(value).trim();
|
||||
if (normalized) return normalized;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizeCloudflareTempEmailBaseUrl(rawValue = '') {
|
||||
const value = String(rawValue || '').trim();
|
||||
if (!value) return '';
|
||||
|
||||
const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`;
|
||||
try {
|
||||
const parsed = new URL(candidate);
|
||||
parsed.hash = '';
|
||||
parsed.search = '';
|
||||
const pathname = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/+$/, '');
|
||||
return `${parsed.origin}${pathname}`;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCloudflareTempEmailDomain(rawValue = '') {
|
||||
let value = String(rawValue || '').trim().toLowerCase();
|
||||
if (!value) return '';
|
||||
value = value.replace(/^@+/, '');
|
||||
value = value.replace(/^https?:\/\//, '');
|
||||
value = value.replace(/\/.*$/, '');
|
||||
if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(value)) {
|
||||
return '';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeCloudflareTempEmailDomains(values) {
|
||||
const domains = [];
|
||||
const seen = new Set();
|
||||
for (const value of Array.isArray(values) ? values : []) {
|
||||
const normalized = normalizeCloudflareTempEmailDomain(value);
|
||||
if (!normalized || seen.has(normalized)) continue;
|
||||
seen.add(normalized);
|
||||
domains.push(normalized);
|
||||
}
|
||||
return domains;
|
||||
}
|
||||
|
||||
function buildCloudflareTempEmailHeaders(config = {}, options = {}) {
|
||||
const headers = {};
|
||||
const adminAuth = firstNonEmptyString([config.adminAuth, config.cloudflareTempEmailAdminAuth]);
|
||||
const customAuth = firstNonEmptyString([config.customAuth, config.cloudflareTempEmailCustomAuth]);
|
||||
if (adminAuth) {
|
||||
headers['x-admin-auth'] = adminAuth;
|
||||
}
|
||||
if (customAuth) {
|
||||
headers['x-custom-auth'] = customAuth;
|
||||
}
|
||||
if (options.json) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
if (options.acceptJson !== false) {
|
||||
headers.Accept = 'application/json';
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
function joinCloudflareTempEmailUrl(baseUrl, path) {
|
||||
const normalizedBase = normalizeCloudflareTempEmailBaseUrl(baseUrl);
|
||||
const normalizedPath = String(path || '').trim();
|
||||
if (!normalizedBase || !normalizedPath) return normalizedBase || '';
|
||||
return `${normalizedBase}${normalizedPath.startsWith('/') ? '' : '/'}${normalizedPath}`;
|
||||
}
|
||||
|
||||
function getCloudflareTempEmailMailRows(payload) {
|
||||
if (Array.isArray(payload)) return payload;
|
||||
if (!payload || typeof payload !== 'object') return [];
|
||||
|
||||
const candidates = [
|
||||
payload.data,
|
||||
payload.items,
|
||||
payload.messages,
|
||||
payload.mails,
|
||||
payload.results,
|
||||
payload.rows,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (Array.isArray(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function normalizeCloudflareTempEmailAddress(value) {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function splitRawMessage(raw = '') {
|
||||
const source = String(raw || '');
|
||||
if (!source) {
|
||||
return { headerText: '', bodyText: '' };
|
||||
}
|
||||
|
||||
const normalized = source.replace(/\r\n/g, '\n');
|
||||
const separatorIndex = normalized.indexOf('\n\n');
|
||||
if (separatorIndex === -1) {
|
||||
return { headerText: normalized, bodyText: '' };
|
||||
}
|
||||
|
||||
return {
|
||||
headerText: normalized.slice(0, separatorIndex),
|
||||
bodyText: normalized.slice(separatorIndex + 2),
|
||||
};
|
||||
}
|
||||
|
||||
function parseRawHeaders(headerText = '') {
|
||||
const headers = {};
|
||||
const lines = String(headerText || '').split('\n');
|
||||
let currentName = '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line) continue;
|
||||
if ((line.startsWith(' ') || line.startsWith('\t')) && currentName) {
|
||||
headers[currentName] += ` ${line.trim()}`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const separatorIndex = line.indexOf(':');
|
||||
if (separatorIndex <= 0) continue;
|
||||
currentName = line.slice(0, separatorIndex).trim().toLowerCase();
|
||||
headers[currentName] = line.slice(separatorIndex + 1).trim();
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function decodeMimeEncodedWords(value = '') {
|
||||
const source = String(value || '');
|
||||
return source.replace(/=\?([^?]+)\?([bBqQ])\?([^?]+)\?=/g, (_match, charset, encoding, encodedText) => {
|
||||
try {
|
||||
if (String(encoding).toUpperCase() === 'B') {
|
||||
return decodeBytesToString(base64ToBytes(encodedText), charset);
|
||||
}
|
||||
return decodeBytesToString(
|
||||
quotedPrintableToBytes(String(encodedText).replace(/_/g, ' '), { headerMode: true }),
|
||||
charset
|
||||
);
|
||||
} catch {
|
||||
return encodedText;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function base64ToBytes(value = '') {
|
||||
const normalized = String(value || '').replace(/\s+/g, '');
|
||||
if (!normalized) return new Uint8Array();
|
||||
|
||||
if (typeof atob === 'function') {
|
||||
const decoded = atob(normalized);
|
||||
const bytes = new Uint8Array(decoded.length);
|
||||
for (let i = 0; i < decoded.length; i += 1) {
|
||||
bytes[i] = decoded.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
if (typeof Buffer !== 'undefined') {
|
||||
return Uint8Array.from(Buffer.from(normalized, 'base64'));
|
||||
}
|
||||
|
||||
throw new Error('No base64 decoder available');
|
||||
}
|
||||
|
||||
function quotedPrintableToBytes(value = '', options = {}) {
|
||||
const { headerMode = false } = options;
|
||||
const source = String(value || '')
|
||||
.replace(/=\r?\n/g, '')
|
||||
.replace(headerMode ? /_/g : /$^/, ' ');
|
||||
|
||||
const bytes = [];
|
||||
for (let index = 0; index < source.length; index += 1) {
|
||||
const char = source[index];
|
||||
if (char === '=' && /^[0-9A-Fa-f]{2}$/.test(source.slice(index + 1, index + 3))) {
|
||||
bytes.push(parseInt(source.slice(index + 1, index + 3), 16));
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
bytes.push(char.charCodeAt(0));
|
||||
}
|
||||
return Uint8Array.from(bytes);
|
||||
}
|
||||
|
||||
function decodeBytesToString(bytes, charset = 'utf-8') {
|
||||
const normalizedCharset = String(charset || 'utf-8').trim().toLowerCase();
|
||||
const candidates = [normalizedCharset];
|
||||
if (normalizedCharset === 'utf8') {
|
||||
candidates.unshift('utf-8');
|
||||
}
|
||||
if (normalizedCharset === 'gb2312' || normalizedCharset === 'gbk') {
|
||||
candidates.unshift('gb18030');
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
if (typeof TextDecoder !== 'undefined') {
|
||||
return new TextDecoder(candidate, { fatal: false }).decode(bytes);
|
||||
}
|
||||
} catch {
|
||||
// ignore and try fallback
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof Buffer !== 'undefined') {
|
||||
return Buffer.from(bytes).toString('utf8');
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (const byte of bytes) {
|
||||
result += String.fromCharCode(byte);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getCharsetFromContentType(contentType = '') {
|
||||
const match = String(contentType || '').match(/charset="?([^";]+)"?/i);
|
||||
return match ? match[1].trim() : 'utf-8';
|
||||
}
|
||||
|
||||
function getBoundaryFromContentType(contentType = '') {
|
||||
const match = String(contentType || '').match(/boundary="?([^";]+)"?/i);
|
||||
return match ? match[1] : '';
|
||||
}
|
||||
|
||||
function stripHtmlTags(value = '') {
|
||||
return String(value || '')
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/&/gi, '&')
|
||||
.replace(/</gi, '<')
|
||||
.replace(/>/gi, '>')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function decodeMimeBody(bodyText = '', headers = {}) {
|
||||
const contentType = String(headers['content-type'] || '');
|
||||
const transferEncoding = String(headers['content-transfer-encoding'] || '').trim().toLowerCase();
|
||||
const charset = getCharsetFromContentType(contentType);
|
||||
let decoded = String(bodyText || '');
|
||||
|
||||
if (transferEncoding === 'base64') {
|
||||
decoded = decodeBytesToString(base64ToBytes(decoded), charset);
|
||||
} else if (transferEncoding === 'quoted-printable') {
|
||||
decoded = decodeBytesToString(quotedPrintableToBytes(decoded), charset);
|
||||
}
|
||||
|
||||
if (/text\/html/i.test(contentType)) {
|
||||
return stripHtmlTags(decoded);
|
||||
}
|
||||
return decoded.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function extractTextFromMime(rawMessage = '', depth = 0) {
|
||||
const { headerText, bodyText } = splitRawMessage(rawMessage);
|
||||
const headers = parseRawHeaders(headerText);
|
||||
const contentType = String(headers['content-type'] || '');
|
||||
const boundary = getBoundaryFromContentType(contentType);
|
||||
|
||||
if (/multipart\//i.test(contentType) && boundary && depth < 6) {
|
||||
const marker = `--${boundary}`;
|
||||
const sections = String(bodyText || '')
|
||||
.split(marker)
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => part && part !== '--');
|
||||
|
||||
const extractedParts = sections
|
||||
.map((part) => part.replace(/--\s*$/, '').trim())
|
||||
.map((part) => extractTextFromMime(part, depth + 1)?.text || '')
|
||||
.filter(Boolean);
|
||||
|
||||
const plainText = extractedParts.join(' ').replace(/\s+/g, ' ').trim();
|
||||
return {
|
||||
headers,
|
||||
text: plainText,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
text: decodeMimeBody(bodyText, headers),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeReceivedDateTime(value) {
|
||||
if (!value && value !== 0) return '';
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return new Date(value).toISOString();
|
||||
}
|
||||
const source = String(value || '').trim();
|
||||
if (!source) return '';
|
||||
const parsed = Date.parse(source);
|
||||
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : source;
|
||||
}
|
||||
|
||||
function normalizeCloudflareTempEmailMessage(row = {}) {
|
||||
if (!row || typeof row !== 'object') return null;
|
||||
|
||||
const address = normalizeCloudflareTempEmailAddress(firstNonEmptyString([
|
||||
row.address,
|
||||
row.mail_address,
|
||||
row.email,
|
||||
row.recipient,
|
||||
]));
|
||||
const raw = firstNonEmptyString([row.raw, row.source, row.mime, row.message]);
|
||||
const parsedMime = raw ? extractTextFromMime(raw) : { headers: {}, text: '' };
|
||||
const subject = decodeMimeEncodedWords(firstNonEmptyString([
|
||||
row.subject,
|
||||
parsedMime.headers.subject,
|
||||
]));
|
||||
const fromAddress = decodeMimeEncodedWords(firstNonEmptyString([
|
||||
row.from,
|
||||
row.sender,
|
||||
row.mail_from,
|
||||
parsedMime.headers.from,
|
||||
]));
|
||||
const bodyPreview = firstNonEmptyString([
|
||||
row.text,
|
||||
row.preview,
|
||||
row.body,
|
||||
parsedMime.text,
|
||||
raw,
|
||||
]).replace(/\s+/g, ' ').trim();
|
||||
|
||||
return {
|
||||
id: firstNonEmptyString([row.id, row.mail_id]),
|
||||
address,
|
||||
addressId: firstNonEmptyString([row.address_id, row.addressId]),
|
||||
subject,
|
||||
from: {
|
||||
emailAddress: {
|
||||
address: fromAddress,
|
||||
},
|
||||
},
|
||||
bodyPreview,
|
||||
raw,
|
||||
receivedDateTime: normalizeReceivedDateTime(firstNonEmptyString([
|
||||
row.receivedDateTime,
|
||||
row.received_at,
|
||||
row.created_at,
|
||||
row.createdAt,
|
||||
row.updated_at,
|
||||
row.date,
|
||||
])),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCloudflareTempEmailMailApiMessages(payload) {
|
||||
return getCloudflareTempEmailMailRows(payload)
|
||||
.map((row) => normalizeCloudflareTempEmailMessage(row))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function getCloudflareTempEmailAddressFromResponse(payload = {}) {
|
||||
return firstNonEmptyString([
|
||||
payload.address,
|
||||
payload.email,
|
||||
payload?.data?.address,
|
||||
payload?.data?.email,
|
||||
]);
|
||||
}
|
||||
|
||||
return {
|
||||
DEFAULT_MAIL_PAGE_SIZE,
|
||||
buildCloudflareTempEmailHeaders,
|
||||
getCloudflareTempEmailAddressFromResponse,
|
||||
joinCloudflareTempEmailUrl,
|
||||
normalizeCloudflareTempEmailAddress,
|
||||
normalizeCloudflareTempEmailBaseUrl,
|
||||
normalizeCloudflareTempEmailDomain,
|
||||
normalizeCloudflareTempEmailDomains,
|
||||
normalizeCloudflareTempEmailMailApiMessages,
|
||||
normalizeCloudflareTempEmailMessage,
|
||||
};
|
||||
});
|
||||
@@ -159,6 +159,7 @@
|
||||
<option value="qq">QQ 邮箱 (wx.mail.qq.com)</option>
|
||||
<option value="inbucket">Inbucket(自定义主机)</option>
|
||||
<option value="2925">2925 邮箱 (2925.com)</option>
|
||||
<option value="cloudflare-temp-email">Cloudflare Temp Email</option>
|
||||
</select>
|
||||
<button id="btn-mail-login" class="btn btn-outline btn-sm data-inline-btn" type="button" disabled>登录</button>
|
||||
</div>
|
||||
@@ -168,8 +169,30 @@
|
||||
<select id="select-email-generator" class="data-select">
|
||||
<option value="duck">DuckDuckGo</option>
|
||||
<option value="cloudflare">Cloudflare</option>
|
||||
<option value="cloudflare-temp-email">Cloudflare Temp Email</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-base-url" style="display:none;">
|
||||
<span class="data-label">Temp API</span>
|
||||
<input type="text" id="input-temp-email-base-url" class="data-input" placeholder="https://your-worker-domain" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-admin-auth" style="display:none;">
|
||||
<span class="data-label">Admin Auth</span>
|
||||
<input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon" placeholder="Cloudflare Temp Email admin password" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-custom-auth" style="display:none;">
|
||||
<span class="data-label">Custom Auth</span>
|
||||
<input type="password" id="input-temp-email-custom-auth" class="data-input data-input-with-icon" placeholder="仅当站点启用了访问密码时再填写;这是额外鉴权,不替代 Admin Auth。" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-domain" style="display:none;">
|
||||
<span class="data-label">Temp 域名</span>
|
||||
<div class="data-inline">
|
||||
<select id="select-temp-email-domain" class="data-select"></select>
|
||||
<input type="text" id="input-temp-email-domain" class="data-input" placeholder="例如 mail.example.com"
|
||||
style="display:none;" />
|
||||
<button id="btn-temp-email-domain-mode" class="btn btn-outline btn-sm" type="button">添加</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-cf-domain" style="display:none;">
|
||||
<span class="data-label">CF 域名</span>
|
||||
<div class="data-inline">
|
||||
|
||||
+256
-5
@@ -68,6 +68,16 @@ const selectMailProvider = document.getElementById('select-mail-provider');
|
||||
const btnMailLogin = document.getElementById('btn-mail-login');
|
||||
const rowEmailGenerator = document.getElementById('row-email-generator');
|
||||
const selectEmailGenerator = document.getElementById('select-email-generator');
|
||||
const rowTempEmailBaseUrl = document.getElementById('row-temp-email-base-url');
|
||||
const inputTempEmailBaseUrl = document.getElementById('input-temp-email-base-url');
|
||||
const rowTempEmailAdminAuth = document.getElementById('row-temp-email-admin-auth');
|
||||
const inputTempEmailAdminAuth = document.getElementById('input-temp-email-admin-auth');
|
||||
const rowTempEmailCustomAuth = document.getElementById('row-temp-email-custom-auth');
|
||||
const inputTempEmailCustomAuth = document.getElementById('input-temp-email-custom-auth');
|
||||
const rowTempEmailDomain = document.getElementById('row-temp-email-domain');
|
||||
const selectTempEmailDomain = document.getElementById('select-temp-email-domain');
|
||||
const inputTempEmailDomain = document.getElementById('input-temp-email-domain');
|
||||
const btnTempEmailDomainMode = document.getElementById('btn-temp-email-domain-mode');
|
||||
const hotmailSection = document.getElementById('hotmail-section');
|
||||
const rowHotmailServiceMode = document.getElementById('row-hotmail-service-mode');
|
||||
const hotmailServiceModeButtons = Array.from(document.querySelectorAll('[data-hotmail-service-mode]'));
|
||||
@@ -163,6 +173,7 @@ let settingsDirty = false;
|
||||
let settingsSaveInFlight = false;
|
||||
let settingsAutoSaveTimer = null;
|
||||
let cloudflareDomainEditMode = false;
|
||||
let cloudflareTempEmailDomainEditMode = false;
|
||||
let modalChoiceResolver = null;
|
||||
let currentModalActions = [];
|
||||
let modalResultBuilder = null;
|
||||
@@ -190,14 +201,22 @@ const MAIL_PROVIDER_LOGIN_CONFIGS = {
|
||||
'163': {
|
||||
label: '163 邮箱',
|
||||
url: 'https://mail.163.com/',
|
||||
buttonLabel: '登录',
|
||||
},
|
||||
'163-vip': {
|
||||
label: '163 VIP 邮箱',
|
||||
url: 'https://vip.163.com/',
|
||||
url: 'https://webmail.vip.163.com/',
|
||||
buttonLabel: '登录',
|
||||
},
|
||||
qq: {
|
||||
label: 'QQ 邮箱',
|
||||
url: 'https://wx.mail.qq.com/',
|
||||
buttonLabel: '登录',
|
||||
},
|
||||
'cloudflare-temp-email': {
|
||||
label: 'Cloudflare Temp Email GitHub',
|
||||
url: 'https://github.com/dreamhunter2333/cloudflare_temp_email',
|
||||
buttonLabel: 'GitHub',
|
||||
},
|
||||
'2925': {
|
||||
label: '2925 邮箱',
|
||||
@@ -859,6 +878,37 @@ function normalizeCloudflareDomains(values = []) {
|
||||
return domains;
|
||||
}
|
||||
|
||||
function normalizeCloudflareTempEmailBaseUrlValue(value = '') {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) return '';
|
||||
const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(raw) ? raw : `https://${raw}`;
|
||||
try {
|
||||
const parsed = new URL(candidate);
|
||||
parsed.hash = '';
|
||||
parsed.search = '';
|
||||
const pathname = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/+$/, '');
|
||||
return `${parsed.origin}${pathname}`;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCloudflareTempEmailDomainValue(value = '') {
|
||||
return normalizeCloudflareDomainValue(value);
|
||||
}
|
||||
|
||||
function normalizeCloudflareTempEmailDomains(values = []) {
|
||||
const seen = new Set();
|
||||
const domains = [];
|
||||
for (const value of Array.isArray(values) ? values : []) {
|
||||
const normalized = normalizeCloudflareTempEmailDomainValue(value);
|
||||
if (!normalized || seen.has(normalized)) continue;
|
||||
seen.add(normalized);
|
||||
domains.push(normalized);
|
||||
}
|
||||
return domains;
|
||||
}
|
||||
|
||||
function getCloudflareDomainsFromState() {
|
||||
const domains = normalizeCloudflareDomains(latestState?.cloudflareDomains || []);
|
||||
const activeDomain = normalizeCloudflareDomainValue(latestState?.cloudflareDomain || '');
|
||||
@@ -868,6 +918,15 @@ function getCloudflareDomainsFromState() {
|
||||
return { domains, activeDomain: activeDomain || domains[0] || '' };
|
||||
}
|
||||
|
||||
function getCloudflareTempEmailDomainsFromState() {
|
||||
const domains = normalizeCloudflareTempEmailDomains(latestState?.cloudflareTempEmailDomains || []);
|
||||
const activeDomain = normalizeCloudflareTempEmailDomainValue(latestState?.cloudflareTempEmailDomain || '');
|
||||
if (activeDomain && !domains.includes(activeDomain)) {
|
||||
domains.unshift(activeDomain);
|
||||
}
|
||||
return { domains, activeDomain: activeDomain || domains[0] || '' };
|
||||
}
|
||||
|
||||
function renderCloudflareDomainOptions(preferredDomain = '') {
|
||||
const preferred = normalizeCloudflareDomainValue(preferredDomain);
|
||||
const { domains, activeDomain } = getCloudflareDomainsFromState();
|
||||
@@ -894,6 +953,32 @@ function renderCloudflareDomainOptions(preferredDomain = '') {
|
||||
selectCfDomain.value = domains.includes(selected) ? selected : domains[0];
|
||||
}
|
||||
|
||||
function renderCloudflareTempEmailDomainOptions(preferredDomain = '') {
|
||||
const preferred = normalizeCloudflareTempEmailDomainValue(preferredDomain);
|
||||
const { domains, activeDomain } = getCloudflareTempEmailDomainsFromState();
|
||||
const selected = preferred || activeDomain;
|
||||
|
||||
selectTempEmailDomain.innerHTML = '';
|
||||
if (domains.length === 0) {
|
||||
const option = document.createElement('option');
|
||||
option.value = '';
|
||||
option.textContent = '请先添加域名';
|
||||
selectTempEmailDomain.appendChild(option);
|
||||
selectTempEmailDomain.disabled = true;
|
||||
selectTempEmailDomain.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const domain of domains) {
|
||||
const option = document.createElement('option');
|
||||
option.value = domain;
|
||||
option.textContent = domain;
|
||||
selectTempEmailDomain.appendChild(option);
|
||||
}
|
||||
selectTempEmailDomain.disabled = false;
|
||||
selectTempEmailDomain.value = domains.includes(selected) ? selected : domains[0];
|
||||
}
|
||||
|
||||
function setCloudflareDomainEditMode(editing, options = {}) {
|
||||
const { clearInput = false } = options;
|
||||
cloudflareDomainEditMode = Boolean(editing);
|
||||
@@ -910,11 +995,31 @@ function setCloudflareDomainEditMode(editing, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function setCloudflareTempEmailDomainEditMode(editing, options = {}) {
|
||||
const { clearInput = false } = options;
|
||||
cloudflareTempEmailDomainEditMode = Boolean(editing);
|
||||
selectTempEmailDomain.style.display = cloudflareTempEmailDomainEditMode ? 'none' : '';
|
||||
inputTempEmailDomain.style.display = cloudflareTempEmailDomainEditMode ? '' : 'none';
|
||||
btnTempEmailDomainMode.textContent = cloudflareTempEmailDomainEditMode ? '保存' : '添加';
|
||||
if (cloudflareTempEmailDomainEditMode) {
|
||||
if (clearInput) {
|
||||
inputTempEmailDomain.value = '';
|
||||
}
|
||||
inputTempEmailDomain.focus();
|
||||
} else if (clearInput) {
|
||||
inputTempEmailDomain.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function collectSettingsPayload() {
|
||||
const { domains, activeDomain } = getCloudflareDomainsFromState();
|
||||
const selectedCloudflareDomain = normalizeCloudflareDomainValue(
|
||||
!cloudflareDomainEditMode ? selectCfDomain.value : activeDomain
|
||||
) || activeDomain;
|
||||
const { domains: tempEmailDomains, activeDomain: tempEmailActiveDomain } = getCloudflareTempEmailDomainsFromState();
|
||||
const selectedCloudflareTempEmailDomain = normalizeCloudflareTempEmailDomainValue(
|
||||
!cloudflareTempEmailDomainEditMode ? selectTempEmailDomain.value : tempEmailActiveDomain
|
||||
) || tempEmailActiveDomain;
|
||||
return {
|
||||
panelMode: selectPanelMode.value,
|
||||
vpsUrl: inputVpsUrl.value.trim(),
|
||||
@@ -935,6 +1040,11 @@ function collectSettingsPayload() {
|
||||
hotmailLocalBaseUrl: inputHotmailLocalBaseUrl.value.trim(),
|
||||
cloudflareDomain: selectedCloudflareDomain,
|
||||
cloudflareDomains: domains,
|
||||
cloudflareTempEmailBaseUrl: normalizeCloudflareTempEmailBaseUrlValue(inputTempEmailBaseUrl.value),
|
||||
cloudflareTempEmailAdminAuth: inputTempEmailAdminAuth.value,
|
||||
cloudflareTempEmailCustomAuth: inputTempEmailCustomAuth.value,
|
||||
cloudflareTempEmailDomain: selectedCloudflareTempEmailDomain,
|
||||
cloudflareTempEmailDomains: tempEmailDomains,
|
||||
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
||||
autoRunFallbackThreadIntervalMinutes: normalizeAutoRunThreadIntervalMinutes(inputAutoSkipFailuresThreadIntervalMinutes.value),
|
||||
autoRunDelayEnabled: inputAutoDelayEnabled.checked,
|
||||
@@ -1206,22 +1316,30 @@ function applySettingsState(state) {
|
||||
inputSub2ApiPassword.value = state?.sub2apiPassword || '';
|
||||
inputSub2ApiGroup.value = state?.sub2apiGroupName || '';
|
||||
const restoredMailProvider = isCustomMailProvider(state?.mailProvider)
|
||||
|| ['hotmail-api', '163', '163-vip', 'qq', 'inbucket', '2925'].includes(String(state?.mailProvider || '').trim())
|
||||
|| ['hotmail-api', '163', '163-vip', 'qq', 'inbucket', '2925', 'cloudflare-temp-email'].includes(String(state?.mailProvider || '').trim())
|
||||
? String(state?.mailProvider || '163').trim()
|
||||
: (String(state?.emailGenerator || '').trim().toLowerCase() === 'custom'
|
||||
|| String(state?.emailGenerator || '').trim().toLowerCase() === 'manual'
|
||||
? 'custom'
|
||||
: '163');
|
||||
selectMailProvider.value = restoredMailProvider;
|
||||
selectEmailGenerator.value = String(state?.emailGenerator || '').trim().toLowerCase() === 'cloudflare' ? 'cloudflare' : 'duck';
|
||||
const restoredEmailGenerator = String(state?.emailGenerator || '').trim().toLowerCase();
|
||||
selectEmailGenerator.value = ['cloudflare', 'cloudflare-temp-email'].includes(restoredEmailGenerator)
|
||||
? restoredEmailGenerator
|
||||
: 'duck';
|
||||
inputEmailPrefix.value = state?.emailPrefix || '';
|
||||
inputInbucketHost.value = state?.inbucketHost || '';
|
||||
inputInbucketMailbox.value = state?.inbucketMailbox || '';
|
||||
setHotmailServiceMode(state?.hotmailServiceMode);
|
||||
inputHotmailRemoteBaseUrl.value = state?.hotmailRemoteBaseUrl || '';
|
||||
inputHotmailLocalBaseUrl.value = state?.hotmailLocalBaseUrl || '';
|
||||
inputTempEmailBaseUrl.value = state?.cloudflareTempEmailBaseUrl || '';
|
||||
inputTempEmailAdminAuth.value = state?.cloudflareTempEmailAdminAuth || '';
|
||||
inputTempEmailCustomAuth.value = state?.cloudflareTempEmailCustomAuth || '';
|
||||
renderCloudflareDomainOptions(state?.cloudflareDomain || '');
|
||||
setCloudflareDomainEditMode(false, { clearInput: true });
|
||||
renderCloudflareTempEmailDomainOptions(state?.cloudflareTempEmailDomain || '');
|
||||
setCloudflareTempEmailDomainEditMode(false, { clearInput: true });
|
||||
inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures);
|
||||
inputAutoSkipFailuresThreadIntervalMinutes.value = String(normalizeAutoRunThreadIntervalMinutes(state?.autoRunFallbackThreadIntervalMinutes));
|
||||
inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
|
||||
@@ -1488,13 +1606,18 @@ function isCustomMailProvider(provider = selectMailProvider.value) {
|
||||
|
||||
function getSelectedEmailGenerator() {
|
||||
const generator = String(selectEmailGenerator.value || '').trim().toLowerCase();
|
||||
if (generator === 'cloudflare') {
|
||||
return 'cloudflare';
|
||||
if (generator === 'custom' || generator === 'manual') {
|
||||
return 'custom';
|
||||
}
|
||||
if (generator === 'cloudflare') return 'cloudflare';
|
||||
if (generator === 'cloudflare-temp-email') return 'cloudflare-temp-email';
|
||||
return 'duck';
|
||||
}
|
||||
|
||||
function getEmailGeneratorUiCopy() {
|
||||
if (getSelectedEmailGenerator() === 'custom') {
|
||||
return getCustomMailProviderUiCopy();
|
||||
}
|
||||
if (getSelectedEmailGenerator() === 'cloudflare') {
|
||||
return {
|
||||
buttonLabel: '生成',
|
||||
@@ -1503,6 +1626,14 @@ function getEmailGeneratorUiCopy() {
|
||||
label: 'Cloudflare 邮箱',
|
||||
};
|
||||
}
|
||||
if (getSelectedEmailGenerator() === 'cloudflare-temp-email') {
|
||||
return {
|
||||
buttonLabel: '生成 Temp',
|
||||
placeholder: '点击生成 Cloudflare Temp Email,或手动粘贴邮箱',
|
||||
successVerb: '生成',
|
||||
label: 'Cloudflare Temp Email',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
buttonLabel: '获取',
|
||||
@@ -1591,6 +1722,7 @@ function updateMailLoginButtonState() {
|
||||
const config = getMailProviderLoginConfig();
|
||||
const loginUrl = getMailProviderLoginUrl();
|
||||
btnMailLogin.disabled = !loginUrl;
|
||||
btnMailLogin.textContent = config?.buttonLabel || '登录';
|
||||
btnMailLogin.title = loginUrl ? `打开 ${config.label} 登录页` : '当前邮箱服务没有可跳转的登录页';
|
||||
}
|
||||
|
||||
@@ -1808,13 +1940,17 @@ function updateMailProviderUI() {
|
||||
const useHotmail = selectMailProvider.value === 'hotmail-api';
|
||||
const useCustomEmail = isCustomMailProvider();
|
||||
const useEmailGenerator = !useHotmail && !useGeneratedAlias && !useCustomEmail;
|
||||
const useCloudflareTempEmailProvider = selectMailProvider.value === 'cloudflare-temp-email';
|
||||
updateMailLoginButtonState();
|
||||
rowEmailPrefix.style.display = useGeneratedAlias ? '' : 'none';
|
||||
const hotmailServiceMode = getSelectedHotmailServiceMode();
|
||||
rowInbucketHost.style.display = useInbucket ? '' : 'none';
|
||||
rowInbucketMailbox.style.display = useInbucket ? '' : 'none';
|
||||
const useCloudflare = selectEmailGenerator.value === 'cloudflare';
|
||||
const useCloudflareTempEmailGenerator = selectEmailGenerator.value === 'cloudflare-temp-email';
|
||||
const showCloudflareDomain = useEmailGenerator && useCloudflare;
|
||||
const showCloudflareTempEmailSettings = useCloudflareTempEmailProvider || (useEmailGenerator && useCloudflareTempEmailGenerator);
|
||||
const showCloudflareTempEmailDomain = useEmailGenerator && useCloudflareTempEmailGenerator;
|
||||
if (rowEmailGenerator) {
|
||||
rowEmailGenerator.style.display = useEmailGenerator ? '' : 'none';
|
||||
}
|
||||
@@ -1825,6 +1961,16 @@ function updateMailProviderUI() {
|
||||
} else {
|
||||
setCloudflareDomainEditMode(false, { clearInput: false });
|
||||
}
|
||||
rowTempEmailBaseUrl.style.display = showCloudflareTempEmailSettings ? '' : 'none';
|
||||
rowTempEmailAdminAuth.style.display = showCloudflareTempEmailSettings ? '' : 'none';
|
||||
rowTempEmailCustomAuth.style.display = showCloudflareTempEmailSettings ? '' : 'none';
|
||||
rowTempEmailDomain.style.display = showCloudflareTempEmailDomain ? '' : 'none';
|
||||
const { domains: tempEmailDomains } = getCloudflareTempEmailDomainsFromState();
|
||||
if (showCloudflareTempEmailDomain) {
|
||||
setCloudflareTempEmailDomainEditMode(cloudflareTempEmailDomainEditMode || tempEmailDomains.length === 0, { clearInput: false });
|
||||
} else {
|
||||
setCloudflareTempEmailDomainEditMode(false, { clearInput: false });
|
||||
}
|
||||
|
||||
if (hotmailSection) {
|
||||
hotmailSection.style.display = useHotmail ? '' : 'none';
|
||||
@@ -1896,6 +2042,38 @@ async function saveCloudflareDomainSettings(domains, activeDomain, options = {})
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCloudflareTempEmailDomainSettings(domains, activeDomain, options = {}) {
|
||||
const { silent = false } = options;
|
||||
const normalizedDomains = normalizeCloudflareTempEmailDomains(domains);
|
||||
const normalizedActiveDomain = normalizeCloudflareTempEmailDomainValue(activeDomain) || normalizedDomains[0] || '';
|
||||
const payload = {
|
||||
cloudflareTempEmailDomain: normalizedActiveDomain,
|
||||
cloudflareTempEmailDomains: normalizedDomains,
|
||||
};
|
||||
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'SAVE_SETTING',
|
||||
source: 'sidepanel',
|
||||
payload,
|
||||
});
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
syncLatestState({
|
||||
...payload,
|
||||
});
|
||||
renderCloudflareTempEmailDomainOptions(normalizedActiveDomain);
|
||||
setCloudflareTempEmailDomainEditMode(false, { clearInput: true });
|
||||
markSettingsDirty(false);
|
||||
updateMailProviderUI();
|
||||
|
||||
if (!silent) {
|
||||
showToast('Cloudflare Temp Email 域名已保存', 'success', 1800);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePanelModeUI() {
|
||||
const useSub2Api = selectPanelMode.value === 'sub2api';
|
||||
rowVpsUrl.style.display = useSub2Api ? 'none' : '';
|
||||
@@ -3038,6 +3216,14 @@ selectCfDomain.addEventListener('change', () => {
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
selectTempEmailDomain.addEventListener('change', () => {
|
||||
if (selectTempEmailDomain.disabled) {
|
||||
return;
|
||||
}
|
||||
markSettingsDirty(true);
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
btnCfDomainMode.addEventListener('click', async () => {
|
||||
try {
|
||||
if (!cloudflareDomainEditMode) {
|
||||
@@ -3059,6 +3245,27 @@ btnCfDomainMode.addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
btnTempEmailDomainMode.addEventListener('click', async () => {
|
||||
try {
|
||||
if (!cloudflareTempEmailDomainEditMode) {
|
||||
setCloudflareTempEmailDomainEditMode(true, { clearInput: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const newDomain = normalizeCloudflareTempEmailDomainValue(inputTempEmailDomain.value);
|
||||
if (!newDomain) {
|
||||
showToast('请输入有效的 Cloudflare Temp Email 域名。', 'warn');
|
||||
inputTempEmailDomain.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const { domains } = getCloudflareTempEmailDomainsFromState();
|
||||
await saveCloudflareTempEmailDomainSettings([...domains, newDomain], newDomain);
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
inputCfDomain.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
@@ -3066,6 +3273,13 @@ inputCfDomain.addEventListener('keydown', (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
inputTempEmailDomain.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
btnTempEmailDomainMode.click();
|
||||
}
|
||||
});
|
||||
|
||||
inputSub2ApiUrl.addEventListener('input', () => {
|
||||
markSettingsDirty(true);
|
||||
scheduleSettingsAutoSave();
|
||||
@@ -3147,6 +3361,31 @@ inputAutoSkipFailures.addEventListener('change', async () => {
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
inputTempEmailBaseUrl.addEventListener('input', () => {
|
||||
markSettingsDirty(true);
|
||||
scheduleSettingsAutoSave();
|
||||
});
|
||||
inputTempEmailBaseUrl.addEventListener('blur', () => {
|
||||
inputTempEmailBaseUrl.value = normalizeCloudflareTempEmailBaseUrlValue(inputTempEmailBaseUrl.value);
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
inputTempEmailAdminAuth.addEventListener('input', () => {
|
||||
markSettingsDirty(true);
|
||||
scheduleSettingsAutoSave();
|
||||
});
|
||||
inputTempEmailAdminAuth.addEventListener('blur', () => {
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
inputTempEmailCustomAuth.addEventListener('input', () => {
|
||||
markSettingsDirty(true);
|
||||
scheduleSettingsAutoSave();
|
||||
});
|
||||
inputTempEmailCustomAuth.addEventListener('blur', () => {
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
inputAutoSkipFailuresThreadIntervalMinutes.addEventListener('input', () => {
|
||||
markSettingsDirty(true);
|
||||
scheduleSettingsAutoSave();
|
||||
@@ -3302,6 +3541,18 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
displayLocalhostUrl.textContent = message.payload.localhostUrl || '等待中...';
|
||||
displayLocalhostUrl.classList.toggle('has-value', Boolean(message.payload.localhostUrl));
|
||||
}
|
||||
if (message.payload.cloudflareTempEmailBaseUrl !== undefined) {
|
||||
inputTempEmailBaseUrl.value = message.payload.cloudflareTempEmailBaseUrl || '';
|
||||
}
|
||||
if (message.payload.cloudflareTempEmailAdminAuth !== undefined) {
|
||||
inputTempEmailAdminAuth.value = message.payload.cloudflareTempEmailAdminAuth || '';
|
||||
}
|
||||
if (message.payload.cloudflareTempEmailCustomAuth !== undefined) {
|
||||
inputTempEmailCustomAuth.value = message.payload.cloudflareTempEmailCustomAuth || '';
|
||||
}
|
||||
if (message.payload.cloudflareTempEmailDomain !== undefined || message.payload.cloudflareTempEmailDomains !== undefined) {
|
||||
renderCloudflareTempEmailDomainOptions(message.payload.cloudflareTempEmailDomain || latestState?.cloudflareTempEmailDomain || '');
|
||||
}
|
||||
if (message.payload.currentHotmailAccountId !== undefined || message.payload.hotmailAccounts !== undefined) {
|
||||
renderHotmailAccounts();
|
||||
if (selectMailProvider.value === 'hotmail-api') {
|
||||
|
||||
@@ -60,11 +60,19 @@ const bundle = [
|
||||
extractFunction('hasSavedProgress'),
|
||||
extractFunction('getRunningSteps'),
|
||||
extractFunction('getAutoRunStatusPayload'),
|
||||
extractFunction('createAutoRunRoundSummary'),
|
||||
extractFunction('normalizeAutoRunRoundSummary'),
|
||||
extractFunction('buildAutoRunRoundSummaries'),
|
||||
extractFunction('serializeAutoRunRoundSummaries'),
|
||||
extractFunction('getAutoRunRoundRetryCount'),
|
||||
extractFunction('formatAutoRunFailureReasons'),
|
||||
extractFunction('logAutoRunFinalSummary'),
|
||||
extractFunction('autoRunLoop'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const STOP_ERROR_MESSAGE = 'Flow stopped.';
|
||||
const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
|
||||
const DEFAULT_STATE = {
|
||||
stepStatuses: {
|
||||
1: 'pending',
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for function ${name}`);
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const ch = source[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('pollCloudflareTempEmailVerificationCode returns code even if delete fails', async () => {
|
||||
const bundle = [
|
||||
extractFunction('isStopError'),
|
||||
extractFunction('throwIfStopped'),
|
||||
extractFunction('summarizeCloudflareTempEmailMessagesForLog'),
|
||||
extractFunction('pollCloudflareTempEmailVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let stopRequested = false;
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
const CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE = 20;
|
||||
const logs = [];
|
||||
function normalizeCloudflareTempEmailAddress(value) {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
async function addLog(message, level) {
|
||||
logs.push({ message, level });
|
||||
}
|
||||
async function sleepWithStop() {}
|
||||
async function listCloudflareTempEmailMessages() {
|
||||
return {
|
||||
config: {},
|
||||
messages: [{
|
||||
id: 'mail-1',
|
||||
address: 'user@example.com',
|
||||
receivedDateTime: '2026-04-13T09:20:00.000Z',
|
||||
subject: 'OpenAI verification code',
|
||||
from: { emailAddress: { address: 'noreply@tm.openai.com' } },
|
||||
bodyPreview: 'Your verification code is 123456.',
|
||||
}],
|
||||
};
|
||||
}
|
||||
function pickVerificationMessageWithTimeFallback(messages) {
|
||||
return {
|
||||
match: {
|
||||
code: '123456',
|
||||
receivedAt: Date.parse(messages[0].receivedDateTime),
|
||||
message: messages[0],
|
||||
},
|
||||
usedRelaxedFilters: false,
|
||||
usedTimeFallback: false,
|
||||
};
|
||||
}
|
||||
async function deleteCloudflareTempEmailMail() {
|
||||
throw new Error('delete failed');
|
||||
}
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
pollCloudflareTempEmailVerificationCode,
|
||||
snapshot() {
|
||||
return { logs };
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const result = await api.pollCloudflareTempEmailVerificationCode(4, { email: 'user@example.com' }, {
|
||||
targetEmail: 'user@example.com',
|
||||
maxAttempts: 1,
|
||||
intervalMs: 1,
|
||||
});
|
||||
|
||||
assert.equal(result.code, '123456');
|
||||
const state = api.snapshot();
|
||||
assert.equal(state.logs.some((entry) => entry.message.includes('删除 Cloudflare Temp Email 邮件失败')), true);
|
||||
});
|
||||
|
||||
test('pollCloudflareTempEmailVerificationCode requires target email', async () => {
|
||||
const bundle = [
|
||||
extractFunction('isStopError'),
|
||||
extractFunction('throwIfStopped'),
|
||||
extractFunction('pollCloudflareTempEmailVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let stopRequested = false;
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
const CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE = 20;
|
||||
function normalizeCloudflareTempEmailAddress(value) {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
async function addLog() {}
|
||||
async function sleepWithStop() {}
|
||||
async function listCloudflareTempEmailMessages() {
|
||||
throw new Error('should not reach list');
|
||||
}
|
||||
function pickVerificationMessageWithTimeFallback() {
|
||||
return { match: null, usedRelaxedFilters: false, usedTimeFallback: false };
|
||||
}
|
||||
async function deleteCloudflareTempEmailMail() {}
|
||||
function summarizeCloudflareTempEmailMessagesForLog() {
|
||||
return '';
|
||||
}
|
||||
|
||||
${bundle}
|
||||
|
||||
return { pollCloudflareTempEmailVerificationCode };
|
||||
`)();
|
||||
|
||||
await assert.rejects(
|
||||
api.pollCloudflareTempEmailVerificationCode(4, {}, {}),
|
||||
/缺少目标邮箱地址/
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
buildCloudflareTempEmailHeaders,
|
||||
getCloudflareTempEmailAddressFromResponse,
|
||||
normalizeCloudflareTempEmailBaseUrl,
|
||||
normalizeCloudflareTempEmailDomain,
|
||||
normalizeCloudflareTempEmailDomains,
|
||||
normalizeCloudflareTempEmailMailApiMessages,
|
||||
} = require('../cloudflare-temp-email-utils.js');
|
||||
|
||||
test('normalizeCloudflareTempEmailBaseUrl normalizes host and preserves path', () => {
|
||||
assert.equal(
|
||||
normalizeCloudflareTempEmailBaseUrl('temp.example.com/api/'),
|
||||
'https://temp.example.com/api'
|
||||
);
|
||||
assert.equal(
|
||||
normalizeCloudflareTempEmailBaseUrl('http://127.0.0.1:8787'),
|
||||
'http://127.0.0.1:8787'
|
||||
);
|
||||
assert.equal(normalizeCloudflareTempEmailBaseUrl('::::'), '');
|
||||
});
|
||||
|
||||
test('normalizeCloudflareTempEmailDomain and domains de-duplicate valid entries', () => {
|
||||
assert.equal(normalizeCloudflareTempEmailDomain('@Mail.Example.com'), 'mail.example.com');
|
||||
assert.equal(normalizeCloudflareTempEmailDomain('not-a-domain'), '');
|
||||
assert.deepEqual(
|
||||
normalizeCloudflareTempEmailDomains(['mail.example.com', 'MAIL.EXAMPLE.COM', 'bad-value']),
|
||||
['mail.example.com']
|
||||
);
|
||||
});
|
||||
|
||||
test('buildCloudflareTempEmailHeaders includes auth headers and content type when needed', () => {
|
||||
assert.deepEqual(
|
||||
buildCloudflareTempEmailHeaders(
|
||||
{
|
||||
adminAuth: 'admin-secret',
|
||||
customAuth: 'site-secret',
|
||||
},
|
||||
{ json: true }
|
||||
),
|
||||
{
|
||||
'x-admin-auth': 'admin-secret',
|
||||
'x-custom-auth': 'site-secret',
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('normalizeCloudflareTempEmailMailApiMessages extracts sender, subject, code, and address from raw mime', () => {
|
||||
const messages = normalizeCloudflareTempEmailMailApiMessages({
|
||||
data: [
|
||||
{
|
||||
id: 'mail-1',
|
||||
address: 'user@example.com',
|
||||
created_at: '2026-04-13T09:15:00.000Z',
|
||||
raw: [
|
||||
'From: OpenAI <noreply@tm.openai.com>',
|
||||
'Subject: =?UTF-8?B?T3BlbkFJIHZlcmlmaWNhdGlvbiBjb2Rl?=',
|
||||
'Content-Type: text/plain; charset=UTF-8',
|
||||
'',
|
||||
'Your verification code is 654321.',
|
||||
].join('\r\n'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(messages.length, 1);
|
||||
assert.equal(messages[0].id, 'mail-1');
|
||||
assert.equal(messages[0].address, 'user@example.com');
|
||||
assert.equal(messages[0].subject, 'OpenAI verification code');
|
||||
assert.equal(messages[0].from.emailAddress.address, 'OpenAI <noreply@tm.openai.com>');
|
||||
assert.match(messages[0].bodyPreview, /654321/);
|
||||
});
|
||||
|
||||
test('normalizeCloudflareTempEmailMailApiMessages decodes multipart quoted printable html bodies', () => {
|
||||
const messages = normalizeCloudflareTempEmailMailApiMessages([
|
||||
{
|
||||
id: 'mail-2',
|
||||
address: 'user@example.com',
|
||||
received_at: '2026-04-13T09:20:00.000Z',
|
||||
source: [
|
||||
'From: ChatGPT <noreply@tm.openai.com>',
|
||||
'Subject: Login code',
|
||||
'Content-Type: multipart/alternative; boundary="abc123"',
|
||||
'',
|
||||
'--abc123',
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
'Content-Transfer-Encoding: quoted-printable',
|
||||
'',
|
||||
'<p>Your login code is <strong>112233</strong>.</p>',
|
||||
'--abc123--',
|
||||
].join('\r\n'),
|
||||
},
|
||||
]);
|
||||
|
||||
assert.equal(messages.length, 1);
|
||||
assert.match(messages[0].bodyPreview, /112233/);
|
||||
assert.equal(messages[0].subject, 'Login code');
|
||||
});
|
||||
|
||||
test('getCloudflareTempEmailAddressFromResponse supports direct and nested response shapes', () => {
|
||||
assert.equal(getCloudflareTempEmailAddressFromResponse({ address: 'one@example.com' }), 'one@example.com');
|
||||
assert.equal(getCloudflareTempEmailAddressFromResponse({ data: { address: 'two@example.com' } }), 'two@example.com');
|
||||
});
|
||||
@@ -52,7 +52,11 @@ function extractFunction(name) {
|
||||
|
||||
const bundle = [
|
||||
extractFunction('getTabRegistry'),
|
||||
extractFunction('normalizeEmailGenerator'),
|
||||
extractFunction('parseUrlSafely'),
|
||||
extractFunction('isHotmailProvider'),
|
||||
extractFunction('isGeneratedAliasProvider'),
|
||||
extractFunction('shouldUseCustomRegistrationEmail'),
|
||||
extractFunction('isLocalhostOAuthCallbackUrl'),
|
||||
extractFunction('isLocalhostOAuthCallbackTabMatch'),
|
||||
extractFunction('closeLocalhostCallbackTabs'),
|
||||
@@ -62,6 +66,9 @@ const bundle = [
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const HOTMAIL_PROVIDER = 'hotmail-api';
|
||||
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
|
||||
const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
|
||||
let currentState = {
|
||||
tabRegistry: {
|
||||
'signup-page': { tabId: 1, ready: true },
|
||||
|
||||
@@ -57,10 +57,11 @@ async function testPollFreshVerificationCodeRethrowsStop() {
|
||||
extractFunction('pollFreshVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const api = new Function(`
|
||||
let stopRequested = false;
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
const HOTMAIL_PROVIDER = 'hotmail-api';
|
||||
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
|
||||
const VERIFICATION_POLL_MAX_ROUNDS = 5;
|
||||
const logs = [];
|
||||
let resendCalls = 0;
|
||||
@@ -119,9 +120,10 @@ async function testResolveVerificationStepRethrowsStopFromFreshRequest() {
|
||||
extractFunction('resolveVerificationStep'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const api = new Function(`
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
const HOTMAIL_PROVIDER = 'hotmail-api';
|
||||
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
|
||||
const logs = [];
|
||||
let pollCalls = 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user