feat: 新增邮件接收功能,支持配置 Cloudflare Temp Email 收件邮箱
This commit is contained in:
+34
-4
@@ -200,6 +200,7 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
cloudflareTempEmailBaseUrl: '',
|
||||
cloudflareTempEmailAdminAuth: '',
|
||||
cloudflareTempEmailCustomAuth: '',
|
||||
cloudflareTempEmailReceiveMailbox: '',
|
||||
cloudflareTempEmailDomain: '',
|
||||
cloudflareTempEmailDomains: [],
|
||||
hotmailAccounts: [],
|
||||
@@ -648,11 +649,32 @@ function getCloudflareTempEmailConfig(state = {}) {
|
||||
baseUrl: normalizeCloudflareTempEmailBaseUrl(state.cloudflareTempEmailBaseUrl),
|
||||
adminAuth: String(state.cloudflareTempEmailAdminAuth || ''),
|
||||
customAuth: String(state.cloudflareTempEmailCustomAuth || ''),
|
||||
receiveMailbox: normalizeCloudflareTempEmailReceiveMailbox(state.cloudflareTempEmailReceiveMailbox),
|
||||
domain: normalizeCloudflareTempEmailDomain(state.cloudflareTempEmailDomain),
|
||||
domains: normalizeCloudflareTempEmailDomains(state.cloudflareTempEmailDomains),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCloudflareTempEmailReceiveMailbox(value = '') {
|
||||
const normalized = normalizeCloudflareTempEmailAddress(value);
|
||||
if (!normalized) return '';
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized) ? normalized : '';
|
||||
}
|
||||
|
||||
function resolveCloudflareTempEmailPollTargetEmail(state = {}, pollPayload = {}, config = getCloudflareTempEmailConfig(state)) {
|
||||
const configuredReceiveMailbox = normalizeCloudflareTempEmailReceiveMailbox(config.receiveMailbox);
|
||||
if (configuredReceiveMailbox) {
|
||||
return configuredReceiveMailbox;
|
||||
}
|
||||
|
||||
const requestedTarget = normalizeCloudflareTempEmailReceiveMailbox(pollPayload.targetEmail);
|
||||
if (requestedTarget) {
|
||||
return requestedTarget;
|
||||
}
|
||||
|
||||
return normalizeCloudflareTempEmailReceiveMailbox(state.email);
|
||||
}
|
||||
|
||||
function normalizePersistentSettingValue(key, value) {
|
||||
switch (key) {
|
||||
case 'panelMode':
|
||||
@@ -715,6 +737,8 @@ function normalizePersistentSettingValue(key, value) {
|
||||
case 'cloudflareTempEmailAdminAuth':
|
||||
case 'cloudflareTempEmailCustomAuth':
|
||||
return String(value || '');
|
||||
case 'cloudflareTempEmailReceiveMailbox':
|
||||
return normalizeCloudflareTempEmailReceiveMailbox(value);
|
||||
case 'cloudflareTempEmailDomain':
|
||||
return normalizeCloudflareTempEmailDomain(value);
|
||||
case 'cloudflareTempEmailDomains':
|
||||
@@ -2629,12 +2653,18 @@ async function listCloudflareTempEmailMessages(state, options = {}) {
|
||||
}
|
||||
|
||||
async function pollCloudflareTempEmailVerificationCode(step, state, pollPayload = {}) {
|
||||
const targetEmail = normalizeCloudflareTempEmailAddress(pollPayload.targetEmail || state.email);
|
||||
const config = ensureCloudflareTempEmailConfig(state, { requireAdminAuth: true });
|
||||
const targetEmail = resolveCloudflareTempEmailPollTargetEmail(state, pollPayload, config);
|
||||
const registrationEmail = normalizeCloudflareTempEmailReceiveMailbox(state.email);
|
||||
if (!targetEmail) {
|
||||
throw new Error('Cloudflare Temp Email 轮询前缺少目标邮箱地址。');
|
||||
throw new Error('Cloudflare Temp Email 轮询前缺少目标邮箱地址,请先填写注册邮箱或“邮件接收”邮箱。');
|
||||
}
|
||||
|
||||
await addLog(`步骤 ${step}:正在轮询 Cloudflare Temp Email 邮件(${targetEmail})...`, 'info');
|
||||
if (registrationEmail && registrationEmail !== targetEmail) {
|
||||
await addLog(`步骤 ${step}:正在轮询 Cloudflare Temp Email 收件邮箱(${targetEmail}),注册邮箱为 ${registrationEmail}...`, 'info');
|
||||
} else {
|
||||
await addLog(`步骤 ${step}:正在轮询 Cloudflare Temp Email 邮件(${targetEmail})...`, 'info');
|
||||
}
|
||||
const maxAttempts = Number(pollPayload.maxAttempts) || 5;
|
||||
const intervalMs = Number(pollPayload.intervalMs) || 3000;
|
||||
let lastError = null;
|
||||
@@ -2642,7 +2672,7 @@ async function pollCloudflareTempEmailVerificationCode(step, state, pollPayload
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
throwIfStopped();
|
||||
try {
|
||||
const { config, messages } = await listCloudflareTempEmailMessages(state, {
|
||||
const { messages } = await listCloudflareTempEmailMessages(state, {
|
||||
address: targetEmail,
|
||||
limit: pollPayload.limit || CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE,
|
||||
offset: pollPayload.offset || 0,
|
||||
|
||||
@@ -202,6 +202,10 @@
|
||||
<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-receive-mailbox" style="display:none;">
|
||||
<span class="data-label">邮件接收</span>
|
||||
<input type="text" id="input-temp-email-receive-mailbox" class="data-input" placeholder="用于接收转发邮件的邮箱,例如 1@email.example.com" />
|
||||
</div>
|
||||
<div class="data-row" id="row-temp-email-domain" style="display:none;">
|
||||
<span class="data-label">Temp 域名</span>
|
||||
<div class="data-inline">
|
||||
|
||||
@@ -78,6 +78,8 @@ 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 rowTempEmailReceiveMailbox = document.getElementById('row-temp-email-receive-mailbox');
|
||||
const inputTempEmailReceiveMailbox = document.getElementById('input-temp-email-receive-mailbox');
|
||||
const rowTempEmailDomain = document.getElementById('row-temp-email-domain');
|
||||
const selectTempEmailDomain = document.getElementById('select-temp-email-domain');
|
||||
const inputTempEmailDomain = document.getElementById('input-temp-email-domain');
|
||||
@@ -1004,6 +1006,12 @@ function normalizeCloudflareTempEmailBaseUrlValue(value = '') {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCloudflareTempEmailReceiveMailboxValue(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (!normalized) return '';
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized) ? normalized : '';
|
||||
}
|
||||
|
||||
function normalizeCloudflareTempEmailDomainValue(value = '') {
|
||||
return normalizeCloudflareDomainValue(value);
|
||||
}
|
||||
@@ -1162,6 +1170,7 @@ function collectSettingsPayload() {
|
||||
cloudflareTempEmailBaseUrl: normalizeCloudflareTempEmailBaseUrlValue(inputTempEmailBaseUrl.value),
|
||||
cloudflareTempEmailAdminAuth: inputTempEmailAdminAuth.value,
|
||||
cloudflareTempEmailCustomAuth: inputTempEmailCustomAuth.value,
|
||||
cloudflareTempEmailReceiveMailbox: normalizeCloudflareTempEmailReceiveMailboxValue(inputTempEmailReceiveMailbox.value),
|
||||
cloudflareTempEmailDomain: selectedCloudflareTempEmailDomain,
|
||||
cloudflareTempEmailDomains: tempEmailDomains,
|
||||
autoRunSkipFailures: inputAutoSkipFailures.checked,
|
||||
@@ -1521,6 +1530,7 @@ function applySettingsState(state) {
|
||||
inputTempEmailBaseUrl.value = state?.cloudflareTempEmailBaseUrl || '';
|
||||
inputTempEmailAdminAuth.value = state?.cloudflareTempEmailAdminAuth || '';
|
||||
inputTempEmailCustomAuth.value = state?.cloudflareTempEmailCustomAuth || '';
|
||||
inputTempEmailReceiveMailbox.value = state?.cloudflareTempEmailReceiveMailbox || '';
|
||||
renderCloudflareDomainOptions(state?.cloudflareDomain || '');
|
||||
setCloudflareDomainEditMode(false, { clearInput: true });
|
||||
renderCloudflareTempEmailDomainOptions(state?.cloudflareTempEmailDomain || '');
|
||||
@@ -2656,6 +2666,7 @@ function updateMailProviderUI() {
|
||||
const useCloudflareTempEmailGenerator = selectedGenerator === 'cloudflare-temp-email';
|
||||
const showCloudflareDomain = useEmailGenerator && useCloudflare;
|
||||
const showCloudflareTempEmailSettings = useCloudflareTempEmailProvider || (useEmailGenerator && useCloudflareTempEmailGenerator);
|
||||
const showCloudflareTempEmailReceiveMailbox = useCloudflareTempEmailProvider && !useCloudflareTempEmailGenerator;
|
||||
const showCloudflareTempEmailDomain = useEmailGenerator && useCloudflareTempEmailGenerator;
|
||||
if (rowEmailGenerator) {
|
||||
rowEmailGenerator.style.display = useEmailGenerator ? '' : 'none';
|
||||
@@ -2680,6 +2691,7 @@ function updateMailProviderUI() {
|
||||
rowTempEmailBaseUrl.style.display = showCloudflareTempEmailSettings ? '' : 'none';
|
||||
rowTempEmailAdminAuth.style.display = showCloudflareTempEmailSettings ? '' : 'none';
|
||||
rowTempEmailCustomAuth.style.display = showCloudflareTempEmailSettings ? '' : 'none';
|
||||
rowTempEmailReceiveMailbox.style.display = showCloudflareTempEmailReceiveMailbox ? '' : 'none';
|
||||
rowTempEmailDomain.style.display = showCloudflareTempEmailDomain ? '' : 'none';
|
||||
const { domains: tempEmailDomains } = getCloudflareTempEmailDomainsFromState();
|
||||
if (showCloudflareTempEmailDomain) {
|
||||
@@ -2737,6 +2749,9 @@ function updateMailProviderUI() {
|
||||
if (autoHintText && useGmail && useGeneratedAlias) {
|
||||
autoHintText.textContent = '请先填写 Gmail 原邮箱,步骤 3 会自动生成 Gmail +tag 地址';
|
||||
}
|
||||
if (autoHintText && showCloudflareTempEmailReceiveMailbox) {
|
||||
autoHintText.textContent = '若注册邮箱会转发到 Cloudflare Temp Email,请在“邮件接收”中填写实际接收转发邮件的邮箱。';
|
||||
}
|
||||
if (useHotmail) {
|
||||
inputEmail.value = getCurrentHotmailEmail();
|
||||
} else if (useLuckmail) {
|
||||
@@ -4717,6 +4732,15 @@ inputTempEmailCustomAuth.addEventListener('blur', () => {
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
inputTempEmailReceiveMailbox.addEventListener('input', () => {
|
||||
markSettingsDirty(true);
|
||||
scheduleSettingsAutoSave();
|
||||
});
|
||||
inputTempEmailReceiveMailbox.addEventListener('blur', () => {
|
||||
inputTempEmailReceiveMailbox.value = normalizeCloudflareTempEmailReceiveMailboxValue(inputTempEmailReceiveMailbox.value);
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
inputAutoSkipFailuresThreadIntervalMinutes.addEventListener('input', () => {
|
||||
markSettingsDirty(true);
|
||||
scheduleSettingsAutoSave();
|
||||
@@ -4889,6 +4913,9 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (message.payload.cloudflareTempEmailCustomAuth !== undefined) {
|
||||
inputTempEmailCustomAuth.value = message.payload.cloudflareTempEmailCustomAuth || '';
|
||||
}
|
||||
if (message.payload.cloudflareTempEmailReceiveMailbox !== undefined) {
|
||||
inputTempEmailReceiveMailbox.value = message.payload.cloudflareTempEmailReceiveMailbox || '';
|
||||
}
|
||||
if (message.payload.cloudflareTempEmailDomain !== undefined || message.payload.cloudflareTempEmailDomains !== undefined) {
|
||||
renderCloudflareTempEmailDomainOptions(message.payload.cloudflareTempEmailDomain || latestState?.cloudflareTempEmailDomain || '');
|
||||
}
|
||||
|
||||
@@ -113,6 +113,13 @@ function normalizeCloudflareDomain(value = '') {
|
||||
function normalizeCloudflareDomains(values = []) {
|
||||
return Array.isArray(values) ? values : [];
|
||||
}
|
||||
function normalizeCloudflareTempEmailAddress(value = '') {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
function normalizeCloudflareTempEmailReceiveMailbox(value = '') {
|
||||
const normalized = normalizeCloudflareTempEmailAddress(value);
|
||||
return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(normalized) ? normalized : '';
|
||||
}
|
||||
function normalizeHotmailAccounts(values = []) {
|
||||
return Array.isArray(values) ? values : [];
|
||||
}
|
||||
@@ -168,6 +175,7 @@ test('normalizePersistentSettingValue handles icloud settings', () => {
|
||||
assert.equal(api.normalizePersistentSettingValue('icloudHostPreference', 'icloud.com'), 'icloud.com');
|
||||
assert.equal(api.normalizePersistentSettingValue('icloudHostPreference', 'bad-host'), 'auto');
|
||||
assert.equal(api.normalizePersistentSettingValue('autoDeleteUsedIcloudAlias', 1), true);
|
||||
assert.equal(api.normalizePersistentSettingValue('cloudflareTempEmailReceiveMailbox', ' Forward@Example.com '), 'forward@example.com');
|
||||
});
|
||||
|
||||
test('finalizeIcloudAliasAfterSuccessfulFlow marks icloud aliases as used without deleting when auto-delete is off', async () => {
|
||||
|
||||
@@ -51,31 +51,10 @@ function extractFunction(name) {
|
||||
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: [{
|
||||
function createProviderApi(options = {}) {
|
||||
const {
|
||||
receiveMailbox = '',
|
||||
messages = [{
|
||||
id: 'mail-1',
|
||||
address: 'user@example.com',
|
||||
receivedDateTime: '2026-04-13T09:20:00.000Z',
|
||||
@@ -83,21 +62,59 @@ async function listCloudflareTempEmailMessages() {
|
||||
from: { emailAddress: { address: 'noreply@tm.openai.com' } },
|
||||
bodyPreview: 'Your verification code is 123456.',
|
||||
}],
|
||||
deleteShouldFail = false,
|
||||
} = options;
|
||||
|
||||
const bundle = [
|
||||
extractFunction('isStopError'),
|
||||
extractFunction('throwIfStopped'),
|
||||
extractFunction('normalizeCloudflareTempEmailReceiveMailbox'),
|
||||
extractFunction('resolveCloudflareTempEmailPollTargetEmail'),
|
||||
extractFunction('summarizeCloudflareTempEmailMessagesForLog'),
|
||||
extractFunction('pollCloudflareTempEmailVerificationCode'),
|
||||
].join('\n');
|
||||
|
||||
return new Function('options', `
|
||||
let stopRequested = false;
|
||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||
const CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE = 20;
|
||||
const logs = [];
|
||||
const listCalls = [];
|
||||
const messages = options.messages;
|
||||
function normalizeCloudflareTempEmailAddress(value) {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
async function addLog(message, level) {
|
||||
logs.push({ message, level });
|
||||
}
|
||||
async function sleepWithStop() {}
|
||||
function ensureCloudflareTempEmailConfig() {
|
||||
return { receiveMailbox: options.receiveMailbox };
|
||||
}
|
||||
async function listCloudflareTempEmailMessages(_state, config) {
|
||||
listCalls.push(config.address);
|
||||
return {
|
||||
config: {},
|
||||
messages,
|
||||
};
|
||||
}
|
||||
function pickVerificationMessageWithTimeFallback(messages) {
|
||||
function pickVerificationMessageWithTimeFallback(currentMessages) {
|
||||
return {
|
||||
match: {
|
||||
code: '123456',
|
||||
receivedAt: Date.parse(messages[0].receivedDateTime),
|
||||
message: messages[0],
|
||||
},
|
||||
match: currentMessages[0]
|
||||
? {
|
||||
code: String(currentMessages[0].bodyPreview).match(/(\\d{6})/)[1],
|
||||
receivedAt: Date.parse(currentMessages[0].receivedDateTime),
|
||||
message: currentMessages[0],
|
||||
}
|
||||
: null,
|
||||
usedRelaxedFilters: false,
|
||||
usedTimeFallback: false,
|
||||
};
|
||||
}
|
||||
async function deleteCloudflareTempEmailMail() {
|
||||
throw new Error('delete failed');
|
||||
if (options.deleteShouldFail) {
|
||||
throw new Error('delete failed');
|
||||
}
|
||||
}
|
||||
|
||||
${bundle}
|
||||
@@ -105,10 +122,14 @@ ${bundle}
|
||||
return {
|
||||
pollCloudflareTempEmailVerificationCode,
|
||||
snapshot() {
|
||||
return { logs };
|
||||
return { logs, listCalls };
|
||||
},
|
||||
};
|
||||
`)();
|
||||
`)(options);
|
||||
}
|
||||
|
||||
test('pollCloudflareTempEmailVerificationCode returns code even if delete fails', async () => {
|
||||
const api = createProviderApi({ deleteShouldFail: true });
|
||||
|
||||
const result = await api.pollCloudflareTempEmailVerificationCode(4, { email: 'user@example.com' }, {
|
||||
targetEmail: 'user@example.com',
|
||||
@@ -119,42 +140,40 @@ return {
|
||||
assert.equal(result.code, '123456');
|
||||
const state = api.snapshot();
|
||||
assert.equal(state.logs.some((entry) => entry.message.includes('删除 Cloudflare Temp Email 邮件失败')), true);
|
||||
assert.deepEqual(state.listCalls, ['user@example.com']);
|
||||
});
|
||||
|
||||
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 };
|
||||
`)();
|
||||
test('pollCloudflareTempEmailVerificationCode requires target email or receive mailbox', async () => {
|
||||
const api = createProviderApi({ messages: [] });
|
||||
|
||||
await assert.rejects(
|
||||
api.pollCloudflareTempEmailVerificationCode(4, {}, {}),
|
||||
/缺少目标邮箱地址/
|
||||
/目标邮箱地址|邮件接收邮箱/
|
||||
);
|
||||
});
|
||||
|
||||
test('pollCloudflareTempEmailVerificationCode prefers configured receive mailbox over registration email', async () => {
|
||||
const api = createProviderApi({
|
||||
receiveMailbox: 'forward-box@email.20021108.xyz',
|
||||
messages: [{
|
||||
id: 'mail-2',
|
||||
address: 'forward-box@email.20021108.xyz',
|
||||
receivedDateTime: '2026-04-13T10:20:00.000Z',
|
||||
subject: 'Login verification code',
|
||||
from: { emailAddress: { address: 'noreply@tm.openai.com' } },
|
||||
bodyPreview: 'Your verification code is 654321.',
|
||||
}],
|
||||
});
|
||||
|
||||
const result = await api.pollCloudflareTempEmailVerificationCode(4, {
|
||||
email: 'duck-forwarded@duck.com',
|
||||
cloudflareTempEmailReceiveMailbox: 'forward-box@email.20021108.xyz',
|
||||
}, {
|
||||
targetEmail: 'duck-forwarded@duck.com',
|
||||
maxAttempts: 1,
|
||||
intervalMs: 1,
|
||||
});
|
||||
|
||||
assert.equal(result.code, '654321');
|
||||
assert.deepEqual(api.snapshot().listCalls, ['forward-box@email.20021108.xyz']);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user