feat: 支持从 CFTE 同步 Temp 域名列表
This commit is contained in:
@@ -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
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user