feat: 新增邮件接收功能,支持配置 Cloudflare Temp Email 收件邮箱

This commit is contained in:
QLHazyCoder
2026-04-15 20:29:35 +08:00
parent 49eccd579c
commit 544ecb33c2
5 changed files with 158 additions and 70 deletions
+34 -4
View File
@@ -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,
+4
View File
@@ -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">
+27
View File
@@ -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 || '');
}
+8
View File
@@ -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 () => {
+85 -66
View File
@@ -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']);
});