Explorar el Código

Merge commit 'c98715a4bc17b249639a7844ad8561aec261da87' into dev

# Conflicts:
#	sidepanel/sidepanel.js
QLHazyCoder hace 4 meses
padre
commit
f5abd68a9d

+ 421 - 12
background.js

@@ -1,6 +1,6 @@
 // background.js — Service Worker: orchestration, state, tab management, message routing
 
-importScripts('data/names.js', 'hotmail-utils.js', 'content/activation-utils.js');
+importScripts('data/names.js', 'hotmail-utils.js', 'cloudflare-temp-email-utils.js', 'content/activation-utils.js');
 
 const {
   buildHotmailMailApiLatestUrl,
@@ -17,6 +17,17 @@ const {
   pickVerificationMessageWithTimeFallback,
   shouldClearHotmailCurrentSelection,
 } = self.HotmailUtils;
+const {
+  DEFAULT_MAIL_PAGE_SIZE: CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE,
+  buildCloudflareTempEmailHeaders,
+  getCloudflareTempEmailAddressFromResponse,
+  joinCloudflareTempEmailUrl,
+  normalizeCloudflareTempEmailAddress,
+  normalizeCloudflareTempEmailBaseUrl,
+  normalizeCloudflareTempEmailDomain,
+  normalizeCloudflareTempEmailDomains,
+  normalizeCloudflareTempEmailMailApiMessages,
+} = self.CloudflareTempEmailUtils;
 const {
   isRecoverableStep9AuthFailure,
 } = self.MultiPageActivationUtils;
@@ -24,6 +35,8 @@ const {
 const LOG_PREFIX = '[MultiPage:bg]';
 const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill';
 const HOTMAIL_PROVIDER = 'hotmail-api';
+const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
+const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
 const HOTMAIL_MAILBOXES = ['INBOX', 'Junk'];
 const STOP_ERROR_MESSAGE = '流程已被用户停止。';
 const HUMAN_STEP_DELAY_MIN = 700;
@@ -85,6 +98,11 @@ const PERSISTED_SETTING_DEFAULTS = {
   hotmailLocalBaseUrl: DEFAULT_HOTMAIL_LOCAL_BASE_URL,
   cloudflareDomain: '',
   cloudflareDomains: [],
+  cloudflareTempEmailBaseUrl: '',
+  cloudflareTempEmailAdminAuth: '',
+  cloudflareTempEmailCustomAuth: '',
+  cloudflareTempEmailDomain: '',
+  cloudflareTempEmailDomains: [],
   hotmailAccounts: [],
 };
 
@@ -222,9 +240,8 @@ function normalizeEmailGenerator(value = '') {
   if (normalized === 'custom' || normalized === 'manual') {
     return 'custom';
   }
-  if (normalized === 'cloudflare') {
-    return 'cloudflare';
-  }
+  if (normalized === 'cloudflare') return 'cloudflare';
+  if (normalized === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return CLOUDFLARE_TEMP_EMAIL_GENERATOR;
   return 'duck';
 }
 
@@ -237,6 +254,7 @@ function normalizeMailProvider(value = '') {
   switch (normalized) {
     case 'custom':
     case HOTMAIL_PROVIDER:
+    case CLOUDFLARE_TEMP_EMAIL_PROVIDER:
     case '163':
     case '163-vip':
     case 'qq':
@@ -340,6 +358,16 @@ function getHotmailServiceSettings(state = {}) {
   };
 }
 
+function getCloudflareTempEmailConfig(state = {}) {
+  return {
+    baseUrl: normalizeCloudflareTempEmailBaseUrl(state.cloudflareTempEmailBaseUrl),
+    adminAuth: String(state.cloudflareTempEmailAdminAuth || ''),
+    customAuth: String(state.cloudflareTempEmailCustomAuth || ''),
+    domain: normalizeCloudflareTempEmailDomain(state.cloudflareTempEmailDomain),
+    domains: normalizeCloudflareTempEmailDomains(state.cloudflareTempEmailDomains),
+  };
+}
+
 function normalizePersistentSettingValue(key, value) {
   switch (key) {
     case 'panelMode':
@@ -391,6 +419,15 @@ function normalizePersistentSettingValue(key, value) {
       return normalizeCloudflareDomain(value);
     case 'cloudflareDomains':
       return normalizeCloudflareDomains(value);
+    case 'cloudflareTempEmailBaseUrl':
+      return normalizeCloudflareTempEmailBaseUrl(value);
+    case 'cloudflareTempEmailAdminAuth':
+    case 'cloudflareTempEmailCustomAuth':
+      return String(value || '');
+    case 'cloudflareTempEmailDomain':
+      return normalizeCloudflareTempEmailDomain(value);
+    case 'cloudflareTempEmailDomains':
+      return normalizeCloudflareTempEmailDomains(value);
     case 'hotmailAccounts':
       return normalizeHotmailAccounts(value);
     default:
@@ -434,6 +471,13 @@ function buildPersistentSettingsPayload(input = {}, options = {}) {
     }
     payload.cloudflareDomains = domains;
   }
+  if (payload.cloudflareTempEmailDomains) {
+    const domains = normalizeCloudflareTempEmailDomains(payload.cloudflareTempEmailDomains);
+    if (payload.cloudflareTempEmailDomain && !domains.includes(payload.cloudflareTempEmailDomain)) {
+      domains.unshift(payload.cloudflareTempEmailDomain);
+    }
+    payload.cloudflareTempEmailDomains = domains;
+  }
 
   return payload;
 }
@@ -1327,6 +1371,120 @@ function buildGeneratedAliasEmail(state) {
   throw new Error(`未支持的别名邮箱类型:${provider}`);
 }
 
+function summarizeCloudflareTempEmailMessagesForLog(messages) {
+  return (messages || [])
+    .slice()
+    .sort((left, right) => {
+      const leftTime = Date.parse(left.receivedDateTime || '') || 0;
+      const rightTime = Date.parse(right.receivedDateTime || '') || 0;
+      return rightTime - leftTime;
+    })
+    .slice(0, 3)
+    .map((message) => {
+      const receivedAt = message?.receivedDateTime || '未知时间';
+      const sender = message?.from?.emailAddress?.address || '未知发件人';
+      const subject = message?.subject || '(无主题)';
+      const preview = String(message?.bodyPreview || '').replace(/\s+/g, ' ').trim().slice(0, 80);
+      const address = message?.address || '未知地址';
+      return `[${address}] ${receivedAt} | ${sender} | ${subject} | ${preview}`;
+    })
+    .join(' || ');
+}
+
+async function deleteCloudflareTempEmailMail(config, mailId) {
+  const normalizedMailId = String(mailId || '').trim();
+  if (!normalizedMailId) return false;
+
+  await requestCloudflareTempEmailJson(config, `/admin/mails/${encodeURIComponent(normalizedMailId)}`, {
+    method: 'DELETE',
+  });
+  return true;
+}
+
+async function listCloudflareTempEmailMessages(state, options = {}) {
+  const config = ensureCloudflareTempEmailConfig(state, { requireAdminAuth: true });
+  const address = normalizeCloudflareTempEmailAddress(options.address);
+  const payload = await requestCloudflareTempEmailJson(config, '/admin/mails', {
+    method: 'GET',
+    searchParams: {
+      limit: Number(options.limit) || CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE,
+      offset: Number(options.offset) || 0,
+      address,
+    },
+  });
+
+  const messages = normalizeCloudflareTempEmailMailApiMessages(payload).filter((message) => {
+    if (!address) return true;
+    return !message.address || normalizeCloudflareTempEmailAddress(message.address) === address;
+  });
+
+  return { config, messages };
+}
+
+async function pollCloudflareTempEmailVerificationCode(step, state, pollPayload = {}) {
+  const targetEmail = normalizeCloudflareTempEmailAddress(pollPayload.targetEmail || state.email);
+  if (!targetEmail) {
+    throw new Error('Cloudflare Temp Email 轮询前缺少目标邮箱地址。');
+  }
+
+  await addLog(`步骤 ${step}:正在轮询 Cloudflare Temp Email 邮件(${targetEmail})...`, 'info');
+  const maxAttempts = Number(pollPayload.maxAttempts) || 5;
+  const intervalMs = Number(pollPayload.intervalMs) || 3000;
+  let lastError = null;
+
+  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+    throwIfStopped();
+    try {
+      const { config, messages } = await listCloudflareTempEmailMessages(state, {
+        address: targetEmail,
+        limit: pollPayload.limit || CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE,
+        offset: pollPayload.offset || 0,
+      });
+      const matchResult = pickVerificationMessageWithTimeFallback(messages, {
+        afterTimestamp: pollPayload.filterAfterTimestamp || 0,
+        senderFilters: pollPayload.senderFilters || [],
+        subjectFilters: pollPayload.subjectFilters || [],
+        excludeCodes: pollPayload.excludeCodes || [],
+      });
+      const match = matchResult.match;
+
+      if (match?.code) {
+        if (matchResult.usedRelaxedFilters) {
+          const fallbackLabel = matchResult.usedTimeFallback ? '宽松匹配 + 时间回退' : '宽松匹配';
+          await addLog(`步骤 ${step}:严格规则未命中,已改用 ${fallbackLabel} 并命中 Cloudflare Temp Email 验证码。`, 'warn');
+        }
+        try {
+          await deleteCloudflareTempEmailMail(config, match.message?.id);
+        } catch (err) {
+          await addLog(`步骤 ${step}:删除 Cloudflare Temp Email 邮件失败:${err.message}`, 'warn');
+        }
+        return {
+          ok: true,
+          code: match.code,
+          emailTimestamp: match.receivedAt || Date.now(),
+          mailId: match.message?.id || '',
+        };
+      }
+
+      lastError = new Error(`步骤 ${step}:暂未在 Cloudflare Temp Email 中找到匹配验证码(${attempt}/${maxAttempts})。`);
+      await addLog(lastError.message, attempt === maxAttempts ? 'warn' : 'info');
+      const sample = summarizeCloudflareTempEmailMessagesForLog(messages);
+      if (sample) {
+        await addLog(`步骤 ${step}:最近邮件样本:${sample}`, 'info');
+      }
+    } catch (err) {
+      lastError = err;
+      await addLog(`步骤 ${step}:Cloudflare Temp Email 轮询失败:${err.message}`, 'warn');
+    }
+
+    if (attempt < maxAttempts) {
+      await sleepWithStop(intervalMs);
+    }
+  }
+
+  throw lastError || new Error(`步骤 ${step}:未在 Cloudflare Temp Email 中找到新的匹配验证码。`);
+}
+
 // ============================================================
 // Tab Registry
 // ============================================================
@@ -1557,11 +1715,14 @@ function buildLocalhostCleanupPrefix(rawUrl) {
 async function closeTabsByUrlPrefix(prefix, options = {}) {
   if (!prefix) return 0;
 
-  const { excludeTabIds = [] } = options;
+  const { excludeTabIds = [], excludeUrls = [], excludeLocalhostCallbacks = false } = options;
   const excluded = new Set(excludeTabIds.filter(id => Number.isInteger(id)));
+  const excludedUrls = new Set((Array.isArray(excludeUrls) ? excludeUrls : []).filter(Boolean));
   const tabs = await chrome.tabs.query({});
   const matchedIds = tabs
     .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
+    .filter((tab) => typeof tab.url === 'string' && !excludedUrls.has(tab.url))
+    .filter((tab) => !(excludeLocalhostCallbacks && isLocalhostOAuthCallbackUrl(tab.url)))
     .filter((tab) => typeof tab.url === 'string' && tab.url.startsWith(prefix))
     .map((tab) => tab.id);
 
@@ -2149,6 +2310,7 @@ function getSourceLabel(source) {
     'inbucket-mail': 'Inbucket 邮箱',
     'duck-mail': 'Duck 邮箱',
     'hotmail-api': 'Hotmail(远程/本地)',
+    'cloudflare-temp-email': 'Cloudflare Temp Email',
   };
   return labels[source] || source || '未知来源';
 }
@@ -3166,7 +3328,10 @@ async function handleStepData(step, payload) {
       }
       const localhostPrefix = buildLocalhostCleanupPrefix(payload.localhostUrl);
       if (localhostPrefix) {
-        await closeTabsByUrlPrefix(localhostPrefix);
+        await closeTabsByUrlPrefix(localhostPrefix, {
+          excludeUrls: [payload.localhostUrl],
+          excludeLocalhostCallbacks: true,
+        });
       }
       if (shouldUseCustomRegistrationEmail(latestState) && latestState.email) {
         await setEmailStateSilently(null);
@@ -3459,7 +3624,9 @@ function getEmailGeneratorLabel(generator) {
   if (generator === 'custom') {
     return '自定义邮箱';
   }
-  return generator === 'cloudflare' ? 'Cloudflare 邮箱' : 'Duck 邮箱';
+  if (generator === 'cloudflare') return 'Cloudflare 邮箱';
+  if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email';
+  return 'Duck 邮箱';
 }
 
 function generateCloudflareAliasLocalPart() {
@@ -3499,6 +3666,107 @@ async function fetchCloudflareEmail(state, options = {}) {
   return aliasEmail;
 }
 
+function ensureCloudflareTempEmailConfig(state, options = {}) {
+  const {
+    requireAdminAuth = false,
+    requireDomain = false,
+  } = options;
+  const config = getCloudflareTempEmailConfig(state);
+  if (!config.baseUrl) {
+    throw new Error('Cloudflare Temp Email 服务地址为空或格式无效。');
+  }
+  if (requireAdminAuth && !config.adminAuth) {
+    throw new Error('Cloudflare Temp Email 缺少 Admin Auth。');
+  }
+  if (requireDomain && !config.domain) {
+    throw new Error('Cloudflare Temp Email 域名为空或格式无效。');
+  }
+  return config;
+}
+
+async function requestCloudflareTempEmailJson(config, path, options = {}) {
+  const {
+    method = 'GET',
+    payload,
+    searchParams,
+    timeoutMs = 20000,
+  } = options;
+
+  const url = new URL(joinCloudflareTempEmailUrl(config.baseUrl, path));
+  if (searchParams && typeof searchParams === 'object') {
+    for (const [key, value] of Object.entries(searchParams)) {
+      if (value === undefined || value === null || value === '') continue;
+      url.searchParams.set(key, String(value));
+    }
+  }
+
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(new Error('timeout')), timeoutMs);
+
+  let response;
+  try {
+    response = await fetch(url.toString(), {
+      method,
+      headers: buildCloudflareTempEmailHeaders(config, {
+        json: payload !== undefined,
+      }),
+      body: payload !== undefined ? JSON.stringify(payload) : undefined,
+      signal: controller.signal,
+    });
+  } catch (err) {
+    const errorMessage = err?.name === 'AbortError'
+      ? `Cloudflare Temp Email 请求超时(>${Math.round(timeoutMs / 1000)} 秒)`
+      : `Cloudflare Temp Email 请求失败:${err.message}`;
+    throw new Error(errorMessage);
+  } finally {
+    clearTimeout(timeoutId);
+  }
+
+  const text = await response.text();
+  let parsed;
+  try {
+    parsed = text ? JSON.parse(text) : {};
+  } catch {
+    parsed = text;
+  }
+
+  if (!response.ok) {
+    const payloadError = typeof parsed === 'object' && parsed
+      ? (parsed.message || parsed.error || parsed.msg)
+      : '';
+    throw new Error(`Cloudflare Temp Email 请求失败:${payloadError || text || `HTTP ${response.status}`}`);
+  }
+
+  return parsed;
+}
+
+async function fetchCloudflareTempEmailAddress(state, options = {}) {
+  throwIfStopped();
+  const latestState = state || await getState();
+  const config = ensureCloudflareTempEmailConfig(latestState, {
+    requireAdminAuth: true,
+    requireDomain: true,
+  });
+  const requestedName = String(options.localPart || options.name || '').trim().toLowerCase() || generateCloudflareAliasLocalPart();
+  const payload = {
+    enablePrefix: true,
+    name: requestedName,
+    domain: config.domain,
+  };
+  const result = await requestCloudflareTempEmailJson(config, '/admin/new_address', {
+    method: 'POST',
+    payload,
+  });
+  const address = normalizeCloudflareTempEmailAddress(getCloudflareTempEmailAddressFromResponse(result));
+  if (!address) {
+    throw new Error('Cloudflare Temp Email 未返回可用邮箱地址。');
+  }
+
+  await setEmailState(address);
+  await addLog(`Cloudflare Temp Email:已生成 ${address}`, 'ok');
+  return address;
+}
+
 async function fetchDuckEmail(options = {}) {
   throwIfStopped();
   const { generateNew = true } = options;
@@ -3533,6 +3801,9 @@ async function fetchGeneratedEmail(state, options = {}) {
   if (generator === 'cloudflare') {
     return fetchCloudflareEmail(currentState, options);
   }
+  if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) {
+    return fetchCloudflareTempEmailAddress(currentState, options);
+  }
   return fetchDuckEmail(options);
 }
 
@@ -3546,6 +3817,7 @@ let autoRunTotalRuns = 1;
 let autoRunAttemptRun = 0;
 const EMAIL_FETCH_MAX_ATTEMPTS = 5;
 const VERIFICATION_POLL_MAX_ROUNDS = 5;
+const STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS = 25000;
 const AUTO_STEP_DELAYS = {
   1: 2000,
   2: 2000,
@@ -3635,7 +3907,10 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
     } catch (err) {
       lastError = err;
       await addLog(`${generatorLabel}自动获取失败(${attempt}/${EMAIL_FETCH_MAX_ATTEMPTS}):${err.message}`, 'warn');
-      if (generator === 'cloudflare' && /域名/.test(String(err.message || ''))) {
+      if (
+        (generator === 'cloudflare' && /域名/.test(String(err.message || '')))
+        || (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR && /(服务地址|Admin Auth|域名)/.test(String(err.message || '')))
+      ) {
         break;
       }
     }
@@ -4757,6 +5032,9 @@ function getMailConfig(state) {
   if (provider === HOTMAIL_PROVIDER) {
     return { provider: HOTMAIL_PROVIDER, label: 'Hotmail(远程/本地)' };
   }
+  if (provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
+    return { provider: CLOUDFLARE_TEMP_EMAIL_PROVIDER, label: 'Cloudflare Temp Email' };
+  }
   if (provider === '163') {
     return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 邮箱' };
   }
@@ -4921,6 +5199,16 @@ async function pollFreshVerificationCode(step, state, mail, pollOverrides = {})
       ...pollOverrides,
     });
   }
+  if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
+    return pollCloudflareTempEmailVerificationCode(step, state, {
+      ...getVerificationPollPayload(step, state),
+      ...pollOverrides,
+    });
+  }
+
+  if (Number(pollOverrides.resendIntervalMs) > 0) {
+    return pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides);
+  }
 
   const stateKey = getVerificationCodeStateKey(step);
   const rejectedCodes = new Set();
@@ -4990,6 +5278,109 @@ async function pollFreshVerificationCode(step, state, mail, pollOverrides = {})
   throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`);
 }
 
+async function pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides = {}) {
+  const stateKey = getVerificationCodeStateKey(step);
+  const rejectedCodes = new Set();
+  if (state[stateKey]) {
+    rejectedCodes.add(state[stateKey]);
+  }
+  for (const code of (pollOverrides.excludeCodes || [])) {
+    if (code) rejectedCodes.add(code);
+  }
+
+  const {
+    maxRounds: _ignoredMaxRounds,
+    resendIntervalMs: _ignoredResendIntervalMs,
+    lastResendAt: _ignoredLastResendAt,
+    ...payloadOverrides
+  } = pollOverrides;
+  let lastError = null;
+  const filterAfterTimestamp = payloadOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp;
+  const maxRounds = pollOverrides.maxRounds || VERIFICATION_POLL_MAX_ROUNDS;
+  const resendIntervalMs = Math.max(0, Number(pollOverrides.resendIntervalMs) || 0);
+  let lastResendAt = Number(pollOverrides.lastResendAt) || 0;
+
+  for (let round = 1; round <= maxRounds; round++) {
+    throwIfStopped();
+    if (round > 1) {
+      lastResendAt = await requestVerificationCodeResend(step);
+    }
+
+    while (true) {
+      throwIfStopped();
+      const payload = getVerificationPollPayload(step, state, {
+        ...payloadOverrides,
+        filterAfterTimestamp,
+        excludeCodes: [...rejectedCodes],
+      });
+
+      if (lastResendAt > 0) {
+        const remainingBeforeResendMs = Math.max(0, resendIntervalMs - (Date.now() - lastResendAt));
+        const baseMaxAttempts = Math.max(1, Number(payload.maxAttempts) || 5);
+        const intervalMs = Math.max(1, Number(payload.intervalMs) || 3000);
+        payload.maxAttempts = Math.max(1, Math.min(baseMaxAttempts, Math.floor(remainingBeforeResendMs / intervalMs) + 1));
+      }
+
+      try {
+        const result = await sendToMailContentScriptResilient(
+          mail,
+          {
+            type: 'POLL_EMAIL',
+            step,
+            source: 'background',
+            payload,
+          },
+          {
+            timeoutMs: 45000,
+            maxRecoveryAttempts: 2,
+          }
+        );
+
+        if (result && result.error) {
+          throw new Error(result.error);
+        }
+
+        if (!result || !result.code) {
+          throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`);
+        }
+
+        if (rejectedCodes.has(result.code)) {
+          throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`);
+        }
+
+        return {
+          ...result,
+          lastResendAt,
+        };
+      } catch (err) {
+        if (isStopError(err)) {
+          throw err;
+        }
+        lastError = err;
+        await addLog(`步骤 ${step}:${err.message}`, 'warn');
+      }
+
+      const remainingBeforeResendMs = lastResendAt > 0
+        ? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt))
+        : 0;
+      if (remainingBeforeResendMs > 0) {
+        await addLog(
+          `步骤 ${step}:距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,继续刷新邮箱(第 ${round}/${maxRounds} 轮)...`,
+          'info'
+        );
+        continue;
+      }
+
+      if (round < maxRounds) {
+        await addLog(`步骤 ${step}:已到 25 秒重发间隔,准备重新发送验证码(第 ${round + 1}/${maxRounds} 轮)...`, 'warn');
+      }
+      break;
+    }
+  }
+
+  throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`);
+}
+
 async function submitVerificationCode(step, code) {
   const signupTabId = await getTabId('signup-page');
   if (!signupTabId) {
@@ -5034,10 +5425,12 @@ async function resolveVerificationStep(step, state, mail, options = {}) {
     ? Boolean(options.requestFreshCodeFirst)
     : (hotmailPollConfig?.requestFreshCodeFirst ?? false);
   const maxSubmitAttempts = 3;
+  const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
+  let lastResendAt = Number(options.lastResendAt) || 0;
 
   if (requestFreshCodeFirst) {
     try {
-      await requestVerificationCodeResend(step);
+      lastResendAt = await requestVerificationCodeResend(step);
       await addLog(`步骤 ${step}:已先请求一封新的${getVerificationCodeLabel(step)}验证码,再开始轮询邮箱。`, 'warn');
     } catch (err) {
       if (isStopError(err) || (step === 7 && isStep7RestartFromStep6Error(err))) {
@@ -5059,7 +5452,10 @@ async function resolveVerificationStep(step, state, mail, options = {}) {
     const result = await pollFreshVerificationCode(step, state, mail, {
       excludeCodes: [...rejectedCodes],
       filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined,
+      resendIntervalMs,
+      lastResendAt,
     });
+    lastResendAt = Number(result?.lastResendAt) || lastResendAt;
 
     throwIfStopped();
     await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`);
@@ -5074,7 +5470,18 @@ async function resolveVerificationStep(step, state, mail, options = {}) {
         throw new Error(`步骤 ${step}:验证码连续失败,已达到 ${maxSubmitAttempts} 次重试上限。`);
       }
 
-      await requestVerificationCodeResend(step);
+      const remainingBeforeResendMs = resendIntervalMs > 0 && lastResendAt > 0
+        ? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt))
+        : 0;
+      if (remainingBeforeResendMs > 0) {
+        await addLog(
+          `步骤 ${step}:提交失败后距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,先继续刷新邮箱(${attempt + 1}/${maxSubmitAttempts})...`,
+          'warn'
+        );
+        continue;
+      }
+
+      lastResendAt = await requestVerificationCodeResend(step);
       await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts})...`, 'warn');
       continue;
     }
@@ -5136,7 +5543,7 @@ async function executeStep4(state) {
   }
 
   throwIfStopped();
-  if (mail.provider === HOTMAIL_PROVIDER) {
+  if (mail.provider === HOTMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
     await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
   } else {
     await addLog(`步骤 4:正在打开${mail.label}...`);
@@ -5164,6 +5571,7 @@ async function executeStep4(state) {
   await resolveVerificationStep(4, state, mail, {
     filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt,
     requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
+    resendIntervalMs: mail.provider === HOTMAIL_PROVIDER ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
   });
   return;
 }
@@ -5271,7 +5679,7 @@ async function runStep7Attempt(state) {
   }
 
   throwIfStopped();
-  if (mail.provider === HOTMAIL_PROVIDER) {
+  if (mail.provider === HOTMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
     await addLog(`步骤 7:正在通过 ${mail.label} 轮询验证码...`);
   } else {
     await addLog(`步骤 7:正在打开${mail.label}...`);
@@ -5298,6 +5706,7 @@ async function runStep7Attempt(state) {
   await resolveVerificationStep(7, state, mail, {
     filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt,
     requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
+    resendIntervalMs: mail.provider === HOTMAIL_PROVIDER ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
   });
 }
 

+ 400 - 0
cloudflare-temp-email-utils.js

@@ -0,0 +1,400 @@
+(function cloudflareTempEmailUtilsModule(root, factory) {
+  if (typeof module !== 'undefined' && module.exports) {
+    module.exports = factory();
+    return;
+  }
+
+  root.CloudflareTempEmailUtils = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createCloudflareTempEmailUtils() {
+  const DEFAULT_MAIL_PAGE_SIZE = 20;
+
+  function firstNonEmptyString(values) {
+    for (const value of values) {
+      if (value === undefined || value === null) continue;
+      const normalized = String(value).trim();
+      if (normalized) return normalized;
+    }
+    return '';
+  }
+
+  function normalizeCloudflareTempEmailBaseUrl(rawValue = '') {
+    const value = String(rawValue || '').trim();
+    if (!value) return '';
+
+    const candidate = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `https://${value}`;
+    try {
+      const parsed = new URL(candidate);
+      parsed.hash = '';
+      parsed.search = '';
+      const pathname = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/+$/, '');
+      return `${parsed.origin}${pathname}`;
+    } catch {
+      return '';
+    }
+  }
+
+  function normalizeCloudflareTempEmailDomain(rawValue = '') {
+    let value = String(rawValue || '').trim().toLowerCase();
+    if (!value) return '';
+    value = value.replace(/^@+/, '');
+    value = value.replace(/^https?:\/\//, '');
+    value = value.replace(/\/.*$/, '');
+    if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(value)) {
+      return '';
+    }
+    return value;
+  }
+
+  function normalizeCloudflareTempEmailDomains(values) {
+    const domains = [];
+    const seen = new Set();
+    for (const value of Array.isArray(values) ? values : []) {
+      const normalized = normalizeCloudflareTempEmailDomain(value);
+      if (!normalized || seen.has(normalized)) continue;
+      seen.add(normalized);
+      domains.push(normalized);
+    }
+    return domains;
+  }
+
+  function buildCloudflareTempEmailHeaders(config = {}, options = {}) {
+    const headers = {};
+    const adminAuth = firstNonEmptyString([config.adminAuth, config.cloudflareTempEmailAdminAuth]);
+    const customAuth = firstNonEmptyString([config.customAuth, config.cloudflareTempEmailCustomAuth]);
+    if (adminAuth) {
+      headers['x-admin-auth'] = adminAuth;
+    }
+    if (customAuth) {
+      headers['x-custom-auth'] = customAuth;
+    }
+    if (options.json) {
+      headers['Content-Type'] = 'application/json';
+    }
+    if (options.acceptJson !== false) {
+      headers.Accept = 'application/json';
+    }
+    return headers;
+  }
+
+  function joinCloudflareTempEmailUrl(baseUrl, path) {
+    const normalizedBase = normalizeCloudflareTempEmailBaseUrl(baseUrl);
+    const normalizedPath = String(path || '').trim();
+    if (!normalizedBase || !normalizedPath) return normalizedBase || '';
+    return `${normalizedBase}${normalizedPath.startsWith('/') ? '' : '/'}${normalizedPath}`;
+  }
+
+  function getCloudflareTempEmailMailRows(payload) {
+    if (Array.isArray(payload)) return payload;
+    if (!payload || typeof payload !== 'object') return [];
+
+    const candidates = [
+      payload.data,
+      payload.items,
+      payload.messages,
+      payload.mails,
+      payload.results,
+      payload.rows,
+    ];
+
+    for (const candidate of candidates) {
+      if (Array.isArray(candidate)) {
+        return candidate;
+      }
+    }
+
+    return [];
+  }
+
+  function normalizeCloudflareTempEmailAddress(value) {
+    return String(value || '').trim().toLowerCase();
+  }
+
+  function splitRawMessage(raw = '') {
+    const source = String(raw || '');
+    if (!source) {
+      return { headerText: '', bodyText: '' };
+    }
+
+    const normalized = source.replace(/\r\n/g, '\n');
+    const separatorIndex = normalized.indexOf('\n\n');
+    if (separatorIndex === -1) {
+      return { headerText: normalized, bodyText: '' };
+    }
+
+    return {
+      headerText: normalized.slice(0, separatorIndex),
+      bodyText: normalized.slice(separatorIndex + 2),
+    };
+  }
+
+  function parseRawHeaders(headerText = '') {
+    const headers = {};
+    const lines = String(headerText || '').split('\n');
+    let currentName = '';
+
+    for (const line of lines) {
+      if (!line) continue;
+      if ((line.startsWith(' ') || line.startsWith('\t')) && currentName) {
+        headers[currentName] += ` ${line.trim()}`;
+        continue;
+      }
+
+      const separatorIndex = line.indexOf(':');
+      if (separatorIndex <= 0) continue;
+      currentName = line.slice(0, separatorIndex).trim().toLowerCase();
+      headers[currentName] = line.slice(separatorIndex + 1).trim();
+    }
+
+    return headers;
+  }
+
+  function decodeMimeEncodedWords(value = '') {
+    const source = String(value || '');
+    return source.replace(/=\?([^?]+)\?([bBqQ])\?([^?]+)\?=/g, (_match, charset, encoding, encodedText) => {
+      try {
+        if (String(encoding).toUpperCase() === 'B') {
+          return decodeBytesToString(base64ToBytes(encodedText), charset);
+        }
+        return decodeBytesToString(
+          quotedPrintableToBytes(String(encodedText).replace(/_/g, ' '), { headerMode: true }),
+          charset
+        );
+      } catch {
+        return encodedText;
+      }
+    });
+  }
+
+  function base64ToBytes(value = '') {
+    const normalized = String(value || '').replace(/\s+/g, '');
+    if (!normalized) return new Uint8Array();
+
+    if (typeof atob === 'function') {
+      const decoded = atob(normalized);
+      const bytes = new Uint8Array(decoded.length);
+      for (let i = 0; i < decoded.length; i += 1) {
+        bytes[i] = decoded.charCodeAt(i);
+      }
+      return bytes;
+    }
+
+    if (typeof Buffer !== 'undefined') {
+      return Uint8Array.from(Buffer.from(normalized, 'base64'));
+    }
+
+    throw new Error('No base64 decoder available');
+  }
+
+  function quotedPrintableToBytes(value = '', options = {}) {
+    const { headerMode = false } = options;
+    const source = String(value || '')
+      .replace(/=\r?\n/g, '')
+      .replace(headerMode ? /_/g : /$^/, ' ');
+
+    const bytes = [];
+    for (let index = 0; index < source.length; index += 1) {
+      const char = source[index];
+      if (char === '=' && /^[0-9A-Fa-f]{2}$/.test(source.slice(index + 1, index + 3))) {
+        bytes.push(parseInt(source.slice(index + 1, index + 3), 16));
+        index += 2;
+        continue;
+      }
+      bytes.push(char.charCodeAt(0));
+    }
+    return Uint8Array.from(bytes);
+  }
+
+  function decodeBytesToString(bytes, charset = 'utf-8') {
+    const normalizedCharset = String(charset || 'utf-8').trim().toLowerCase();
+    const candidates = [normalizedCharset];
+    if (normalizedCharset === 'utf8') {
+      candidates.unshift('utf-8');
+    }
+    if (normalizedCharset === 'gb2312' || normalizedCharset === 'gbk') {
+      candidates.unshift('gb18030');
+    }
+
+    for (const candidate of candidates) {
+      try {
+        if (typeof TextDecoder !== 'undefined') {
+          return new TextDecoder(candidate, { fatal: false }).decode(bytes);
+        }
+      } catch {
+        // ignore and try fallback
+      }
+    }
+
+    if (typeof Buffer !== 'undefined') {
+      return Buffer.from(bytes).toString('utf8');
+    }
+
+    let result = '';
+    for (const byte of bytes) {
+      result += String.fromCharCode(byte);
+    }
+    return result;
+  }
+
+  function getCharsetFromContentType(contentType = '') {
+    const match = String(contentType || '').match(/charset="?([^";]+)"?/i);
+    return match ? match[1].trim() : 'utf-8';
+  }
+
+  function getBoundaryFromContentType(contentType = '') {
+    const match = String(contentType || '').match(/boundary="?([^";]+)"?/i);
+    return match ? match[1] : '';
+  }
+
+  function stripHtmlTags(value = '') {
+    return String(value || '')
+      .replace(/<style[\s\S]*?<\/style>/gi, ' ')
+      .replace(/<script[\s\S]*?<\/script>/gi, ' ')
+      .replace(/<[^>]+>/g, ' ')
+      .replace(/&nbsp;/gi, ' ')
+      .replace(/&amp;/gi, '&')
+      .replace(/&lt;/gi, '<')
+      .replace(/&gt;/gi, '>')
+      .replace(/\s+/g, ' ')
+      .trim();
+  }
+
+  function decodeMimeBody(bodyText = '', headers = {}) {
+    const contentType = String(headers['content-type'] || '');
+    const transferEncoding = String(headers['content-transfer-encoding'] || '').trim().toLowerCase();
+    const charset = getCharsetFromContentType(contentType);
+    let decoded = String(bodyText || '');
+
+    if (transferEncoding === 'base64') {
+      decoded = decodeBytesToString(base64ToBytes(decoded), charset);
+    } else if (transferEncoding === 'quoted-printable') {
+      decoded = decodeBytesToString(quotedPrintableToBytes(decoded), charset);
+    }
+
+    if (/text\/html/i.test(contentType)) {
+      return stripHtmlTags(decoded);
+    }
+    return decoded.replace(/\s+/g, ' ').trim();
+  }
+
+  function extractTextFromMime(rawMessage = '', depth = 0) {
+    const { headerText, bodyText } = splitRawMessage(rawMessage);
+    const headers = parseRawHeaders(headerText);
+    const contentType = String(headers['content-type'] || '');
+    const boundary = getBoundaryFromContentType(contentType);
+
+    if (/multipart\//i.test(contentType) && boundary && depth < 6) {
+      const marker = `--${boundary}`;
+      const sections = String(bodyText || '')
+        .split(marker)
+        .map((part) => part.trim())
+        .filter((part) => part && part !== '--');
+
+      const extractedParts = sections
+        .map((part) => part.replace(/--\s*$/, '').trim())
+        .map((part) => extractTextFromMime(part, depth + 1)?.text || '')
+        .filter(Boolean);
+
+      const plainText = extractedParts.join(' ').replace(/\s+/g, ' ').trim();
+      return {
+        headers,
+        text: plainText,
+      };
+    }
+
+    return {
+      headers,
+      text: decodeMimeBody(bodyText, headers),
+    };
+  }
+
+  function normalizeReceivedDateTime(value) {
+    if (!value && value !== 0) return '';
+    if (typeof value === 'number' && Number.isFinite(value)) {
+      return new Date(value).toISOString();
+    }
+    const source = String(value || '').trim();
+    if (!source) return '';
+    const parsed = Date.parse(source);
+    return Number.isFinite(parsed) ? new Date(parsed).toISOString() : source;
+  }
+
+  function normalizeCloudflareTempEmailMessage(row = {}) {
+    if (!row || typeof row !== 'object') return null;
+
+    const address = normalizeCloudflareTempEmailAddress(firstNonEmptyString([
+      row.address,
+      row.mail_address,
+      row.email,
+      row.recipient,
+    ]));
+    const raw = firstNonEmptyString([row.raw, row.source, row.mime, row.message]);
+    const parsedMime = raw ? extractTextFromMime(raw) : { headers: {}, text: '' };
+    const subject = decodeMimeEncodedWords(firstNonEmptyString([
+      row.subject,
+      parsedMime.headers.subject,
+    ]));
+    const fromAddress = decodeMimeEncodedWords(firstNonEmptyString([
+      row.from,
+      row.sender,
+      row.mail_from,
+      parsedMime.headers.from,
+    ]));
+    const bodyPreview = firstNonEmptyString([
+      row.text,
+      row.preview,
+      row.body,
+      parsedMime.text,
+      raw,
+    ]).replace(/\s+/g, ' ').trim();
+
+    return {
+      id: firstNonEmptyString([row.id, row.mail_id]),
+      address,
+      addressId: firstNonEmptyString([row.address_id, row.addressId]),
+      subject,
+      from: {
+        emailAddress: {
+          address: fromAddress,
+        },
+      },
+      bodyPreview,
+      raw,
+      receivedDateTime: normalizeReceivedDateTime(firstNonEmptyString([
+        row.receivedDateTime,
+        row.received_at,
+        row.created_at,
+        row.createdAt,
+        row.updated_at,
+        row.date,
+      ])),
+    };
+  }
+
+  function normalizeCloudflareTempEmailMailApiMessages(payload) {
+    return getCloudflareTempEmailMailRows(payload)
+      .map((row) => normalizeCloudflareTempEmailMessage(row))
+      .filter(Boolean);
+  }
+
+  function getCloudflareTempEmailAddressFromResponse(payload = {}) {
+    return firstNonEmptyString([
+      payload.address,
+      payload.email,
+      payload?.data?.address,
+      payload?.data?.email,
+    ]);
+  }
+
+  return {
+    DEFAULT_MAIL_PAGE_SIZE,
+    buildCloudflareTempEmailHeaders,
+    getCloudflareTempEmailAddressFromResponse,
+    joinCloudflareTempEmailUrl,
+    normalizeCloudflareTempEmailAddress,
+    normalizeCloudflareTempEmailBaseUrl,
+    normalizeCloudflareTempEmailDomain,
+    normalizeCloudflareTempEmailDomains,
+    normalizeCloudflareTempEmailMailApiMessages,
+    normalizeCloudflareTempEmailMessage,
+  };
+});

+ 23 - 0
sidepanel/sidepanel.html

@@ -159,6 +159,7 @@
             <option value="qq">QQ 邮箱 (wx.mail.qq.com)</option>
             <option value="inbucket">Inbucket(自定义主机)</option>
             <option value="2925">2925 邮箱 (2925.com)</option>
+            <option value="cloudflare-temp-email">Cloudflare Temp Email</option>
           </select>
           <button id="btn-mail-login" class="btn btn-outline btn-sm data-inline-btn" type="button" disabled>登录</button>
         </div>
@@ -175,8 +176,30 @@
         <select id="select-email-generator" class="data-select">
           <option value="duck">DuckDuckGo</option>
           <option value="cloudflare">Cloudflare</option>
+          <option value="cloudflare-temp-email">Cloudflare Temp Email</option>
         </select>
       </div>
+      <div class="data-row" id="row-temp-email-base-url" style="display:none;">
+        <span class="data-label">Temp API</span>
+        <input type="text" id="input-temp-email-base-url" class="data-input" placeholder="https://your-worker-domain" />
+      </div>
+      <div class="data-row" id="row-temp-email-admin-auth" style="display:none;">
+        <span class="data-label">Admin Auth</span>
+        <input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon" placeholder="Cloudflare Temp Email admin password" />
+      </div>
+      <div class="data-row" id="row-temp-email-custom-auth" style="display:none;">
+        <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-domain" style="display:none;">
+        <span class="data-label">Temp 域名</span>
+        <div class="data-inline">
+          <select id="select-temp-email-domain" class="data-select"></select>
+          <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>
+        </div>
+      </div>
       <div class="data-row" id="row-cf-domain" style="display:none;">
         <span class="data-label">CF 域名</span>
         <div class="data-inline">

+ 256 - 5
sidepanel/sidepanel.js

@@ -70,6 +70,16 @@ const rowMail2925Mode = document.getElementById('row-mail-2925-mode');
 const mail2925ModeButtons = Array.from(document.querySelectorAll('[data-mail-2925-mode]'));
 const rowEmailGenerator = document.getElementById('row-email-generator');
 const selectEmailGenerator = document.getElementById('select-email-generator');
+const rowTempEmailBaseUrl = document.getElementById('row-temp-email-base-url');
+const inputTempEmailBaseUrl = document.getElementById('input-temp-email-base-url');
+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 rowTempEmailDomain = document.getElementById('row-temp-email-domain');
+const selectTempEmailDomain = document.getElementById('select-temp-email-domain');
+const inputTempEmailDomain = document.getElementById('input-temp-email-domain');
+const btnTempEmailDomainMode = document.getElementById('btn-temp-email-domain-mode');
 const hotmailSection = document.getElementById('hotmail-section');
 const rowHotmailServiceMode = document.getElementById('row-hotmail-service-mode');
 const hotmailServiceModeButtons = Array.from(document.querySelectorAll('[data-hotmail-service-mode]'));
@@ -168,6 +178,7 @@ let settingsDirty = false;
 let settingsSaveInFlight = false;
 let settingsAutoSaveTimer = null;
 let cloudflareDomainEditMode = false;
+let cloudflareTempEmailDomainEditMode = false;
 let modalChoiceResolver = null;
 let currentModalActions = [];
 let modalResultBuilder = null;
@@ -195,14 +206,22 @@ const MAIL_PROVIDER_LOGIN_CONFIGS = {
   '163': {
     label: '163 邮箱',
     url: 'https://mail.163.com/',
+    buttonLabel: '登录',
   },
   '163-vip': {
     label: '163 VIP 邮箱',
-    url: 'https://vip.163.com/',
+    url: 'https://webmail.vip.163.com/',
+    buttonLabel: '登录',
   },
   qq: {
     label: 'QQ 邮箱',
     url: 'https://wx.mail.qq.com/',
+    buttonLabel: '登录',
+  },
+  'cloudflare-temp-email': {
+    label: 'Cloudflare Temp Email GitHub',
+    url: 'https://github.com/dreamhunter2333/cloudflare_temp_email',
+    buttonLabel: 'GitHub',
   },
   '2925': {
     label: '2925 邮箱',
@@ -865,6 +884,37 @@ function normalizeCloudflareDomains(values = []) {
   return domains;
 }
 
+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}`;
+  try {
+    const parsed = new URL(candidate);
+    parsed.hash = '';
+    parsed.search = '';
+    const pathname = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/+$/, '');
+    return `${parsed.origin}${pathname}`;
+  } catch {
+    return '';
+  }
+}
+
+function normalizeCloudflareTempEmailDomainValue(value = '') {
+  return normalizeCloudflareDomainValue(value);
+}
+
+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;
+}
+
 function getCloudflareDomainsFromState() {
   const domains = normalizeCloudflareDomains(latestState?.cloudflareDomains || []);
   const activeDomain = normalizeCloudflareDomainValue(latestState?.cloudflareDomain || '');
@@ -874,6 +924,15 @@ function getCloudflareDomainsFromState() {
   return { domains, activeDomain: activeDomain || domains[0] || '' };
 }
 
+function getCloudflareTempEmailDomainsFromState() {
+  const domains = normalizeCloudflareTempEmailDomains(latestState?.cloudflareTempEmailDomains || []);
+  const activeDomain = normalizeCloudflareTempEmailDomainValue(latestState?.cloudflareTempEmailDomain || '');
+  if (activeDomain && !domains.includes(activeDomain)) {
+    domains.unshift(activeDomain);
+  }
+  return { domains, activeDomain: activeDomain || domains[0] || '' };
+}
+
 function renderCloudflareDomainOptions(preferredDomain = '') {
   const preferred = normalizeCloudflareDomainValue(preferredDomain);
   const { domains, activeDomain } = getCloudflareDomainsFromState();
@@ -900,6 +959,32 @@ function renderCloudflareDomainOptions(preferredDomain = '') {
   selectCfDomain.value = domains.includes(selected) ? selected : domains[0];
 }
 
+function renderCloudflareTempEmailDomainOptions(preferredDomain = '') {
+  const preferred = normalizeCloudflareTempEmailDomainValue(preferredDomain);
+  const { domains, activeDomain } = getCloudflareTempEmailDomainsFromState();
+  const selected = preferred || activeDomain;
+
+  selectTempEmailDomain.innerHTML = '';
+  if (domains.length === 0) {
+    const option = document.createElement('option');
+    option.value = '';
+    option.textContent = '请先添加域名';
+    selectTempEmailDomain.appendChild(option);
+    selectTempEmailDomain.disabled = true;
+    selectTempEmailDomain.value = '';
+    return;
+  }
+
+  for (const domain of domains) {
+    const option = document.createElement('option');
+    option.value = domain;
+    option.textContent = domain;
+    selectTempEmailDomain.appendChild(option);
+  }
+  selectTempEmailDomain.disabled = false;
+  selectTempEmailDomain.value = domains.includes(selected) ? selected : domains[0];
+}
+
 function setCloudflareDomainEditMode(editing, options = {}) {
   const { clearInput = false } = options;
   cloudflareDomainEditMode = Boolean(editing);
@@ -916,11 +1001,31 @@ function setCloudflareDomainEditMode(editing, options = {}) {
   }
 }
 
+function setCloudflareTempEmailDomainEditMode(editing, options = {}) {
+  const { clearInput = false } = options;
+  cloudflareTempEmailDomainEditMode = Boolean(editing);
+  selectTempEmailDomain.style.display = cloudflareTempEmailDomainEditMode ? 'none' : '';
+  inputTempEmailDomain.style.display = cloudflareTempEmailDomainEditMode ? '' : 'none';
+  btnTempEmailDomainMode.textContent = cloudflareTempEmailDomainEditMode ? '保存' : '添加';
+  if (cloudflareTempEmailDomainEditMode) {
+    if (clearInput) {
+      inputTempEmailDomain.value = '';
+    }
+    inputTempEmailDomain.focus();
+  } else if (clearInput) {
+    inputTempEmailDomain.value = '';
+  }
+}
+
 function collectSettingsPayload() {
   const { domains, activeDomain } = getCloudflareDomainsFromState();
   const selectedCloudflareDomain = normalizeCloudflareDomainValue(
     !cloudflareDomainEditMode ? selectCfDomain.value : activeDomain
   ) || activeDomain;
+  const { domains: tempEmailDomains, activeDomain: tempEmailActiveDomain } = getCloudflareTempEmailDomainsFromState();
+  const selectedCloudflareTempEmailDomain = normalizeCloudflareTempEmailDomainValue(
+    !cloudflareTempEmailDomainEditMode ? selectTempEmailDomain.value : tempEmailActiveDomain
+  ) || tempEmailActiveDomain;
   return {
     panelMode: selectPanelMode.value,
     vpsUrl: inputVpsUrl.value.trim(),
@@ -942,6 +1047,11 @@ function collectSettingsPayload() {
     hotmailLocalBaseUrl: inputHotmailLocalBaseUrl.value.trim(),
     cloudflareDomain: selectedCloudflareDomain,
     cloudflareDomains: domains,
+    cloudflareTempEmailBaseUrl: normalizeCloudflareTempEmailBaseUrlValue(inputTempEmailBaseUrl.value),
+    cloudflareTempEmailAdminAuth: inputTempEmailAdminAuth.value,
+    cloudflareTempEmailCustomAuth: inputTempEmailCustomAuth.value,
+    cloudflareTempEmailDomain: selectedCloudflareTempEmailDomain,
+    cloudflareTempEmailDomains: tempEmailDomains,
     autoRunSkipFailures: inputAutoSkipFailures.checked,
     autoRunFallbackThreadIntervalMinutes: normalizeAutoRunThreadIntervalMinutes(inputAutoSkipFailuresThreadIntervalMinutes.value),
     autoRunDelayEnabled: inputAutoDelayEnabled.checked,
@@ -1233,7 +1343,7 @@ function applySettingsState(state) {
   inputSub2ApiPassword.value = state?.sub2apiPassword || '';
   inputSub2ApiGroup.value = state?.sub2apiGroupName || '';
   const restoredMailProvider = isCustomMailProvider(state?.mailProvider)
-    || ['hotmail-api', '163', '163-vip', 'qq', 'inbucket', '2925'].includes(String(state?.mailProvider || '').trim())
+    || ['hotmail-api', '163', '163-vip', 'qq', 'inbucket', '2925', 'cloudflare-temp-email'].includes(String(state?.mailProvider || '').trim())
     ? String(state?.mailProvider || '163').trim()
     : (String(state?.emailGenerator || '').trim().toLowerCase() === 'custom'
       || String(state?.emailGenerator || '').trim().toLowerCase() === 'manual'
@@ -1241,15 +1351,23 @@ function applySettingsState(state) {
       : '163');
   selectMailProvider.value = restoredMailProvider;
   setMail2925Mode(state?.mail2925Mode);
-  selectEmailGenerator.value = String(state?.emailGenerator || '').trim().toLowerCase() === 'cloudflare' ? 'cloudflare' : 'duck';
+  const restoredEmailGenerator = String(state?.emailGenerator || '').trim().toLowerCase();
+  selectEmailGenerator.value = ['cloudflare', 'cloudflare-temp-email'].includes(restoredEmailGenerator)
+    ? restoredEmailGenerator
+    : 'duck';
   inputEmailPrefix.value = state?.emailPrefix || '';
   inputInbucketHost.value = state?.inbucketHost || '';
   inputInbucketMailbox.value = state?.inbucketMailbox || '';
   setHotmailServiceMode(state?.hotmailServiceMode);
   inputHotmailRemoteBaseUrl.value = state?.hotmailRemoteBaseUrl || '';
   inputHotmailLocalBaseUrl.value = state?.hotmailLocalBaseUrl || '';
+  inputTempEmailBaseUrl.value = state?.cloudflareTempEmailBaseUrl || '';
+  inputTempEmailAdminAuth.value = state?.cloudflareTempEmailAdminAuth || '';
+  inputTempEmailCustomAuth.value = state?.cloudflareTempEmailCustomAuth || '';
   renderCloudflareDomainOptions(state?.cloudflareDomain || '');
   setCloudflareDomainEditMode(false, { clearInput: true });
+  renderCloudflareTempEmailDomainOptions(state?.cloudflareTempEmailDomain || '');
+  setCloudflareTempEmailDomainEditMode(false, { clearInput: true });
   inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures);
   inputAutoSkipFailuresThreadIntervalMinutes.value = String(normalizeAutoRunThreadIntervalMinutes(state?.autoRunFallbackThreadIntervalMinutes));
   inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
@@ -1516,13 +1634,18 @@ function isCustomMailProvider(provider = selectMailProvider.value) {
 
 function getSelectedEmailGenerator() {
   const generator = String(selectEmailGenerator.value || '').trim().toLowerCase();
-  if (generator === 'cloudflare') {
-    return 'cloudflare';
+  if (generator === 'custom' || generator === 'manual') {
+    return 'custom';
   }
+  if (generator === 'cloudflare') return 'cloudflare';
+  if (generator === 'cloudflare-temp-email') return 'cloudflare-temp-email';
   return 'duck';
 }
 
 function getEmailGeneratorUiCopy() {
+  if (getSelectedEmailGenerator() === 'custom') {
+    return getCustomMailProviderUiCopy();
+  }
   if (getSelectedEmailGenerator() === 'cloudflare') {
     return {
       buttonLabel: '生成',
@@ -1531,6 +1654,14 @@ function getEmailGeneratorUiCopy() {
       label: 'Cloudflare 邮箱',
     };
   }
+  if (getSelectedEmailGenerator() === 'cloudflare-temp-email') {
+    return {
+      buttonLabel: '生成 Temp',
+      placeholder: '点击生成 Cloudflare Temp Email,或手动粘贴邮箱',
+      successVerb: '生成',
+      label: 'Cloudflare Temp Email',
+    };
+  }
 
   return {
     buttonLabel: '获取',
@@ -1623,6 +1754,7 @@ function updateMailLoginButtonState() {
   const config = getMailProviderLoginConfig();
   const loginUrl = getMailProviderLoginUrl();
   btnMailLogin.disabled = !loginUrl;
+  btnMailLogin.textContent = config?.buttonLabel || '登录';
   btnMailLogin.title = loginUrl ? `打开 ${config.label} 登录页` : '当前邮箱服务没有可跳转的登录页';
 }
 
@@ -1841,6 +1973,7 @@ function updateMailProviderUI() {
   const useHotmail = selectMailProvider.value === 'hotmail-api';
   const useCustomEmail = isCustomMailProvider();
   const useEmailGenerator = !useHotmail && !useGeneratedAlias && !useCustomEmail;
+  const useCloudflareTempEmailProvider = selectMailProvider.value === 'cloudflare-temp-email';
   updateMailLoginButtonState();
   if (rowMail2925Mode) {
     rowMail2925Mode.style.display = use2925 ? '' : 'none';
@@ -1850,7 +1983,10 @@ function updateMailProviderUI() {
   rowInbucketHost.style.display = useInbucket ? '' : 'none';
   rowInbucketMailbox.style.display = useInbucket ? '' : 'none';
   const useCloudflare = selectEmailGenerator.value === 'cloudflare';
+  const useCloudflareTempEmailGenerator = selectEmailGenerator.value === 'cloudflare-temp-email';
   const showCloudflareDomain = useEmailGenerator && useCloudflare;
+  const showCloudflareTempEmailSettings = useCloudflareTempEmailProvider || (useEmailGenerator && useCloudflareTempEmailGenerator);
+  const showCloudflareTempEmailDomain = useEmailGenerator && useCloudflareTempEmailGenerator;
   if (rowEmailGenerator) {
     rowEmailGenerator.style.display = useEmailGenerator ? '' : 'none';
   }
@@ -1861,6 +1997,16 @@ function updateMailProviderUI() {
   } else {
     setCloudflareDomainEditMode(false, { clearInput: false });
   }
+  rowTempEmailBaseUrl.style.display = showCloudflareTempEmailSettings ? '' : 'none';
+  rowTempEmailAdminAuth.style.display = showCloudflareTempEmailSettings ? '' : 'none';
+  rowTempEmailCustomAuth.style.display = showCloudflareTempEmailSettings ? '' : 'none';
+  rowTempEmailDomain.style.display = showCloudflareTempEmailDomain ? '' : 'none';
+  const { domains: tempEmailDomains } = getCloudflareTempEmailDomainsFromState();
+  if (showCloudflareTempEmailDomain) {
+    setCloudflareTempEmailDomainEditMode(cloudflareTempEmailDomainEditMode || tempEmailDomains.length === 0, { clearInput: false });
+  } else {
+    setCloudflareTempEmailDomainEditMode(false, { clearInput: false });
+  }
 
   if (hotmailSection) {
     hotmailSection.style.display = useHotmail ? '' : 'none';
@@ -1932,6 +2078,38 @@ async function saveCloudflareDomainSettings(domains, activeDomain, options = {})
   }
 }
 
+async function saveCloudflareTempEmailDomainSettings(domains, activeDomain, options = {}) {
+  const { silent = false } = options;
+  const normalizedDomains = normalizeCloudflareTempEmailDomains(domains);
+  const normalizedActiveDomain = normalizeCloudflareTempEmailDomainValue(activeDomain) || normalizedDomains[0] || '';
+  const payload = {
+    cloudflareTempEmailDomain: normalizedActiveDomain,
+    cloudflareTempEmailDomains: normalizedDomains,
+  };
+
+  const response = await chrome.runtime.sendMessage({
+    type: 'SAVE_SETTING',
+    source: 'sidepanel',
+    payload,
+  });
+
+  if (response?.error) {
+    throw new Error(response.error);
+  }
+
+  syncLatestState({
+    ...payload,
+  });
+  renderCloudflareTempEmailDomainOptions(normalizedActiveDomain);
+  setCloudflareTempEmailDomainEditMode(false, { clearInput: true });
+  markSettingsDirty(false);
+  updateMailProviderUI();
+
+  if (!silent) {
+    showToast('Cloudflare Temp Email 域名已保存', 'success', 1800);
+  }
+}
+
 function updatePanelModeUI() {
   const useSub2Api = selectPanelMode.value === 'sub2api';
   rowVpsUrl.style.display = useSub2Api ? 'none' : '';
@@ -3102,6 +3280,14 @@ selectCfDomain.addEventListener('change', () => {
   saveSettings({ silent: true }).catch(() => { });
 });
 
+selectTempEmailDomain.addEventListener('change', () => {
+  if (selectTempEmailDomain.disabled) {
+    return;
+  }
+  markSettingsDirty(true);
+  saveSettings({ silent: true }).catch(() => { });
+});
+
 btnCfDomainMode.addEventListener('click', async () => {
   try {
     if (!cloudflareDomainEditMode) {
@@ -3123,6 +3309,27 @@ 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);
+  } catch (err) {
+    showToast(err.message, 'error');
+  }
+});
+
 inputCfDomain.addEventListener('keydown', (event) => {
   if (event.key === 'Enter') {
     event.preventDefault();
@@ -3130,6 +3337,13 @@ inputCfDomain.addEventListener('keydown', (event) => {
   }
 });
 
+inputTempEmailDomain.addEventListener('keydown', (event) => {
+  if (event.key === 'Enter') {
+    event.preventDefault();
+    btnTempEmailDomainMode.click();
+  }
+});
+
 inputSub2ApiUrl.addEventListener('input', () => {
   markSettingsDirty(true);
   scheduleSettingsAutoSave();
@@ -3211,6 +3425,31 @@ inputAutoSkipFailures.addEventListener('change', async () => {
   saveSettings({ silent: true }).catch(() => { });
 });
 
+inputTempEmailBaseUrl.addEventListener('input', () => {
+  markSettingsDirty(true);
+  scheduleSettingsAutoSave();
+});
+inputTempEmailBaseUrl.addEventListener('blur', () => {
+  inputTempEmailBaseUrl.value = normalizeCloudflareTempEmailBaseUrlValue(inputTempEmailBaseUrl.value);
+  saveSettings({ silent: true }).catch(() => { });
+});
+
+inputTempEmailAdminAuth.addEventListener('input', () => {
+  markSettingsDirty(true);
+  scheduleSettingsAutoSave();
+});
+inputTempEmailAdminAuth.addEventListener('blur', () => {
+  saveSettings({ silent: true }).catch(() => { });
+});
+
+inputTempEmailCustomAuth.addEventListener('input', () => {
+  markSettingsDirty(true);
+  scheduleSettingsAutoSave();
+});
+inputTempEmailCustomAuth.addEventListener('blur', () => {
+  saveSettings({ silent: true }).catch(() => { });
+});
+
 inputAutoSkipFailuresThreadIntervalMinutes.addEventListener('input', () => {
   markSettingsDirty(true);
   scheduleSettingsAutoSave();
@@ -3366,6 +3605,18 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
         displayLocalhostUrl.textContent = message.payload.localhostUrl || '等待中...';
         displayLocalhostUrl.classList.toggle('has-value', Boolean(message.payload.localhostUrl));
       }
+      if (message.payload.cloudflareTempEmailBaseUrl !== undefined) {
+        inputTempEmailBaseUrl.value = message.payload.cloudflareTempEmailBaseUrl || '';
+      }
+      if (message.payload.cloudflareTempEmailAdminAuth !== undefined) {
+        inputTempEmailAdminAuth.value = message.payload.cloudflareTempEmailAdminAuth || '';
+      }
+      if (message.payload.cloudflareTempEmailCustomAuth !== undefined) {
+        inputTempEmailCustomAuth.value = message.payload.cloudflareTempEmailCustomAuth || '';
+      }
+      if (message.payload.cloudflareTempEmailDomain !== undefined || message.payload.cloudflareTempEmailDomains !== undefined) {
+        renderCloudflareTempEmailDomainOptions(message.payload.cloudflareTempEmailDomain || latestState?.cloudflareTempEmailDomain || '');
+      }
       if (message.payload.currentHotmailAccountId !== undefined || message.payload.hotmailAccounts !== undefined) {
         renderHotmailAccounts();
         if (selectMailProvider.value === 'hotmail-api') {

+ 9 - 0
tests/auto-run-fresh-attempt-reset.test.js

@@ -60,11 +60,20 @@ const bundle = [
   extractFunction('hasSavedProgress'),
   extractFunction('getRunningSteps'),
   extractFunction('getAutoRunStatusPayload'),
+  extractFunction('createAutoRunRoundSummary'),
+  extractFunction('normalizeAutoRunRoundSummary'),
+  extractFunction('buildAutoRunRoundSummaries'),
+  extractFunction('serializeAutoRunRoundSummaries'),
+  extractFunction('getAutoRunRoundRetryCount'),
+  extractFunction('formatAutoRunFailureReasons'),
+  extractFunction('logAutoRunFinalSummary'),
+  extractFunction('waitBetweenAutoRunRounds'),
   extractFunction('autoRunLoop'),
 ].join('\n');
 
 const api = new Function(`
 const STOP_ERROR_MESSAGE = 'Flow stopped.';
+const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
 const DEFAULT_STATE = {
   stepStatuses: {
     1: 'pending',

+ 160 - 0
tests/cloudflare-temp-email-provider.test.js

@@ -0,0 +1,160 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('fs');
+
+const source = fs.readFileSync('background.js', 'utf8');
+
+function extractFunction(name) {
+  const markers = [`async function ${name}(`, `function ${name}(`];
+  const start = markers
+    .map((marker) => source.indexOf(marker))
+    .find((index) => index >= 0);
+  if (start < 0) {
+    throw new Error(`missing function ${name}`);
+  }
+
+  let parenDepth = 0;
+  let signatureEnded = false;
+  let braceStart = -1;
+  for (let i = start; i < source.length; i += 1) {
+    const ch = source[i];
+    if (ch === '(') {
+      parenDepth += 1;
+    } else if (ch === ')') {
+      parenDepth -= 1;
+      if (parenDepth === 0) {
+        signatureEnded = true;
+      }
+    } else if (ch === '{' && signatureEnded) {
+      braceStart = i;
+      break;
+    }
+  }
+  if (braceStart < 0) {
+    throw new Error(`missing body for function ${name}`);
+  }
+
+  let depth = 0;
+  let end = braceStart;
+  for (; end < source.length; end += 1) {
+    const ch = source[end];
+    if (ch === '{') depth += 1;
+    if (ch === '}') {
+      depth -= 1;
+      if (depth === 0) {
+        end += 1;
+        break;
+      }
+    }
+  }
+
+  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: [{
+      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.',
+    }],
+  };
+}
+function pickVerificationMessageWithTimeFallback(messages) {
+  return {
+    match: {
+      code: '123456',
+      receivedAt: Date.parse(messages[0].receivedDateTime),
+      message: messages[0],
+    },
+    usedRelaxedFilters: false,
+    usedTimeFallback: false,
+  };
+}
+async function deleteCloudflareTempEmailMail() {
+  throw new Error('delete failed');
+}
+
+${bundle}
+
+return {
+  pollCloudflareTempEmailVerificationCode,
+  snapshot() {
+    return { logs };
+  },
+};
+`)();
+
+  const result = await api.pollCloudflareTempEmailVerificationCode(4, { email: 'user@example.com' }, {
+    targetEmail: 'user@example.com',
+    maxAttempts: 1,
+    intervalMs: 1,
+  });
+
+  assert.equal(result.code, '123456');
+  const state = api.snapshot();
+  assert.equal(state.logs.some((entry) => entry.message.includes('删除 Cloudflare Temp Email 邮件失败')), true);
+});
+
+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 };
+`)();
+
+  await assert.rejects(
+    api.pollCloudflareTempEmailVerificationCode(4, {}, {}),
+    /缺少目标邮箱地址/
+  );
+});

+ 107 - 0
tests/cloudflare-temp-email-utils.test.js

@@ -0,0 +1,107 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+
+const {
+  buildCloudflareTempEmailHeaders,
+  getCloudflareTempEmailAddressFromResponse,
+  normalizeCloudflareTempEmailBaseUrl,
+  normalizeCloudflareTempEmailDomain,
+  normalizeCloudflareTempEmailDomains,
+  normalizeCloudflareTempEmailMailApiMessages,
+} = require('../cloudflare-temp-email-utils.js');
+
+test('normalizeCloudflareTempEmailBaseUrl normalizes host and preserves path', () => {
+  assert.equal(
+    normalizeCloudflareTempEmailBaseUrl('temp.example.com/api/'),
+    'https://temp.example.com/api'
+  );
+  assert.equal(
+    normalizeCloudflareTempEmailBaseUrl('http://127.0.0.1:8787'),
+    'http://127.0.0.1:8787'
+  );
+  assert.equal(normalizeCloudflareTempEmailBaseUrl('::::'), '');
+});
+
+test('normalizeCloudflareTempEmailDomain and domains de-duplicate valid entries', () => {
+  assert.equal(normalizeCloudflareTempEmailDomain('@Mail.Example.com'), 'mail.example.com');
+  assert.equal(normalizeCloudflareTempEmailDomain('not-a-domain'), '');
+  assert.deepEqual(
+    normalizeCloudflareTempEmailDomains(['mail.example.com', 'MAIL.EXAMPLE.COM', 'bad-value']),
+    ['mail.example.com']
+  );
+});
+
+test('buildCloudflareTempEmailHeaders includes auth headers and content type when needed', () => {
+  assert.deepEqual(
+    buildCloudflareTempEmailHeaders(
+      {
+        adminAuth: 'admin-secret',
+        customAuth: 'site-secret',
+      },
+      { json: true }
+    ),
+    {
+      'x-admin-auth': 'admin-secret',
+      'x-custom-auth': 'site-secret',
+      'Content-Type': 'application/json',
+      Accept: 'application/json',
+    }
+  );
+});
+
+test('normalizeCloudflareTempEmailMailApiMessages extracts sender, subject, code, and address from raw mime', () => {
+  const messages = normalizeCloudflareTempEmailMailApiMessages({
+    data: [
+      {
+        id: 'mail-1',
+        address: 'user@example.com',
+        created_at: '2026-04-13T09:15:00.000Z',
+        raw: [
+          'From: OpenAI <noreply@tm.openai.com>',
+          'Subject: =?UTF-8?B?T3BlbkFJIHZlcmlmaWNhdGlvbiBjb2Rl?=',
+          'Content-Type: text/plain; charset=UTF-8',
+          '',
+          'Your verification code is 654321.',
+        ].join('\r\n'),
+      },
+    ],
+  });
+
+  assert.equal(messages.length, 1);
+  assert.equal(messages[0].id, 'mail-1');
+  assert.equal(messages[0].address, 'user@example.com');
+  assert.equal(messages[0].subject, 'OpenAI verification code');
+  assert.equal(messages[0].from.emailAddress.address, 'OpenAI <noreply@tm.openai.com>');
+  assert.match(messages[0].bodyPreview, /654321/);
+});
+
+test('normalizeCloudflareTempEmailMailApiMessages decodes multipart quoted printable html bodies', () => {
+  const messages = normalizeCloudflareTempEmailMailApiMessages([
+    {
+      id: 'mail-2',
+      address: 'user@example.com',
+      received_at: '2026-04-13T09:20:00.000Z',
+      source: [
+        'From: ChatGPT <noreply@tm.openai.com>',
+        'Subject: Login code',
+        'Content-Type: multipart/alternative; boundary="abc123"',
+        '',
+        '--abc123',
+        'Content-Type: text/html; charset=UTF-8',
+        'Content-Transfer-Encoding: quoted-printable',
+        '',
+        '<p>Your login code is <strong>112233</strong>.</p>',
+        '--abc123--',
+      ].join('\r\n'),
+    },
+  ]);
+
+  assert.equal(messages.length, 1);
+  assert.match(messages[0].bodyPreview, /112233/);
+  assert.equal(messages[0].subject, 'Login code');
+});
+
+test('getCloudflareTempEmailAddressFromResponse supports direct and nested response shapes', () => {
+  assert.equal(getCloudflareTempEmailAddressFromResponse({ address: 'one@example.com' }), 'one@example.com');
+  assert.equal(getCloudflareTempEmailAddressFromResponse({ data: { address: 'two@example.com' } }), 'two@example.com');
+});

+ 8 - 0
tests/step9-localhost-cleanup-scope.test.js

@@ -52,7 +52,12 @@ function extractFunction(name) {
 
 const bundle = [
   extractFunction('getTabRegistry'),
+  extractFunction('normalizeEmailGenerator'),
   extractFunction('parseUrlSafely'),
+  extractFunction('isHotmailProvider'),
+  extractFunction('isCustomMailProvider'),
+  extractFunction('isGeneratedAliasProvider'),
+  extractFunction('shouldUseCustomRegistrationEmail'),
   extractFunction('isLocalhostOAuthCallbackUrl'),
   extractFunction('isLocalhostOAuthCallbackTabMatch'),
   extractFunction('closeLocalhostCallbackTabs'),
@@ -62,6 +67,9 @@ const bundle = [
 ].join('\n');
 
 const api = new Function(`
+const HOTMAIL_PROVIDER = 'hotmail-api';
+const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
+const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
 let currentState = {
   tabRegistry: {
     'signup-page': { tabId: 1, ready: true },

+ 4 - 2
tests/verification-stop-propagation.test.js

@@ -57,10 +57,11 @@ async function testPollFreshVerificationCodeRethrowsStop() {
     extractFunction('pollFreshVerificationCode'),
   ].join('\n');
 
-  const api = new Function(`
+const api = new Function(`
 let stopRequested = false;
 const STOP_ERROR_MESSAGE = '流程已被用户停止。';
 const HOTMAIL_PROVIDER = 'hotmail-api';
+const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
 const VERIFICATION_POLL_MAX_ROUNDS = 5;
 const logs = [];
 let resendCalls = 0;
@@ -119,9 +120,10 @@ async function testResolveVerificationStepRethrowsStopFromFreshRequest() {
     extractFunction('resolveVerificationStep'),
   ].join('\n');
 
-  const api = new Function(`
+const api = new Function(`
 const STOP_ERROR_MESSAGE = '流程已被用户停止。';
 const HOTMAIL_PROVIDER = 'hotmail-api';
+const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
 const logs = [];
 let pollCalls = 0;