Explorar o código

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

QLHazyCoder hai 4 meses
pai
achega
544ecb33c2

+ 34 - 4
background.js

@@ -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 - 0
sidepanel/sidepanel.html

@@ -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 - 0
sidepanel/sidepanel.js

@@ -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 - 0
tests/background-icloud.test.js

@@ -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 () => {

+ 72 - 53
tests/cloudflare-temp-email-provider.test.js

@@ -51,53 +51,70 @@ function extractFunction(name) {
   return source.slice(start, end);
 }
 
-test('pollCloudflareTempEmailVerificationCode returns code even if delete fails', async () => {
+function createProviderApi(options = {}) {
+  const {
+    receiveMailbox = '',
+    messages = [{
+      id: 'mail-1',
+      address: 'user@example.com',
+      receivedDateTime: '2026-04-13T09:20:00.000Z',
+      subject: 'OpenAI verification code',
+      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');
 
-const api = new Function(`
+  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() {}
-async function listCloudflareTempEmailMessages() {
+function ensureCloudflareTempEmailConfig() {
+  return { receiveMailbox: options.receiveMailbox };
+}
+async function listCloudflareTempEmailMessages(_state, config) {
+  listCalls.push(config.address);
   return {
     config: {},
-    messages: [{
-      id: 'mail-1',
-      address: 'user@example.com',
-      receivedDateTime: '2026-04-13T09:20:00.000Z',
-      subject: 'OpenAI verification code',
-      from: { emailAddress: { address: 'noreply@tm.openai.com' } },
-      bodyPreview: 'Your verification code is 123456.',
-    }],
+    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']);
+});