From 0614917ad4d723564ee97560eb7fc6e7e26465f3 Mon Sep 17 00:00:00 2001 From: Twelveeee Date: Mon, 13 Apr 2026 17:27:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9ECloudflareTempEmail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 303 +++++++++++++- cloudflare-temp-email-utils.js | 400 +++++++++++++++++++ sidepanel/sidepanel.html | 23 ++ sidepanel/sidepanel.js | 261 +++++++++++- tests/auto-run-fresh-attempt-reset.test.js | 8 + tests/cloudflare-temp-email-provider.test.js | 160 ++++++++ tests/cloudflare-temp-email-utils.test.js | 107 +++++ tests/step9-localhost-cleanup-scope.test.js | 7 + tests/verification-stop-propagation.test.js | 6 +- 9 files changed, 1258 insertions(+), 17 deletions(-) create mode 100644 cloudflare-temp-email-utils.js create mode 100644 tests/cloudflare-temp-email-provider.test.js create mode 100644 tests/cloudflare-temp-email-utils.test.js diff --git a/background.js b/background.js index 11fd5ec..d28953a 100644 --- a/background.js +++ b/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; @@ -81,6 +94,11 @@ const PERSISTED_SETTING_DEFAULTS = { hotmailLocalBaseUrl: DEFAULT_HOTMAIL_LOCAL_BASE_URL, cloudflareDomain: '', cloudflareDomains: [], + cloudflareTempEmailBaseUrl: '', + cloudflareTempEmailAdminAuth: '', + cloudflareTempEmailCustomAuth: '', + cloudflareTempEmailDomain: '', + cloudflareTempEmailDomains: [], hotmailAccounts: [], }; @@ -218,9 +236,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'; } @@ -233,6 +250,7 @@ function normalizeMailProvider(value = '') { switch (normalized) { case 'custom': case HOTMAIL_PROVIDER: + case CLOUDFLARE_TEMP_EMAIL_PROVIDER: case '163': case '163-vip': case 'qq': @@ -330,6 +348,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': @@ -379,6 +407,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: @@ -422,6 +459,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; } @@ -1302,6 +1346,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 // ============================================================ @@ -1532,11 +1690,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); @@ -2124,6 +2285,7 @@ function getSourceLabel(source) { 'inbucket-mail': 'Inbucket 邮箱', 'duck-mail': 'Duck 邮箱', 'hotmail-api': 'Hotmail(远程/本地)', + 'cloudflare-temp-email': 'Cloudflare Temp Email', }; return labels[source] || source || '未知来源'; } @@ -3141,7 +3303,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); @@ -3434,7 +3599,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() { @@ -3474,6 +3641,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; @@ -3508,6 +3776,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); } @@ -3610,7 +3881,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; } } @@ -4732,6 +5006,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 邮箱' }; } @@ -4896,6 +5173,12 @@ async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) ...pollOverrides, }); } + if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { + return pollCloudflareTempEmailVerificationCode(step, state, { + ...getVerificationPollPayload(step, state), + ...pollOverrides, + }); + } const stateKey = getVerificationCodeStateKey(step); const rejectedCodes = new Set(); @@ -5111,7 +5394,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}...`); @@ -5246,7 +5529,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}...`); diff --git a/cloudflare-temp-email-utils.js b/cloudflare-temp-email-utils.js new file mode 100644 index 0000000..e7a8992 --- /dev/null +++ b/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(//gi, ' ') + .replace(//gi, ' ') + .replace(/<[^>]+>/g, ' ') + .replace(/ /gi, ' ') + .replace(/&/gi, '&') + .replace(/</gi, '<') + .replace(/>/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, + }; +}); diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 944b7e7..45ba375 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -159,6 +159,7 @@ + @@ -168,8 +169,30 @@ + + + +