From a7ea261093c7fca5ed1f1fff7fac0cf8a1745c17 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Fri, 15 May 2026 02:52:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=BB=8E=20CFTE=20?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=20Temp=20=E5=9F=9F=E5=90=8D=E5=88=97?= =?UTF-8?q?=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sidepanel/sidepanel.html | 4 +- sidepanel/sidepanel.js | 178 ++++++++++++--- ...dflare-temp-email-random-subdomain.test.js | 210 ++++++++++++++++++ 3 files changed, 365 insertions(+), 27 deletions(-) diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 3db3046..92edae9 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -725,14 +725,14 @@
- + diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 9e29c62..9061f0b 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -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'); } diff --git a/tests/sidepanel-cloudflare-temp-email-random-subdomain.test.js b/tests/sidepanel-cloudflare-temp-email-random-subdomain.test.js index d553d40..303a298 100644 --- a/tests/sidepanel-cloudflare-temp-email-random-subdomain.test.js +++ b/tests/sidepanel-cloudflare-temp-email-random-subdomain.test.js @@ -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"[^>]*>更新 { + 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');