feat: 支持从 CFTE 同步 Temp 域名列表

This commit is contained in:
QLHazyCoder
2026-05-15 02:52:48 +08:00
parent d681972724
commit a7ea261093
3 changed files with 365 additions and 27 deletions
+2 -2
View File
@@ -725,14 +725,14 @@
<div id="temp-email-domain-picker" class="editable-list-picker">
<button id="btn-temp-email-domain-menu" class="data-select editable-list-trigger" type="button"
aria-haspopup="listbox" aria-expanded="false">
<span id="temp-email-domain-current" class="truncate">请先添加域名</span>
<span id="temp-email-domain-current" class="truncate">请先更新域名</span>
<span class="editable-list-caret" aria-hidden="true"></span>
</button>
<div id="temp-email-domain-menu" class="editable-list-menu" role="listbox" hidden></div>
</div>
<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>
<button id="btn-temp-email-domain-mode" class="btn btn-outline btn-sm" type="button">更新</button>
</div>
</div>
</div>
+153 -25
View File
@@ -1310,7 +1310,7 @@ const tempEmailDomainPicker = createEditableListPicker({
trigger: btnTempEmailDomainMenu,
current: tempEmailDomainCurrent,
menu: tempEmailDomainMenu,
emptyLabel: '请先添加域名',
emptyLabel: '请先更新域名',
itemLabel: '域名',
normalizeItems: normalizeCloudflareTempEmailDomains,
normalizeValue: normalizeCloudflareTempEmailDomainValue,
@@ -2981,16 +2981,11 @@ function setCloudflareDomainEditMode(editing, options = {}) {
function setCloudflareTempEmailDomainEditMode(editing, options = {}) {
const { clearInput = false } = options;
cloudflareTempEmailDomainEditMode = Boolean(editing);
tempEmailDomainPicker.setVisible(!cloudflareTempEmailDomainEditMode);
inputTempEmailDomain.style.display = cloudflareTempEmailDomainEditMode ? '' : 'none';
btnTempEmailDomainMode.textContent = cloudflareTempEmailDomainEditMode ? '保存' : '添加';
if (cloudflareTempEmailDomainEditMode) {
if (clearInput) {
inputTempEmailDomain.value = '';
}
inputTempEmailDomain.focus();
} else if (clearInput) {
cloudflareTempEmailDomainEditMode = false;
tempEmailDomainPicker.setVisible(true);
inputTempEmailDomain.style.display = 'none';
btnTempEmailDomainMode.textContent = '更新';
if (clearInput) {
inputTempEmailDomain.value = '';
}
}
@@ -10427,6 +10422,152 @@ async function saveCloudflareTempEmailDomainSettings(domains, activeDomain, opti
}
}
function joinCloudflareTempEmailSettingsUrl(baseUrl, path) {
const normalizedBaseUrl = normalizeCloudflareTempEmailBaseUrlValue(baseUrl);
const normalizedPath = String(path || '').trim();
if (!normalizedBaseUrl || !normalizedPath) {
return normalizedBaseUrl || '';
}
return `${normalizedBaseUrl}${normalizedPath.startsWith('/') ? '' : '/'}${normalizedPath}`;
}
function buildCloudflareTempEmailSyncHeaders(options = {}) {
const { includeAdminAuth = false } = options;
const headers = {
Accept: 'application/json',
};
const customAuth = String(inputTempEmailCustomAuth?.value || '').trim();
if (customAuth) {
headers['x-custom-auth'] = customAuth;
}
if (includeAdminAuth) {
const adminAuth = String(inputTempEmailAdminAuth?.value || '').trim();
if (adminAuth) {
headers['x-admin-auth'] = adminAuth;
}
}
return headers;
}
async function requestCloudflareTempEmailSyncPayload(baseUrl, path, options = {}) {
const url = joinCloudflareTempEmailSettingsUrl(baseUrl, path);
if (!url) {
throw new Error('Cloudflare Temp Email 服务地址为空或格式无效。');
}
const response = await fetch(url, {
cache: 'no-store',
headers: buildCloudflareTempEmailSyncHeaders(options),
});
const rawText = await response.text();
let payload = rawText;
try {
payload = rawText ? JSON.parse(rawText) : {};
} catch {
payload = rawText;
}
if (!response.ok) {
const message = typeof payload === 'object' && payload
? (payload.message || payload.error || payload.msg)
: '';
throw new Error(message || `HTTP ${response.status}`);
}
return payload;
}
function mergeCloudflareTempEmailDomains(existingDomains, fetchedDomains) {
const mergedDomains = [];
const seen = new Set();
for (const domain of normalizeCloudflareTempEmailDomains(fetchedDomains)) {
if (seen.has(domain)) continue;
seen.add(domain);
mergedDomains.push(domain);
}
for (const domain of normalizeCloudflareTempEmailDomains(existingDomains)) {
if (seen.has(domain)) continue;
seen.add(domain);
mergedDomains.push(domain);
}
return mergedDomains;
}
async function fetchCloudflareTempEmailAvailableDomains(baseUrl) {
let openSettingsError = null;
try {
const payload = await requestCloudflareTempEmailSyncPayload(baseUrl, '/open_api/settings');
const domains = normalizeCloudflareTempEmailDomains(payload?.domains || []);
if (domains.length) {
return {
domains,
source: 'open_api/settings',
};
}
openSettingsError = new Error('公开设置未返回可用域名。');
} catch (error) {
openSettingsError = error;
}
const adminAuth = String(inputTempEmailAdminAuth?.value || '').trim();
if (!adminAuth) {
throw openSettingsError || new Error('未获取到可用域名。');
}
const payload = await requestCloudflareTempEmailSyncPayload(baseUrl, '/admin/worker/configs', {
includeAdminAuth: true,
});
const domains = normalizeCloudflareTempEmailDomains(payload?.DOMAINS || []);
if (!domains.length) {
throw openSettingsError || new Error('管理配置未返回可用域名。');
}
return {
domains,
source: 'admin/worker/configs',
};
}
async function syncCloudflareTempEmailDomainsFromService() {
const normalizedBaseUrl = normalizeCloudflareTempEmailBaseUrlValue(inputTempEmailBaseUrl?.value || '');
if (!normalizedBaseUrl) {
throw new Error('请先填写有效的 Cloudflare Temp Email 服务地址。');
}
const previousButtonText = btnTempEmailDomainMode.textContent;
btnTempEmailDomainMode.disabled = true;
btnTempEmailDomainMode.textContent = '更新中...';
try {
const { domains: fetchedDomains, source } = await fetchCloudflareTempEmailAvailableDomains(normalizedBaseUrl);
const { domains: existingDomains, activeDomain } = getCloudflareTempEmailDomainsFromState();
const currentDomain = normalizeCloudflareTempEmailDomainValue(selectTempEmailDomain.value || activeDomain);
const mergedDomains = mergeCloudflareTempEmailDomains(existingDomains, fetchedDomains);
const nextActiveDomain = mergedDomains.includes(currentDomain)
? currentDomain
: (fetchedDomains[0] || mergedDomains[0] || '');
const existingSet = new Set(normalizeCloudflareTempEmailDomains(existingDomains));
const addedCount = fetchedDomains.filter((domain) => !existingSet.has(domain)).length;
await saveCloudflareTempEmailDomainSettings(mergedDomains, nextActiveDomain, { silent: true });
if (addedCount > 0) {
showToast(`已更新 Cloudflare Temp Email 域名,新增 ${addedCount} 个。`, 'success', 2200);
} else {
showToast('已同步 Cloudflare Temp Email 域名,暂无新增项。', 'info', 2200);
}
return {
domains: mergedDomains,
activeDomain: nextActiveDomain,
source,
};
} catch (error) {
const message = error?.message || String(error || '未知错误');
throw new Error(`更新 Cloudflare Temp Email 域名失败:${message}`);
} finally {
btnTempEmailDomainMode.disabled = false;
btnTempEmailDomainMode.textContent = previousButtonText || '更新';
}
}
async function handleDeleteCloudflareDomain(domain) {
const targetDomain = normalizeCloudflareDomainValue(domain);
if (!targetDomain) {
@@ -12736,20 +12877,7 @@ 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);
await syncCloudflareTempEmailDomainsFromService();
} catch (err) {
showToast(err.message, 'error');
}
@@ -65,6 +65,7 @@ test('sidepanel html places cloudflare temp email controls in a standalone secti
assert.match(html, /id="btn-cloudflare-temp-email-github"/);
assert.match(html, /id="row-temp-email-random-subdomain-toggle"/);
assert.match(html, /id="input-temp-email-use-random-subdomain"/);
assert.match(html, /id="btn-temp-email-domain-mode"[^>]*>更新</);
assert.doesNotMatch(html, /id="row-temp-email-random-subdomain-domain"/);
});
@@ -173,6 +174,215 @@ return {
assert.deepEqual(api.calls.domainEditMode, [{ editing: false, options: { clearInput: true } }]);
});
test('setCloudflareTempEmailDomainEditMode keeps the selector visible and the button in update mode', () => {
const bundle = extractFunction('setCloudflareTempEmailDomainEditMode');
const api = new Function(`
let cloudflareTempEmailDomainEditMode = true;
const tempEmailDomainPicker = {
visibility: [],
setVisible(value) {
this.visibility.push(value);
},
};
const inputTempEmailDomain = { value: 'old.example.com', style: { display: '' } };
const btnTempEmailDomainMode = { textContent: '' };
${bundle}
return {
setCloudflareTempEmailDomainEditMode,
tempEmailDomainPicker,
inputTempEmailDomain,
btnTempEmailDomainMode,
getMode() {
return cloudflareTempEmailDomainEditMode;
},
};
`)();
api.setCloudflareTempEmailDomainEditMode(true, { clearInput: true });
assert.equal(api.getMode(), false);
assert.deepEqual(api.tempEmailDomainPicker.visibility, [true]);
assert.equal(api.inputTempEmailDomain.style.display, 'none');
assert.equal(api.inputTempEmailDomain.value, '');
assert.equal(api.btnTempEmailDomainMode.textContent, '更新');
});
test('syncCloudflareTempEmailDomainsFromService merges domains from open settings and preserves the current selection', async () => {
const bundle = [
extractFunction('joinCloudflareTempEmailSettingsUrl'),
extractFunction('buildCloudflareTempEmailSyncHeaders'),
extractFunction('requestCloudflareTempEmailSyncPayload'),
extractFunction('mergeCloudflareTempEmailDomains'),
extractFunction('fetchCloudflareTempEmailAvailableDomains'),
extractFunction('syncCloudflareTempEmailDomainsFromService'),
].join('\n');
const api = new Function(`
const fetchCalls = [];
const saveCalls = [];
const toastCalls = [];
const btnTempEmailDomainMode = { textContent: '更新', disabled: false };
const inputTempEmailBaseUrl = { value: 'https://temp.example.com' };
const inputTempEmailCustomAuth = { value: 'custom-secret' };
const inputTempEmailAdminAuth = { value: 'admin-secret' };
const selectTempEmailDomain = { value: 'old.example.com' };
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}\`;
const parsed = new URL(candidate);
parsed.hash = '';
parsed.search = '';
const pathname = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\\/+$/, '');
return \`\${parsed.origin}\${pathname}\`;
}
function normalizeCloudflareTempEmailDomainValue(value = '') {
let normalized = String(value || '').trim().toLowerCase();
if (!normalized) return '';
normalized = normalized.replace(/^@+/, '');
normalized = normalized.replace(/^https?:\\/\\//, '');
normalized = normalized.replace(/\\/.*$/, '');
return /^[a-z0-9.-]+\\.[a-z]{2,}$/i.test(normalized) ? normalized : '';
}
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;
}
async function fetch(url, options = {}) {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
async text() {
return JSON.stringify({
domains: ['new.example.com', 'old.example.com'],
});
},
};
}
function getCloudflareTempEmailDomainsFromState() {
return {
domains: ['old.example.com', 'local-only.example.com'],
activeDomain: 'old.example.com',
};
}
async function saveCloudflareTempEmailDomainSettings(domains, activeDomain, options) {
saveCalls.push({ domains, activeDomain, options });
}
function showToast(message, type, duration) {
toastCalls.push({ message, type, duration });
}
${bundle}
return {
syncCloudflareTempEmailDomainsFromService,
fetchCalls,
saveCalls,
toastCalls,
btnTempEmailDomainMode,
};
`)();
const result = await api.syncCloudflareTempEmailDomainsFromService();
assert.equal(result.source, 'open_api/settings');
assert.deepEqual(api.fetchCalls.map((item) => item.url), ['https://temp.example.com/open_api/settings']);
assert.deepEqual(api.saveCalls, [{
domains: ['new.example.com', 'old.example.com', 'local-only.example.com'],
activeDomain: 'old.example.com',
options: { silent: true },
}]);
assert.match(api.toastCalls[0].message, /新增 1 个/);
assert.equal(api.toastCalls[0].type, 'success');
assert.equal(api.btnTempEmailDomainMode.disabled, false);
assert.equal(api.btnTempEmailDomainMode.textContent, '更新');
});
test('fetchCloudflareTempEmailAvailableDomains falls back to admin worker configs when open settings has no domains', async () => {
const bundle = [
extractFunction('joinCloudflareTempEmailSettingsUrl'),
extractFunction('buildCloudflareTempEmailSyncHeaders'),
extractFunction('requestCloudflareTempEmailSyncPayload'),
extractFunction('fetchCloudflareTempEmailAvailableDomains'),
].join('\n');
const api = new Function(`
const fetchCalls = [];
const inputTempEmailCustomAuth = { value: 'custom-secret' };
const inputTempEmailAdminAuth = { value: 'admin-secret' };
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}\`;
const parsed = new URL(candidate);
parsed.hash = '';
parsed.search = '';
const pathname = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\\/+$/, '');
return \`\${parsed.origin}\${pathname}\`;
}
function normalizeCloudflareTempEmailDomainValue(value = '') {
let normalized = String(value || '').trim().toLowerCase();
if (!normalized) return '';
normalized = normalized.replace(/^@+/, '');
normalized = normalized.replace(/^https?:\\/\\//, '');
normalized = normalized.replace(/\\/.*$/, '');
return /^[a-z0-9.-]+\\.[a-z]{2,}$/i.test(normalized) ? normalized : '';
}
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;
}
async function fetch(url, options = {}) {
fetchCalls.push({ url, options });
if (url.endsWith('/open_api/settings')) {
return {
ok: true,
status: 200,
async text() {
return JSON.stringify({ domains: [] });
},
};
}
return {
ok: true,
status: 200,
async text() {
return JSON.stringify({ DOMAINS: ['admin.example.com'] });
},
};
}
${bundle}
return {
fetchCloudflareTempEmailAvailableDomains,
fetchCalls,
};
`)();
const result = await api.fetchCloudflareTempEmailAvailableDomains('https://temp.example.com');
assert.equal(result.source, 'admin/worker/configs');
assert.deepEqual(result.domains, ['admin.example.com']);
assert.deepEqual(api.fetchCalls.map((item) => item.url), [
'https://temp.example.com/open_api/settings',
'https://temp.example.com/admin/worker/configs',
]);
assert.equal(api.fetchCalls[1].options.headers['x-admin-auth'], 'admin-secret');
});
test('updateMailProviderUI keeps the temp domain selector visible and updates the hint when random subdomain is enabled', () => {
const bundle = extractFunction('updateMailProviderUI');