diff --git a/background.js b/background.js index 8d5931d..868cf69 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', 'luckmail-utils.js', 'content/activation-utils.js'); +importScripts('data/names.js', 'hotmail-utils.js', 'luckmail-utils.js', 'cloudflare-temp-email-utils.js', 'icloud-utils.js', 'content/activation-utils.js'); const { buildHotmailMailApiLatestUrl, @@ -44,14 +44,47 @@ const { normalizeTimestamp: normalizeLuckmailTimestamp, pickLuckmailVerificationMail, } = self.LuckMailUtils; +const { + DEFAULT_MAIL_PAGE_SIZE: CLOUDFLARE_TEMP_EMAIL_DEFAULT_PAGE_SIZE, + buildCloudflareTempEmailHeaders, + getCloudflareTempEmailAddressFromResponse, + joinCloudflareTempEmailUrl, + normalizeCloudflareTempEmailAddress, + normalizeCloudflareTempEmailBaseUrl, + normalizeCloudflareTempEmailDomain, + normalizeCloudflareTempEmailDomains, + normalizeCloudflareTempEmailMailApiMessages, +} = self.CloudflareTempEmailUtils; +const { + findIcloudAliasByEmail, + getConfiguredIcloudHostPreference, + getIcloudHostHintFromMessage, + getIcloudLoginUrlForHost, + getIcloudSetupUrlForHost, + normalizeBooleanMap, + normalizeIcloudAliasList, + normalizeIcloudHost, + pickReusableIcloudAlias, + toNormalizedEmailSet, +} = self.IcloudUtils; const { isRecoverableStep9AuthFailure, } = self.MultiPageActivationUtils; const LOG_PREFIX = '[MultiPage:bg]'; const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill'; +const ICLOUD_SETUP_URLS = [ + 'https://setup.icloud.com.cn/setup/ws/1', + 'https://setup.icloud.com/setup/ws/1', +]; +const ICLOUD_LOGIN_URLS = [ + 'https://www.icloud.com.cn/', + 'https://www.icloud.com/', +]; const HOTMAIL_PROVIDER = 'hotmail-api'; const LUCKMAIL_PROVIDER = 'luckmail-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; @@ -78,6 +111,7 @@ const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373'; const HOTMAIL_LOCAL_HELPER_TIMEOUT_MS = 45000; const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai'; const DISPLAY_TIMEZONE = 'Asia/Shanghai'; +const PERSISTENT_ALIAS_STATE_KEYS = ['manualAliasUsage', 'preservedAliases']; initializeSessionStorageAccess(); @@ -102,6 +136,8 @@ const PERSISTED_SETTING_DEFAULTS = { autoStepDelaySeconds: null, mailProvider: '163', emailGenerator: 'duck', + autoDeleteUsedIcloudAlias: false, + icloudHostPreference: 'auto', emailPrefix: '', inbucketHost: '', inbucketMailbox: '', @@ -110,6 +146,11 @@ const PERSISTED_SETTING_DEFAULTS = { hotmailLocalBaseUrl: DEFAULT_HOTMAIL_LOCAL_BASE_URL, cloudflareDomain: '', cloudflareDomains: [], + cloudflareTempEmailBaseUrl: '', + cloudflareTempEmailAdminAuth: '', + cloudflareTempEmailCustomAuth: '', + cloudflareTempEmailDomain: '', + cloudflareTempEmailDomains: [], hotmailAccounts: [], }; @@ -127,6 +168,8 @@ const DEFAULT_STATE = { email: null, // 运行时邮箱,由程序自动获取并写入,不能手动预填。 password: null, // 运行时实际密码,由 customPassword 或程序自动生成后写入。 accounts: [], // 已生成账号记录:{ email, password, createdAt }。 + manualAliasUsage: {}, + preservedAliases: {}, lastEmailTimestamp: null, // 最近一次获取到邮箱数据的运行时时间戳。 lastSignupCode: null, // 注册验证码,运行时由程序自动读取并写入。 lastLoginCode: null, // 登录验证码,运行时由程序自动读取并写入。 @@ -163,6 +206,7 @@ const DEFAULT_STATE = { signupVerificationRequestedAt: null, loginVerificationRequestedAt: null, currentHotmailAccountId: null, + preferredIcloudHost: '', }; function normalizeAutoRunDelayMinutes(value) { @@ -256,9 +300,11 @@ function normalizeEmailGenerator(value = '') { if (normalized === 'custom' || normalized === 'manual') { return 'custom'; } - if (normalized === 'cloudflare') { - return 'cloudflare'; + if (normalized === 'icloud') { + return 'icloud'; } + if (normalized === 'cloudflare') return 'cloudflare'; + if (normalized === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return CLOUDFLARE_TEMP_EMAIL_GENERATOR; return 'duck'; } @@ -272,6 +318,7 @@ function normalizeMailProvider(value = '') { case 'custom': case HOTMAIL_PROVIDER: case LUCKMAIL_PROVIDER: + case CLOUDFLARE_TEMP_EMAIL_PROVIDER: case '163': case '163-vip': case 'qq': @@ -411,6 +458,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': @@ -444,6 +501,10 @@ function normalizePersistentSettingValue(key, value) { return normalizeMailProvider(value); case 'emailGenerator': return normalizeEmailGenerator(value); + case 'autoDeleteUsedIcloudAlias': + return Boolean(value); + case 'icloudHostPreference': + return normalizeIcloudHost(value) || 'auto'; case 'emailPrefix': return String(value || '').trim(); case 'inbucketHost': @@ -460,6 +521,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: @@ -503,6 +573,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; } @@ -512,12 +589,29 @@ async function getPersistedSettings() { return buildPersistentSettingsPayload(stored, { fillDefaults: true }); } +async function getPersistedAliasState() { + try { + const stored = await chrome.storage.local.get(PERSISTENT_ALIAS_STATE_KEYS); + return { + manualAliasUsage: normalizeBooleanMap(stored.manualAliasUsage), + preservedAliases: normalizeBooleanMap(stored.preservedAliases), + }; + } catch (err) { + console.warn(LOG_PREFIX, 'Failed to read persisted iCloud alias state:', err?.message || err); + return { + manualAliasUsage: {}, + preservedAliases: {}, + }; + } +} + async function getState() { - const [state, persistedSettings] = await Promise.all([ + const [state, persistedSettings, persistedAliasState] = await Promise.all([ chrome.storage.session.get(null), getPersistedSettings(), + getPersistedAliasState(), ]); - return { ...DEFAULT_STATE, ...persistedSettings, ...state }; + return { ...DEFAULT_STATE, ...persistedSettings, ...persistedAliasState, ...state }; } async function initializeSessionStorageAccess() { @@ -537,6 +631,16 @@ async function setState(updates) { console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200)); if (Object.keys(updates || {}).length > 0) { await chrome.storage.session.set(updates); + const persistentAliasUpdates = {}; + if (Object.prototype.hasOwnProperty.call(updates, 'manualAliasUsage')) { + persistentAliasUpdates.manualAliasUsage = normalizeBooleanMap(updates.manualAliasUsage); + } + if (Object.prototype.hasOwnProperty.call(updates, 'preservedAliases')) { + persistentAliasUpdates.preservedAliases = normalizeBooleanMap(updates.preservedAliases); + } + if (Object.keys(persistentAliasUpdates).length > 0) { + await chrome.storage.local.set(persistentAliasUpdates); + } } } @@ -615,6 +719,13 @@ function broadcastDataUpdate(payload) { }).catch(() => { }); } +function broadcastIcloudAliasesChanged(payload = {}) { + chrome.runtime.sendMessage({ + type: 'ICLOUD_ALIASES_CHANGED', + payload, + }).catch(() => { }); +} + async function setEmailStateSilently(email) { await setState({ email }); broadcastDataUpdate({ email }); @@ -712,10 +823,62 @@ async function clearLuckmailRuntimeState(options = {}) { broadcastDataUpdate(updates); } +function getManualAliasUsageMap(state) { + return normalizeBooleanMap(state?.manualAliasUsage); +} + +function getPreservedAliasMap(state) { + return normalizeBooleanMap(state?.preservedAliases); +} + +function isAliasPreserved(state, email) { + const normalizedEmail = String(email || '').trim().toLowerCase(); + if (!normalizedEmail) return false; + return Boolean(getPreservedAliasMap(state)[normalizedEmail]); +} + +function getEffectiveUsedEmails(state) { + return toNormalizedEmailSet(getManualAliasUsageMap(state)); +} + +async function setIcloudAliasUsedState(payload = {}, options = {}) { + const email = String(payload.email || '').trim().toLowerCase(); + if (!email) { + throw new Error('未提供 iCloud 隐私邮箱地址。'); + } + + const used = Boolean(payload.used); + const state = await getState(); + const manualAliasUsage = getManualAliasUsageMap(state); + manualAliasUsage[email] = used; + await setState({ manualAliasUsage }); + if (!options.silentLog) { + await addLog(`iCloud:已将 ${email} 标记为${used ? '已用' : '未用'}`, 'ok'); + } + broadcastIcloudAliasesChanged({ reason: 'used-updated', email, used }); + return { email, used }; +} + +async function setIcloudAliasPreservedState(payload = {}) { + const email = String(payload.email || '').trim().toLowerCase(); + if (!email) { + throw new Error('未提供 iCloud 隐私邮箱地址。'); + } + + const preserved = Boolean(payload.preserved); + const state = await getState(); + const preservedAliases = getPreservedAliasMap(state); + preservedAliases[email] = preserved; + await setState({ preservedAliases }); + await addLog(`iCloud:已将 ${email} ${preserved ? '设为保留' : '取消保留'}`, 'ok'); + broadcastIcloudAliasesChanged({ reason: 'preserved-updated', email, preserved }); + return { email, preserved }; +} + async function resetState() { console.log(LOG_PREFIX, 'Resetting all state'); // Preserve settings and persistent data across resets - const [prev, persistedSettings] = await Promise.all([ + const [prev, persistedSettings, persistedAliasState] = await Promise.all([ chrome.storage.session.get([ 'seenCodes', 'seenInbucketMailIds', @@ -729,13 +892,16 @@ async function resetState() { 'luckmailUsedPurchases', 'luckmailPreserveTagId', 'luckmailPreserveTagName', + 'preferredIcloudHost', ]), getPersistedSettings(), + getPersistedAliasState(), ]); await chrome.storage.session.clear(); await chrome.storage.session.set({ ...DEFAULT_STATE, ...persistedSettings, + ...persistedAliasState, seenCodes: prev.seenCodes || [], seenInbucketMailIds: prev.seenInbucketMailIds || [], accounts: prev.accounts || [], @@ -750,6 +916,7 @@ async function resetState() { luckmailPreserveTagName: String(prev.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME, currentLuckmailPurchase: null, currentLuckmailMailCursor: null, + preferredIcloudHost: prev.preferredIcloudHost || '', }); } @@ -2183,6 +2350,522 @@ async function pollLuckmailVerificationCode(step, state, pollPayload = {}) { throw lastError || new Error(`步骤 ${step}:未在 LuckMail 邮箱中找到新的匹配验证码。`); } +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 中找到新的匹配验证码。`); +} + +async function getOpenIcloudHostPreference() { + try { + const tabs = await chrome.tabs.query({ + url: [ + 'https://www.icloud.com/*', + 'https://www.icloud.com.cn/*', + ], + }); + + const activeTab = tabs.find((tab) => tab.active); + const candidates = activeTab ? [activeTab, ...tabs.filter((tab) => tab.id !== activeTab.id)] : tabs; + for (const tab of candidates) { + try { + const host = normalizeIcloudHost(new URL(tab.url).host); + if (host) return host; + } catch {} + } + } catch {} + + return ''; +} + +async function getPreferredIcloudLoginUrl(error = null, state = null) { + const currentState = state || await getState(); + const configuredHost = getConfiguredIcloudHostPreference(currentState); + if (configuredHost) { + return getIcloudLoginUrlForHost(configuredHost); + } + + const messageHint = getIcloudHostHintFromMessage(getErrorMessage(error)); + if (messageHint) { + return getIcloudLoginUrlForHost(messageHint); + } + + const savedHost = normalizeIcloudHost(currentState?.preferredIcloudHost); + if (savedHost) { + return getIcloudLoginUrlForHost(savedHost); + } + + const openHost = await getOpenIcloudHostPreference(); + if (openHost) { + return getIcloudLoginUrlForHost(openHost); + } + + return ICLOUD_LOGIN_URLS[0]; +} + +async function getPreferredIcloudSetupUrls(state = null, error = null) { + const preferredLoginUrl = await getPreferredIcloudLoginUrl(error, state); + const preferredHost = normalizeIcloudHost(new URL(preferredLoginUrl).host); + const preferredSetupUrl = getIcloudSetupUrlForHost(preferredHost); + if (!preferredSetupUrl) { + return [...ICLOUD_SETUP_URLS]; + } + return [ + preferredSetupUrl, + ...ICLOUD_SETUP_URLS.filter((url) => url !== preferredSetupUrl), + ]; +} + +function isIcloudLoginRequiredError(error) { + const message = getErrorMessage(error).toLowerCase(); + return message.includes('could not validate icloud session') + || message.includes('hide my email service was unavailable') + || /\bstatus (401|403|409|421)\b/.test(message); +} + +let lastIcloudLoginPromptAt = 0; + +async function openIcloudLoginPage(preferredUrl) { + const tabs = await chrome.tabs.query({ + url: [ + 'https://www.icloud.com/*', + 'https://www.icloud.com.cn/*', + ], + }); + const preferredHost = new URL(preferredUrl).host; + const existing = tabs.find((tab) => { + try { + return new URL(tab.url).host === preferredHost; + } catch { + return false; + } + }); + + if (existing?.id) { + await chrome.tabs.update(existing.id, { active: true }); + if (existing.url !== preferredUrl) { + await chrome.tabs.update(existing.id, { url: preferredUrl }); + } + return existing.id; + } + + const created = await chrome.tabs.create({ url: preferredUrl, active: true }); + return created.id; +} + +async function promptIcloudLogin(error, actionLabel = 'iCloud 操作') { + const now = Date.now(); + const preferredUrl = await getPreferredIcloudLoginUrl(error); + const originalError = getErrorMessage(error); + + chrome.runtime.sendMessage({ + type: 'ICLOUD_LOGIN_REQUIRED', + payload: { + actionLabel, + loginUrl: preferredUrl, + message: '需要先登录 iCloud,我已经为你打开登录页。', + detail: originalError, + }, + }).catch(() => { }); + + if (now - lastIcloudLoginPromptAt < 15000) { + return; + } + lastIcloudLoginPromptAt = now; + + await addLog(`iCloud:${actionLabel}时需要登录,正在打开 ${new URL(preferredUrl).host} ...`, 'warn'); + + try { + await openIcloudLoginPage(preferredUrl); + } catch (tabErr) { + await addLog(`iCloud:自动打开登录页失败:${getErrorMessage(tabErr)}`, 'warn'); + } +} + +async function withIcloudLoginHelp(actionLabel, action) { + try { + return await action(); + } catch (err) { + if (isIcloudLoginRequiredError(err)) { + await promptIcloudLogin(err, actionLabel); + throw new Error('请先在新打开的 iCloud 页面中完成登录,再回来点击“我已登录”。'); + } + throw err; + } +} + +async function icloudRequest(method, url, options = {}) { + const { data } = options; + let response; + try { + response = await fetch(url, { + method, + credentials: 'include', + headers: data !== undefined ? { 'Content-Type': 'application/json' } : undefined, + body: data !== undefined ? JSON.stringify(data) : undefined, + }); + } catch (err) { + throw new Error(`iCloud 请求失败:${method} ${url},${err.message}`); + } + + if (!response.ok) { + throw new Error(`iCloud 请求失败:${method} ${url},status ${response.status}`); + } + + try { + return await response.json(); + } catch (err) { + throw new Error(`iCloud 返回的 JSON 无法解析:${method} ${url},${err.message}`); + } +} + +async function validateIcloudSession(setupUrl) { + const data = await icloudRequest('POST', `${setupUrl}/validate`); + if (!data?.webservices?.premiummailsettings?.url) { + throw new Error('Could not validate iCloud session. Hide My Email service was unavailable.'); + } + return data; +} + +async function resolveIcloudPremiumMailService() { + const errors = []; + const state = await getState(); + const setupUrls = await getPreferredIcloudSetupUrls(state); + + for (const setupUrl of setupUrls) { + try { + const data = await validateIcloudSession(setupUrl); + const preferredIcloudHost = normalizeIcloudHost(new URL(setupUrl).host); + if (preferredIcloudHost && preferredIcloudHost !== normalizeIcloudHost(state.preferredIcloudHost)) { + await setState({ preferredIcloudHost }); + } + return { + setupUrl, + serviceUrl: String(data.webservices.premiummailsettings.url || '').replace(/\/$/, ''), + }; + } catch (err) { + errors.push(`${new URL(setupUrl).host}: ${getErrorMessage(err)}`); + } + } + + throw new Error(errors.length + ? `Could not validate iCloud session. ${errors.join(' | ')}` + : 'Could not validate iCloud session. 请先在当前浏览器登录 icloud.com.cn 或 icloud.com。'); +} + +function getIcloudAliasLabel() { + const now = new Date(); + const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; + return `MultiPage ${dateStr}`; +} + +async function checkIcloudSession() { + return withIcloudLoginHelp('检查 iCloud 会话', async () => { + const { setupUrl } = await resolveIcloudPremiumMailService(); + await addLog(`iCloud:会话校验通过(${new URL(setupUrl).host})`, 'ok'); + return { ok: true, setupUrl }; + }); +} + +async function listIcloudAliases() { + return withIcloudLoginHelp('加载 iCloud 隐私邮箱列表', async () => { + const { serviceUrl } = await resolveIcloudPremiumMailService(); + const response = await icloudRequest('GET', `${serviceUrl}/v2/hme/list`); + const state = await getState(); + return normalizeIcloudAliasList(response, { + usedEmails: getEffectiveUsedEmails(state), + preservedEmails: getPreservedAliasMap(state), + }); + }); +} + +async function deleteIcloudAlias(payload) { + return withIcloudLoginHelp('删除 iCloud 隐私邮箱', async () => { + const alias = typeof payload === 'string' + ? { email: String(payload).trim().toLowerCase(), anonymousId: '' } + : { + email: String(payload?.email || '').trim().toLowerCase(), + anonymousId: String(payload?.anonymousId || '').trim(), + }; + + if (!alias.email) { + throw new Error('未提供需要删除的 iCloud 隐私邮箱。'); + } + if (!alias.anonymousId) { + throw new Error(`缺少 ${alias.email} 的 anonymousId,请先刷新 iCloud 别名列表。`); + } + + const { serviceUrl } = await resolveIcloudPremiumMailService(); + + try { + const directDelete = await icloudRequest('POST', `${serviceUrl}/v1/hme/delete`, { + data: { anonymousId: alias.anonymousId }, + }); + if (directDelete?.success === false) { + throw new Error(directDelete?.error?.errorMessage || 'delete failed'); + } + } catch (err) { + await addLog(`iCloud:直接删除 ${alias.email} 失败,尝试先停用再删除...`, 'warn'); + + const deactivated = await icloudRequest('POST', `${serviceUrl}/v1/hme/deactivate`, { + data: { anonymousId: alias.anonymousId }, + }); + if (deactivated?.success === false) { + throw new Error(deactivated?.error?.errorMessage || `停用 ${alias.email} 失败`); + } + + const deleted = await icloudRequest('POST', `${serviceUrl}/v1/hme/delete`, { + data: { anonymousId: alias.anonymousId }, + }); + if (deleted?.success === false) { + throw new Error(deleted?.error?.errorMessage || `删除 ${alias.email} 失败`); + } + } + + const state = await getState(); + const manualAliasUsage = getManualAliasUsageMap(state); + const preservedAliases = getPreservedAliasMap(state); + delete manualAliasUsage[alias.email]; + delete preservedAliases[alias.email]; + await setState({ manualAliasUsage, preservedAliases }); + + await addLog(`iCloud:已删除 ${alias.email}`, 'ok'); + broadcastIcloudAliasesChanged({ reason: 'deleted', email: alias.email }); + return { email: alias.email }; + }); +} + +async function deleteUsedIcloudAliases() { + const aliases = await listIcloudAliases(); + const usedAliases = aliases.filter((alias) => alias.used); + if (!usedAliases.length) { + return { deleted: [], skipped: [] }; + } + + const deleted = []; + const skipped = []; + for (const alias of usedAliases) { + if (alias.preserved) { + skipped.push({ email: alias.email, error: 'preserved' }); + continue; + } + try { + await deleteIcloudAlias(alias); + deleted.push(alias.email); + } catch (err) { + skipped.push({ email: alias.email, error: getErrorMessage(err) }); + } + } + return { deleted, skipped }; +} + +async function fetchIcloudHideMyEmail() { + return withIcloudLoginHelp('获取 iCloud 隐私邮箱', async () => { + throwIfStopped(); + await addLog('iCloud:正在校验当前浏览器登录状态...', 'info'); + + const { serviceUrl, setupUrl } = await resolveIcloudPremiumMailService(); + await addLog(`iCloud:已通过 ${new URL(setupUrl).host} 验证会话`, 'ok'); + + const existingAliasesResponse = await icloudRequest('GET', `${serviceUrl}/v2/hme/list`); + const state = await getState(); + const existingAliases = normalizeIcloudAliasList(existingAliasesResponse, { + usedEmails: getEffectiveUsedEmails(state), + preservedEmails: getPreservedAliasMap(state), + }); + + const reusableAlias = pickReusableIcloudAlias(existingAliases); + if (reusableAlias) { + await setEmailState(reusableAlias.email); + await addLog(`iCloud:复用未使用别名 ${reusableAlias.email}`, 'ok'); + broadcastIcloudAliasesChanged({ reason: 'selected', email: reusableAlias.email }); + return reusableAlias.email; + } + + await addLog('iCloud:没有可复用别名,开始生成新的 Hide My Email 地址...', 'warn'); + + const generated = await icloudRequest('POST', `${serviceUrl}/v1/hme/generate`); + if (!generated?.success || !generated?.result?.hme) { + throw new Error(generated?.error?.errorMessage || 'iCloud 隐私邮箱生成失败。'); + } + + const reserved = await icloudRequest('POST', `${serviceUrl}/v1/hme/reserve`, { + data: { + hme: generated.result.hme, + label: getIcloudAliasLabel(), + note: 'Generated through Multi-Page Automation', + }, + }); + + if (!reserved?.success || !reserved?.result?.hme?.hme) { + throw new Error(reserved?.error?.errorMessage || 'iCloud 隐私邮箱保留失败。'); + } + + const alias = String(reserved.result.hme.hme || '').trim().toLowerCase(); + await setEmailState(alias); + await addLog(`iCloud:已创建并保留新别名 ${alias}`, 'ok'); + broadcastIcloudAliasesChanged({ reason: 'created', email: alias }); + return alias; + }); +} + +async function finalizeIcloudAliasAfterSuccessfulFlow(state) { + const email = String(state?.email || '').trim().toLowerCase(); + if (!email) { + return { handled: false, deleted: false }; + } + + const knownIcloudAlias = normalizeEmailGenerator(state?.emailGenerator) === 'icloud' + || Object.prototype.hasOwnProperty.call(getManualAliasUsageMap(state), email) + || Object.prototype.hasOwnProperty.call(getPreservedAliasMap(state), email); + if (!knownIcloudAlias) { + return { handled: false, deleted: false }; + } + + await setIcloudAliasUsedState({ email, used: true }, { silentLog: true }); + await addLog(`iCloud:流程成功后已标记 ${email} 为已用。`, 'ok'); + + if (!state.autoDeleteUsedIcloudAlias) { + return { handled: true, deleted: false }; + } + + if (isAliasPreserved(state, email)) { + await addLog(`iCloud:${email} 已被标记为保留,跳过自动删除。`, 'info'); + return { handled: true, deleted: false }; + } + + try { + const aliases = await listIcloudAliases(); + const alias = findIcloudAliasByEmail(aliases, email); + if (!alias) { + await addLog(`iCloud:自动删除跳过,列表中未找到 ${email}。`, 'warn'); + return { handled: true, deleted: false }; + } + if (alias.preserved) { + await addLog(`iCloud:${email} 在最新别名列表中已是保留状态,跳过自动删除。`, 'info'); + return { handled: true, deleted: false }; + } + if (!alias.anonymousId) { + await addLog(`iCloud:自动删除跳过,${email} 缺少 anonymousId,请先刷新列表后重试。`, 'warn'); + return { handled: true, deleted: false }; + } + await deleteIcloudAlias(alias); + await addLog(`iCloud:流程成功后已自动删除 ${email}。`, 'ok'); + return { handled: true, deleted: true }; + } catch (err) { + await addLog(`iCloud:自动删除 ${email} 失败:${getErrorMessage(err)}`, 'warn'); + return { handled: true, deleted: false }; + } +} + // ============================================================ // Tab Registry // ============================================================ @@ -2413,12 +3096,16 @@ 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)) + .filter((tab) => !isLocalhostOAuthCallbackUrl(tab.url)) .map((tab) => tab.id); if (!matchedIds.length) return 0; @@ -3006,6 +3693,7 @@ function getSourceLabel(source) { 'duck-mail': 'Duck 邮箱', 'hotmail-api': 'Hotmail(远程/本地)', 'luckmail-api': 'LuckMail(API 购邮)', + 'cloudflare-temp-email': 'Cloudflare Temp Email', }; return labels[source] || source || '未知来源'; } @@ -3981,6 +4669,41 @@ async function handleMessage(message, sender) { return { ok: true, email }; } + case 'CHECK_ICLOUD_SESSION': { + clearStopRequest(); + return await checkIcloudSession(); + } + + case 'LIST_ICLOUD_ALIASES': { + clearStopRequest(); + const aliases = await listIcloudAliases(); + return { ok: true, aliases }; + } + + case 'SET_ICLOUD_ALIAS_USED_STATE': { + clearStopRequest(); + const result = await setIcloudAliasUsedState(message.payload || {}); + return { ok: true, ...result }; + } + + case 'SET_ICLOUD_ALIAS_PRESERVED_STATE': { + clearStopRequest(); + const result = await setIcloudAliasPreservedState(message.payload || {}); + return { ok: true, ...result }; + } + + case 'DELETE_ICLOUD_ALIAS': { + clearStopRequest(); + const result = await deleteIcloudAlias(message.payload || {}); + return { ok: true, ...result }; + } + + case 'DELETE_USED_ICLOUD_ALIASES': { + clearStopRequest(); + const result = await deleteUsedIcloudAliases(); + return { ok: true, ...result }; + } + case 'STOP_FLOW': { await requestStop(); return { ok: true }; @@ -4071,8 +4794,12 @@ async function handleStepData(step, payload) { } const localhostPrefix = buildLocalhostCleanupPrefix(payload.localhostUrl); if (localhostPrefix) { - await closeTabsByUrlPrefix(localhostPrefix); + await closeTabsByUrlPrefix(localhostPrefix, { + excludeUrls: [payload.localhostUrl], + excludeLocalhostCallbacks: true, + }); } + await finalizeIcloudAliasAfterSuccessfulFlow(latestState); if (shouldUseCustomRegistrationEmail(latestState) && latestState.email) { await setEmailStateSilently(null); } @@ -4364,7 +5091,12 @@ function getEmailGeneratorLabel(generator) { if (generator === 'custom') { return '自定义邮箱'; } - return generator === 'cloudflare' ? 'Cloudflare 邮箱' : 'Duck 邮箱'; + if (generator === 'icloud') { + return 'iCloud 隐私邮箱'; + } + if (generator === 'cloudflare') return 'Cloudflare 邮箱'; + if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email'; + return 'Duck 邮箱'; } function generateCloudflareAliasLocalPart() { @@ -4404,6 +5136,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; @@ -4435,9 +5268,15 @@ async function fetchGeneratedEmail(state, options = {}) { if (generator === 'custom') { throw new Error('当前邮箱生成方式为自定义邮箱,请直接填写注册邮箱。'); } + if (generator === 'icloud') { + return fetchIcloudHideMyEmail(); + } if (generator === 'cloudflare') { return fetchCloudflareEmail(currentState, options); } + if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) { + return fetchCloudflareTempEmailAddress(currentState, options); + } return fetchDuckEmail(options); } @@ -4451,6 +5290,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, @@ -4546,7 +5386,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; } } @@ -5674,6 +6517,9 @@ function getMailConfig(state) { if (provider === LUCKMAIL_PROVIDER) { return { provider: LUCKMAIL_PROVIDER, label: 'LuckMail(API 购邮)' }; } + 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 邮箱' }; } @@ -5844,6 +6690,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(); @@ -5913,6 +6769,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) { @@ -5957,10 +6916,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))) { @@ -5982,7 +6943,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}`); @@ -5997,7 +6961,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; } @@ -6059,7 +7034,7 @@ async function executeStep4(state) { } throwIfStopped(); - if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER) { + if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`); } else { await addLog(`步骤 4:正在打开${mail.label}...`); @@ -6087,6 +7062,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; } @@ -6194,7 +7170,7 @@ async function runStep7Attempt(state) { } throwIfStopped(); - if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER) { + if (mail.provider === HOTMAIL_PROVIDER || mail.provider === LUCKMAIL_PROVIDER || mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { await addLog(`步骤 7:正在通过 ${mail.label} 轮询验证码...`); } else { await addLog(`步骤 7:正在打开${mail.label}...`); @@ -6221,6 +7197,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, }); } 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/icloud-utils.js b/icloud-utils.js new file mode 100644 index 0000000..82c7cc1 --- /dev/null +++ b/icloud-utils.js @@ -0,0 +1,177 @@ +(function icloudUtilsModule(root, factory) { + if (typeof module !== 'undefined' && module.exports) { + module.exports = factory(); + return; + } + + root.IcloudUtils = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createIcloudUtils() { + function normalizeIcloudHost(rawHost) { + const host = String(rawHost || '').trim().toLowerCase(); + if (!host) return ''; + if (host === 'icloud.com' || host === 'www.icloud.com' || host === 'setup.icloud.com') return 'icloud.com'; + if (host === 'icloud.com.cn' || host === 'www.icloud.com.cn' || host === 'setup.icloud.com.cn') return 'icloud.com.cn'; + return ''; + } + + function getConfiguredIcloudHostPreference(stateOrValue = '') { + const preference = typeof stateOrValue === 'object' + ? String(stateOrValue?.icloudHostPreference || '').trim().toLowerCase() + : String(stateOrValue || '').trim().toLowerCase(); + if (!preference || preference === 'auto') return ''; + return normalizeIcloudHost(preference); + } + + function getIcloudLoginUrlForHost(host) { + const normalizedHost = normalizeIcloudHost(host); + if (normalizedHost === 'icloud.com') return 'https://www.icloud.com/'; + if (normalizedHost === 'icloud.com.cn') return 'https://www.icloud.com.cn/'; + return ''; + } + + function getIcloudSetupUrlForHost(host) { + const normalizedHost = normalizeIcloudHost(host); + if (normalizedHost === 'icloud.com') return 'https://setup.icloud.com/setup/ws/1'; + if (normalizedHost === 'icloud.com.cn') return 'https://setup.icloud.com.cn/setup/ws/1'; + return ''; + } + + function getIcloudHostHintFromMessage(message) { + const lower = String(message || '').toLowerCase(); + if (lower.includes('setup.icloud.com.cn') || lower.includes('www.icloud.com.cn') || lower.includes('icloud.com.cn')) { + return 'icloud.com.cn'; + } + if (lower.includes('setup.icloud.com') || lower.includes('www.icloud.com') || lower.includes('icloud.com')) { + return 'icloud.com'; + } + return ''; + } + + function normalizeBooleanMap(rawValue = {}) { + if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) { + return {}; + } + + return Object.entries(rawValue).reduce((result, [key, value]) => { + const normalizedKey = String(key || '').trim().toLowerCase(); + if (!normalizedKey) { + return result; + } + result[normalizedKey] = Boolean(value); + return result; + }, {}); + } + + function toNormalizedEmailSet(values = []) { + if (values instanceof Set) { + return new Set(Array.from(values, (item) => String(item || '').trim().toLowerCase()).filter(Boolean)); + } + if (Array.isArray(values)) { + return new Set(values.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean)); + } + if (values && typeof values === 'object') { + const normalizedMap = normalizeBooleanMap(values); + return new Set(Object.entries(normalizedMap) + .filter(([, value]) => value) + .map(([email]) => email)); + } + return new Set(); + } + + function findIcloudAliasArray(node, depth = 0) { + if (!node || depth > 4) return null; + if (Array.isArray(node)) { + return node.some((item) => item && typeof item === 'object') ? node : null; + } + if (typeof node !== 'object') return null; + + const priorityKeys = ['hmeEmails', 'hmeEmailList', 'hmeList', 'hmes', 'aliases', 'items']; + for (const key of priorityKeys) { + if (Array.isArray(node[key])) return node[key]; + } + + for (const value of Object.values(node)) { + const nested = findIcloudAliasArray(value, depth + 1); + if (nested) return nested; + } + + return null; + } + + function normalizeIcloudAliasRecord(raw, options = {}) { + const usedEmails = toNormalizedEmailSet(options.usedEmails); + const preservedEmails = toNormalizedEmailSet(options.preservedEmails); + const anonymousId = String(raw?.anonymousId || raw?.id || '').trim(); + const email = String( + raw?.hme + || raw?.email + || raw?.alias + || raw?.address + || raw?.metaData?.hme + || '' + ).trim().toLowerCase(); + + if (!email || !email.includes('@')) return null; + + const label = String(raw?.label || raw?.metaData?.label || '').trim(); + const note = String(raw?.note || raw?.metaData?.note || '').trim(); + const state = String(raw?.state || raw?.status || '').trim().toLowerCase(); + const createdAt = raw?.createTimestamp + || raw?.createTime + || raw?.createdAt + || raw?.createdDate + || null; + + return { + anonymousId, + email, + label, + note, + state, + active: raw?.active !== false && raw?.isActive !== false && state !== 'inactive' && state !== 'deleted', + used: usedEmails.has(email), + preserved: preservedEmails.has(email), + createdAt, + }; + } + + function normalizeIcloudAliasList(response, options = {}) { + const aliases = findIcloudAliasArray(response); + if (!aliases) return []; + + return aliases + .map((alias) => normalizeIcloudAliasRecord(alias, options)) + .filter(Boolean) + .sort((left, right) => { + if (left.active !== right.active) return left.active ? -1 : 1; + if (left.used !== right.used) return left.used ? 1 : -1; + return String(left.email).localeCompare(String(right.email)); + }); + } + + function pickReusableIcloudAlias(aliases = []) { + return (Array.isArray(aliases) ? aliases : []).find((alias) => alias?.active && !alias?.used) || null; + } + + function findIcloudAliasByEmail(aliases = [], email = '') { + const normalizedEmail = String(email || '').trim().toLowerCase(); + if (!normalizedEmail) return null; + return (Array.isArray(aliases) ? aliases : []) + .find((alias) => String(alias?.email || '').trim().toLowerCase() === normalizedEmail) || null; + } + + return { + findIcloudAliasArray, + findIcloudAliasByEmail, + getConfiguredIcloudHostPreference, + getIcloudHostHintFromMessage, + getIcloudLoginUrlForHost, + getIcloudSetupUrlForHost, + normalizeBooleanMap, + normalizeIcloudAliasList, + normalizeIcloudAliasRecord, + normalizeIcloudHost, + pickReusableIcloudAlias, + toNormalizedEmailSet, + }; +}); diff --git a/manifest.json b/manifest.json index 079d4ad..53a06b4 100644 --- a/manifest.json +++ b/manifest.json @@ -8,17 +8,29 @@ "alarms", "tabs", "webNavigation", + "declarativeNetRequest", "debugger", "storage", "scripting", "activeTab" ], "host_permissions": [ + "https://*.icloud.com/*", + "https://*.icloud.com.cn/*", "" ], "background": { "service_worker": "background.js" }, + "declarative_net_request": { + "rule_resources": [ + { + "id": "icloud_headers", + "enabled": true, + "path": "rules.json" + } + ] + }, "side_panel": { "default_path": "sidepanel/sidepanel.html" }, diff --git a/rules.json b/rules.json new file mode 100644 index 0000000..3bb7919 --- /dev/null +++ b/rules.json @@ -0,0 +1,50 @@ +[ + { + "id": 1, + "priority": 1, + "action": { + "type": "modifyHeaders", + "requestHeaders": [ + { + "header": "Origin", + "operation": "set", + "value": "https://www.icloud.com" + }, + { + "header": "Referer", + "operation": "set", + "value": "https://www.icloud.com/" + } + ] + }, + "condition": { + "urlFilter": "|https://*.icloud.com/*", + "resourceTypes": ["xmlhttprequest"], + "excludedInitiatorDomains": ["apple.com", "icloud.com"] + } + }, + { + "id": 2, + "priority": 1, + "action": { + "type": "modifyHeaders", + "requestHeaders": [ + { + "header": "Origin", + "operation": "set", + "value": "https://www.icloud.com.cn" + }, + { + "header": "Referer", + "operation": "set", + "value": "https://www.icloud.com.cn/" + } + ] + }, + "condition": { + "urlFilter": "|https://*.icloud.com.cn/*", + "resourceTypes": ["xmlhttprequest"], + "excludedInitiatorDomains": ["apple.com.cn", "icloud.com.cn"] + } + } +] diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index d11eb72..b4503c3 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -1035,6 +1035,163 @@ header { text-align: center; } +.icloud-card { + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + background: color-mix(in srgb, var(--bg-base) 84%, transparent); + padding: 10px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.icloud-summary { + color: var(--text-secondary); + font-size: 12px; + line-height: 1.6; +} + +.icloud-login-help { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + border: 1px solid color-mix(in srgb, var(--orange) 26%, var(--border)); + background: color-mix(in srgb, var(--orange-soft) 62%, var(--bg-base)); + border-radius: var(--radius-sm); + padding: 10px; +} + +.icloud-login-help-main { + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; +} + +.icloud-login-help-title { + color: var(--text-primary); + font-weight: 700; + font-size: 13px; +} + +.icloud-login-help-text { + color: var(--text-secondary); + font-size: 12px; + line-height: 1.5; +} + +.icloud-toolbar { + display: flex; + gap: 8px; + align-items: center; +} + +.icloud-search { + flex: 1; +} + +.icloud-filter { + flex: 0 0 130px; +} + +.icloud-bulkbar { + display: flex; + gap: 8px; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; +} + +.icloud-select-all { + flex: 0 0 auto; +} + +.icloud-bulk-actions { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.icloud-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 360px; + overflow: auto; + padding-right: 4px; +} + +.icloud-item { + border: 1px solid var(--border); + background: var(--bg-base); + border-radius: var(--radius-sm); + padding: 10px; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 10px; + align-items: flex-start; +} + +.icloud-item-check { + margin-top: 4px; +} + +.icloud-item-main { + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.icloud-item-actions { + display: flex; + gap: 6px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.icloud-item-email { + font-weight: 600; + color: var(--text-primary); + word-break: break-all; +} + +.icloud-item-meta { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.icloud-tag { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 999px; + font-size: 11px; + background: var(--bg-surface); + color: var(--text-secondary); +} + +.icloud-tag.used { + background: var(--orange-soft); + color: var(--orange); +} + +.icloud-tag.active { + background: var(--green-soft); + color: var(--green); +} + +.icloud-empty { + color: var(--text-muted); + font-size: 12px; + border: 1px dashed var(--border); + border-radius: var(--radius-sm); + padding: 12px; + text-align: center; +} + .data-select { flex: 1; padding: 7px 10px; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index b57ec4d..202a96c 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -160,6 +160,7 @@ + @@ -169,8 +170,31 @@ + + + + +
就绪 diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 20402e3..ee6164d 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -68,8 +68,38 @@ const selectMailProvider = document.getElementById('select-mail-provider'); const btnMailLogin = document.getElementById('btn-mail-login'); 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 luckmailSection = document.getElementById('luckmail-section'); +const icloudSection = document.getElementById('icloud-section'); +const icloudSummary = document.getElementById('icloud-summary'); +const icloudList = document.getElementById('icloud-list'); +const icloudLoginHelp = document.getElementById('icloud-login-help'); +const icloudLoginHelpTitle = document.getElementById('icloud-login-help-title'); +const icloudLoginHelpText = document.getElementById('icloud-login-help-text'); +const btnIcloudLoginDone = document.getElementById('btn-icloud-login-done'); +const btnIcloudRefresh = document.getElementById('btn-icloud-refresh'); +const btnIcloudDeleteUsed = document.getElementById('btn-icloud-delete-used'); +const selectIcloudHostPreference = document.getElementById('select-icloud-host-preference'); +const checkboxAutoDeleteIcloud = document.getElementById('checkbox-auto-delete-icloud'); +const inputIcloudSearch = document.getElementById('input-icloud-search'); +const selectIcloudFilter = document.getElementById('select-icloud-filter'); +const checkboxIcloudSelectAll = document.getElementById('checkbox-icloud-select-all'); +const icloudSelectionSummary = document.getElementById('icloud-selection-summary'); +const btnIcloudBulkUsed = document.getElementById('btn-icloud-bulk-used'); +const btnIcloudBulkUnused = document.getElementById('btn-icloud-bulk-unused'); +const btnIcloudBulkPreserve = document.getElementById('btn-icloud-bulk-preserve'); +const btnIcloudBulkUnpreserve = document.getElementById('btn-icloud-bulk-unpreserve'); +const btnIcloudBulkDelete = document.getElementById('btn-icloud-bulk-delete'); const rowHotmailServiceMode = document.getElementById('row-hotmail-service-mode'); const hotmailServiceModeButtons = Array.from(document.querySelectorAll('[data-hotmail-service-mode]')); const rowHotmailRemoteBaseUrl = document.getElementById('row-hotmail-remote-base-url'); @@ -185,6 +215,12 @@ let settingsDirty = false; let settingsSaveInFlight = false; let settingsAutoSaveTimer = null; let cloudflareDomainEditMode = false; +let cloudflareTempEmailDomainEditMode = false; +let icloudRefreshQueued = false; +let lastRenderedIcloudAliases = []; +let icloudSelectedEmails = new Set(); +let icloudSearchTerm = ''; +let icloudFilterMode = 'all'; let modalChoiceResolver = null; let currentModalActions = []; let modalResultBuilder = null; @@ -224,14 +260,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 邮箱', @@ -893,6 +937,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 || ''); @@ -902,6 +977,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(); @@ -928,6 +1012,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); @@ -944,11 +1054,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(), @@ -961,6 +1091,8 @@ function collectSettingsPayload() { customPassword: inputPassword.value, mailProvider: selectMailProvider.value, emailGenerator: selectEmailGenerator.value, + autoDeleteUsedIcloudAlias: checkboxAutoDeleteIcloud?.checked, + icloudHostPreference: selectIcloudHostPreference?.value || 'auto', emailPrefix: inputEmailPrefix.value.trim(), inbucketHost: inputInbucketHost.value.trim(), inbucketMailbox: inputInbucketMailbox.value.trim(), @@ -973,6 +1105,11 @@ function collectSettingsPayload() { luckmailDomain: inputLuckmailDomain.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, @@ -1244,14 +1381,33 @@ function applySettingsState(state) { inputSub2ApiPassword.value = state?.sub2apiPassword || ''; inputSub2ApiGroup.value = state?.sub2apiGroupName || ''; const restoredMailProvider = isCustomMailProvider(state?.mailProvider) - || ['hotmail-api', 'luckmail-api', '163', '163-vip', 'qq', 'inbucket', '2925'].includes(String(state?.mailProvider || '').trim()) + || ['hotmail-api', 'luckmail-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' ? 'custom' : '163'); selectMailProvider.value = restoredMailProvider; - selectEmailGenerator.value = String(state?.emailGenerator || '').trim().toLowerCase() === 'cloudflare' ? 'cloudflare' : 'duck'; + { + const restoredGenerator = String(state?.emailGenerator || '').trim().toLowerCase(); + if (restoredGenerator === 'icloud') { + selectEmailGenerator.value = 'icloud'; + } else if (restoredGenerator === 'cloudflare') { + selectEmailGenerator.value = 'cloudflare'; + } else if (restoredGenerator === 'cloudflare-temp-email') { + selectEmailGenerator.value = 'cloudflare-temp-email'; + } else { + selectEmailGenerator.value = 'duck'; + } + } + if (selectIcloudHostPreference) { + selectIcloudHostPreference.value = String(state?.icloudHostPreference || '').trim().toLowerCase() === 'icloud.com' + ? 'icloud.com' + : (String(state?.icloudHostPreference || '').trim().toLowerCase() === 'icloud.com.cn' ? 'icloud.com.cn' : 'auto'); + } + if (checkboxAutoDeleteIcloud) { + checkboxAutoDeleteIcloud.checked = Boolean(state?.autoDeleteUsedIcloudAlias); + } inputEmailPrefix.value = state?.emailPrefix || ''; inputInbucketHost.value = state?.inbucketHost || ''; inputInbucketMailbox.value = state?.inbucketMailbox || ''; @@ -1262,8 +1418,13 @@ function applySettingsState(state) { inputLuckmailBaseUrl.value = normalizeLuckmailBaseUrl(state?.luckmailBaseUrl); selectLuckmailEmailType.value = normalizeLuckmailEmailType(state?.luckmailEmailType); inputLuckmailDomain.value = state?.luckmailDomain || ''; + 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); @@ -1289,6 +1450,9 @@ async function restoreState() { try { const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }); applySettingsState(state); + if (getSelectedEmailGenerator() === 'icloud' && icloudSection?.style.display !== 'none') { + refreshIcloudAliases({ silent: true }).catch(() => { }); + } if (state.oauthUrl) { displayOauthUrl.textContent = state.oauthUrl; @@ -1564,13 +1728,29 @@ function normalizeLuckmailEmailType(value = '') { function getSelectedEmailGenerator() { const generator = String(selectEmailGenerator.value || '').trim().toLowerCase(); - if (generator === 'cloudflare') { - return 'cloudflare'; + if (generator === 'custom' || generator === 'manual') { + return 'custom'; } + if (generator === 'icloud') { + return 'icloud'; + } + 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() === 'icloud') { + return { + buttonLabel: '获取', + placeholder: '点击获取 iCloud 隐私邮箱,或手动粘贴邮箱', + successVerb: '获取', + label: 'iCloud 隐私邮箱', + }; + } if (getSelectedEmailGenerator() === 'cloudflare') { return { buttonLabel: '生成', @@ -1579,6 +1759,14 @@ function getEmailGeneratorUiCopy() { label: 'Cloudflare 邮箱', }; } + if (getSelectedEmailGenerator() === 'cloudflare-temp-email') { + return { + buttonLabel: '生成 Temp', + placeholder: '点击生成 Cloudflare Temp Email,或手动粘贴邮箱', + successVerb: '生成', + label: 'Cloudflare Temp Email', + }; + } return { buttonLabel: '获取', @@ -2089,6 +2277,7 @@ function updateMailLoginButtonState() { const config = getMailProviderLoginConfig(); const loginUrl = getMailProviderLoginUrl(); btnMailLogin.disabled = !loginUrl; + btnMailLogin.textContent = config?.buttonLabel || '登录'; btnMailLogin.title = loginUrl ? `打开 ${config.label} 登录页` : '当前邮箱服务没有可跳转的登录页'; } @@ -2307,16 +2496,32 @@ function updateMailProviderUI() { const useLuckmail = isLuckmailProvider(); const useCustomEmail = isCustomMailProvider(); const useEmailGenerator = !useHotmail && !useLuckmail && !useGeneratedAlias && !useCustomEmail; + const useCloudflareTempEmailProvider = selectMailProvider.value === 'cloudflare-temp-email'; updateMailLoginButtonState(); rowEmailPrefix.style.display = useGeneratedAlias ? '' : 'none'; const hotmailServiceMode = getSelectedHotmailServiceMode(); rowInbucketHost.style.display = useInbucket ? '' : 'none'; rowInbucketMailbox.style.display = useInbucket ? '' : 'none'; - const useCloudflare = selectEmailGenerator.value === 'cloudflare'; + const selectedGenerator = getSelectedEmailGenerator(); + const useCloudflare = selectedGenerator === 'cloudflare'; + const useIcloud = selectedGenerator === 'icloud'; + const useCloudflareTempEmailGenerator = selectedGenerator === 'cloudflare-temp-email'; const showCloudflareDomain = useEmailGenerator && useCloudflare; + const showCloudflareTempEmailSettings = useCloudflareTempEmailProvider || (useEmailGenerator && useCloudflareTempEmailGenerator); + const showCloudflareTempEmailDomain = useEmailGenerator && useCloudflareTempEmailGenerator; if (rowEmailGenerator) { rowEmailGenerator.style.display = useEmailGenerator ? '' : 'none'; } + if (icloudSection) { + const showIcloudSection = useEmailGenerator && useIcloud; + icloudSection.style.display = showIcloudSection ? '' : 'none'; + if (showIcloudSection && !lastRenderedIcloudAliases.length) { + queueIcloudAliasRefresh(); + } + if (!showIcloudSection) { + hideIcloudLoginHelp(); + } + } rowCfDomain.style.display = showCloudflareDomain ? '' : 'none'; const { domains } = getCloudflareDomainsFromState(); if (showCloudflareDomain) { @@ -2324,6 +2529,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'; @@ -2362,7 +2577,7 @@ function updateMailProviderUI() { ? '步骤 3 会自动购买 LuckMail 邮箱并用于收码' : (useGeneratedAlias ? '步骤 3 会自动生成邮箱,无需手动获取' - : (useCustomEmail ? '请先填写自定义注册邮箱,成功一轮后会自动清空' : '先自动获取邮箱,或手动粘贴邮箱后再继续'))); + : (useCustomEmail ? '请先填写自定义注册邮箱,成功一轮后会自动清空' : `先自动获取${uiCopy.label},或手动粘贴邮箱后再继续`))); } if (useHotmail) { inputEmail.value = getCurrentHotmailEmail(); @@ -2407,6 +2622,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' : ''; @@ -2499,6 +2746,11 @@ function updateButtonStates() { }); btnReset.disabled = anyRunning || autoScheduled || isAutoRunPausedPhase() || autoLocked; + const disableIcloudControls = anyRunning || autoScheduled || autoLocked; + if (btnIcloudRefresh) btnIcloudRefresh.disabled = disableIcloudControls; + if (btnIcloudDeleteUsed) btnIcloudDeleteUsed.disabled = disableIcloudControls || !(lastRenderedIcloudAliases.some((alias) => alias.used && !alias.preserved)); + if (selectIcloudHostPreference) selectIcloudHostPreference.disabled = disableIcloudControls; + if (checkboxAutoDeleteIcloud) checkboxAutoDeleteIcloud.disabled = disableIcloudControls; updateStopButtonState(anyRunning || autoScheduled || isAutoRunPausedPhase() || autoLocked); } @@ -2621,6 +2873,375 @@ function escapeHtml(text) { return div.innerHTML; } +function normalizeIcloudSearchText(value) { + return String(value || '').trim().toLowerCase(); +} + +function getFilteredIcloudAliases(aliases = lastRenderedIcloudAliases) { + const searchTerm = normalizeIcloudSearchText(icloudSearchTerm); + return (Array.isArray(aliases) ? aliases : []).filter((alias) => { + const matchesFilter = (() => { + switch (icloudFilterMode) { + case 'active': return Boolean(alias.active); + case 'used': return Boolean(alias.used); + case 'unused': return !alias.used; + case 'preserved': return Boolean(alias.preserved); + default: return true; + } + })(); + + if (!matchesFilter) return false; + if (!searchTerm) return true; + + const haystack = [ + alias.email, + alias.label, + alias.note, + alias.used ? '已用 used' : '未用 unused', + alias.active ? '可用 active' : '不可用 inactive', + alias.preserved ? '保留 preserved' : '', + ].join(' ').toLowerCase(); + + return haystack.includes(searchTerm); + }); +} + +function pruneIcloudSelection(aliases = lastRenderedIcloudAliases) { + const existing = new Set((Array.isArray(aliases) ? aliases : []).map((alias) => alias.email)); + icloudSelectedEmails = new Set([...icloudSelectedEmails].filter((email) => existing.has(email))); +} + +function updateIcloudBulkUI(visibleAliases = getFilteredIcloudAliases()) { + if (!checkboxIcloudSelectAll || !icloudSelectionSummary) { + return; + } + + const visibleEmails = visibleAliases.map((alias) => alias.email); + const selectedVisibleCount = visibleEmails.filter((email) => icloudSelectedEmails.has(email)).length; + const hasVisible = visibleEmails.length > 0; + + checkboxIcloudSelectAll.checked = hasVisible && selectedVisibleCount === visibleEmails.length; + checkboxIcloudSelectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleEmails.length; + checkboxIcloudSelectAll.disabled = !hasVisible; + icloudSelectionSummary.textContent = `已选 ${icloudSelectedEmails.size} 个(当前显示 ${visibleEmails.length} 个)`; + + const hasSelection = icloudSelectedEmails.size > 0; + if (btnIcloudBulkUsed) btnIcloudBulkUsed.disabled = !hasSelection; + if (btnIcloudBulkUnused) btnIcloudBulkUnused.disabled = !hasSelection; + if (btnIcloudBulkPreserve) btnIcloudBulkPreserve.disabled = !hasSelection; + if (btnIcloudBulkUnpreserve) btnIcloudBulkUnpreserve.disabled = !hasSelection; + if (btnIcloudBulkDelete) btnIcloudBulkDelete.disabled = !hasSelection; +} + +function setIcloudLoadingState(loading, summary = '') { + if (btnIcloudRefresh) btnIcloudRefresh.disabled = loading; + if (btnIcloudDeleteUsed) btnIcloudDeleteUsed.disabled = loading; + if (btnIcloudLoginDone) btnIcloudLoginDone.disabled = loading; + if (inputIcloudSearch) inputIcloudSearch.disabled = loading; + if (selectIcloudFilter) selectIcloudFilter.disabled = loading; + if (checkboxIcloudSelectAll) checkboxIcloudSelectAll.disabled = loading || getFilteredIcloudAliases().length === 0; + if (btnIcloudBulkUsed) btnIcloudBulkUsed.disabled = loading || icloudSelectedEmails.size === 0; + if (btnIcloudBulkUnused) btnIcloudBulkUnused.disabled = loading || icloudSelectedEmails.size === 0; + if (btnIcloudBulkPreserve) btnIcloudBulkPreserve.disabled = loading || icloudSelectedEmails.size === 0; + if (btnIcloudBulkUnpreserve) btnIcloudBulkUnpreserve.disabled = loading || icloudSelectedEmails.size === 0; + if (btnIcloudBulkDelete) btnIcloudBulkDelete.disabled = loading || icloudSelectedEmails.size === 0; + if (summary && icloudSummary) icloudSummary.textContent = summary; +} + +function showIcloudLoginHelp(payload = {}) { + if (!icloudLoginHelp) return; + const loginUrl = String(payload.loginUrl || '').trim(); + const host = loginUrl ? new URL(loginUrl).host : 'icloud.com.cn / icloud.com'; + if (icloudLoginHelpTitle) icloudLoginHelpTitle.textContent = '需要登录 iCloud'; + if (icloudLoginHelpText) icloudLoginHelpText.textContent = `我已经为你打开 ${host}。请在那个页面完成登录,然后回到这里点击“我已登录”。`; + icloudLoginHelp.style.display = 'flex'; +} + +function hideIcloudLoginHelp() { + if (icloudLoginHelp) { + icloudLoginHelp.style.display = 'none'; + } +} + +function renderIcloudAliases(aliases = []) { + if (!icloudList || !icloudSummary) return; + + lastRenderedIcloudAliases = Array.isArray(aliases) ? aliases : []; + pruneIcloudSelection(lastRenderedIcloudAliases); + icloudList.innerHTML = ''; + + if (!aliases.length) { + icloudSelectedEmails.clear(); + icloudList.innerHTML = '
未找到 iCloud Hide My Email 别名。
'; + icloudSummary.textContent = '加载你的 iCloud Hide My Email 别名以便在这里管理。'; + if (btnIcloudDeleteUsed) btnIcloudDeleteUsed.disabled = true; + updateIcloudBulkUI([]); + return; + } + + const usedCount = aliases.filter((alias) => alias.used).length; + const deletableUsedCount = aliases.filter((alias) => alias.used && !alias.preserved).length; + icloudSummary.textContent = `已加载 ${aliases.length} 个别名,其中 ${usedCount} 个已标记为已用。`; + if (btnIcloudDeleteUsed) btnIcloudDeleteUsed.disabled = deletableUsedCount === 0; + + const visibleAliases = getFilteredIcloudAliases(aliases); + if (!visibleAliases.length) { + icloudList.innerHTML = '
没有匹配当前筛选条件的别名。
'; + updateIcloudBulkUI([]); + return; + } + + for (const alias of visibleAliases) { + const item = document.createElement('div'); + item.className = 'icloud-item'; + item.innerHTML = ` + +
+ +
+ ${alias.used ? '已用' : ''} + ${!alias.used && alias.active ? '可用' : ''} + ${alias.preserved ? '保留' : ''} + ${alias.label ? `${escapeHtml(alias.label)}` : ''} + ${alias.note ? `${escapeHtml(alias.note)}` : ''} +
+
+
+ + + +
+ `; + + item.querySelector('[data-action="select"]').addEventListener('change', (event) => { + if (event.target.checked) { + icloudSelectedEmails.add(alias.email); + } else { + icloudSelectedEmails.delete(alias.email); + } + updateIcloudBulkUI(visibleAliases); + }); + item.querySelector('[data-action="toggle-used"]').addEventListener('click', async () => { + await setSingleIcloudAliasUsedState(alias, !alias.used); + }); + item.querySelector('[data-action="toggle-preserved"]').addEventListener('click', async () => { + await setSingleIcloudAliasPreservedState(alias, !alias.preserved); + }); + item.querySelector('[data-action="delete"]').addEventListener('click', async () => { + await deleteSingleIcloudAlias(alias); + }); + icloudList.appendChild(item); + } + + updateIcloudBulkUI(visibleAliases); +} + +async function refreshIcloudAliases(options = {}) { + const { silent = false } = options; + if (!icloudSection || icloudSection.style.display === 'none') { + return; + } + + if (!silent) setIcloudLoadingState(true, '正在加载 iCloud 别名...'); + try { + const response = await chrome.runtime.sendMessage({ + type: 'LIST_ICLOUD_ALIASES', + source: 'sidepanel', + payload: {}, + }); + if (response?.error) throw new Error(response.error); + hideIcloudLoginHelp(); + renderIcloudAliases(response?.aliases || []); + } catch (err) { + icloudSelectedEmails.clear(); + if (icloudList) { + icloudList.innerHTML = '
无法加载 iCloud 别名。
'; + } + if (icloudSummary) { + icloudSummary.textContent = err.message; + } + updateIcloudBulkUI([]); + if (!silent) showToast(`iCloud 别名加载失败:${err.message}`, 'error'); + } finally { + setIcloudLoadingState(false); + } +} + +function queueIcloudAliasRefresh() { + if (icloudRefreshQueued) return; + icloudRefreshQueued = true; + setTimeout(async () => { + icloudRefreshQueued = false; + await refreshIcloudAliases({ silent: true }); + }, 150); +} + +async function deleteSingleIcloudAlias(alias) { + const confirmed = await openConfirmModal({ + title: '删除 iCloud 别名', + message: `确认删除 ${alias.email} 吗?此操作不可撤销。`, + confirmLabel: '确认删除', + confirmVariant: 'btn-danger', + }); + if (!confirmed) { + return; + } + + setIcloudLoadingState(true, `正在删除 ${alias.email} ...`); + try { + const response = await chrome.runtime.sendMessage({ + type: 'DELETE_ICLOUD_ALIAS', + source: 'sidepanel', + payload: { email: alias.email, anonymousId: alias.anonymousId }, + }); + if (response?.error) throw new Error(response.error); + showToast(`已删除 ${alias.email}`, 'success', 2200); + await refreshIcloudAliases({ silent: true }); + } catch (err) { + if (icloudSummary) icloudSummary.textContent = err.message; + showToast(`删除 iCloud 别名失败:${err.message}`, 'error'); + } finally { + setIcloudLoadingState(false); + } +} + +async function setSingleIcloudAliasUsedState(alias, used) { + setIcloudLoadingState(true, `正在更新 ${alias.email} 的使用状态...`); + try { + const response = await chrome.runtime.sendMessage({ + type: 'SET_ICLOUD_ALIAS_USED_STATE', + source: 'sidepanel', + payload: { email: alias.email, used }, + }); + if (response?.error) throw new Error(response.error); + showToast(`${alias.email} 已${used ? '标记为已用' : '恢复为未用'}`, 'success', 2200); + await refreshIcloudAliases({ silent: true }); + } catch (err) { + if (icloudSummary) icloudSummary.textContent = err.message; + showToast(`更新 iCloud 使用状态失败:${err.message}`, 'error'); + } finally { + setIcloudLoadingState(false); + } +} + +async function setSingleIcloudAliasPreservedState(alias, preserved) { + setIcloudLoadingState(true, `正在更新 ${alias.email} 的保留状态...`); + try { + const response = await chrome.runtime.sendMessage({ + type: 'SET_ICLOUD_ALIAS_PRESERVED_STATE', + source: 'sidepanel', + payload: { email: alias.email, preserved }, + }); + if (response?.error) throw new Error(response.error); + showToast(`${alias.email} 已${preserved ? '设为保留' : '取消保留'}`, 'success', 2200); + await refreshIcloudAliases({ silent: true }); + } catch (err) { + if (icloudSummary) icloudSummary.textContent = err.message; + showToast(`更新 iCloud 保留状态失败:${err.message}`, 'error'); + } finally { + setIcloudLoadingState(false); + } +} + +async function runBulkIcloudAction(action) { + const selectedAliases = lastRenderedIcloudAliases.filter((alias) => icloudSelectedEmails.has(alias.email)); + if (!selectedAliases.length) { + updateIcloudBulkUI(); + return; + } + + if (action === 'delete') { + const confirmed = await openConfirmModal({ + title: '批量删除 iCloud 别名', + message: `确认删除选中的 ${selectedAliases.length} 个 iCloud 别名吗?此操作不可撤销。`, + confirmLabel: '确认删除', + confirmVariant: 'btn-danger', + }); + if (!confirmed) { + return; + } + } + + const actionLabelMap = { + used: '标记已用', + unused: '标记未用', + preserve: '保留', + unpreserve: '取消保留', + delete: '删除', + }; + setIcloudLoadingState(true, `正在批量${actionLabelMap[action] || '处理'} iCloud 别名...`); + + try { + for (const alias of selectedAliases) { + let response = null; + if (action === 'used' || action === 'unused') { + response = await chrome.runtime.sendMessage({ + type: 'SET_ICLOUD_ALIAS_USED_STATE', + source: 'sidepanel', + payload: { email: alias.email, used: action === 'used' }, + }); + } else if (action === 'preserve' || action === 'unpreserve') { + response = await chrome.runtime.sendMessage({ + type: 'SET_ICLOUD_ALIAS_PRESERVED_STATE', + source: 'sidepanel', + payload: { email: alias.email, preserved: action === 'preserve' }, + }); + } else if (action === 'delete') { + response = await chrome.runtime.sendMessage({ + type: 'DELETE_ICLOUD_ALIAS', + source: 'sidepanel', + payload: { email: alias.email, anonymousId: alias.anonymousId }, + }); + icloudSelectedEmails.delete(alias.email); + } + + if (response?.error) { + throw new Error(response.error); + } + } + + showToast(`已批量${actionLabelMap[action] || '处理'} ${selectedAliases.length} 个 iCloud 别名`, 'success', 2400); + await refreshIcloudAliases({ silent: true }); + } catch (err) { + if (icloudSummary) icloudSummary.textContent = err.message; + showToast(`批量处理 iCloud 别名失败:${err.message}`, 'error'); + } finally { + setIcloudLoadingState(false); + updateIcloudBulkUI(); + } +} + +async function deleteUsedIcloudAliases() { + const confirmed = await openConfirmModal({ + title: '删除已用 iCloud 别名', + message: '确认删除所有未保留的已用 iCloud 别名吗?此操作不可撤销。', + confirmLabel: '确认删除', + confirmVariant: 'btn-danger', + }); + if (!confirmed) { + return; + } + + setIcloudLoadingState(true, '正在删除已用 iCloud 别名...'); + try { + const response = await chrome.runtime.sendMessage({ + type: 'DELETE_USED_ICLOUD_ALIASES', + source: 'sidepanel', + payload: {}, + }); + if (response?.error) throw new Error(response.error); + const deleted = response?.deleted || []; + const skipped = response?.skipped || []; + showToast(`已删除 ${deleted.length} 个已用别名,跳过 ${skipped.length} 个`, skipped.length ? 'warn' : 'success', 2800); + await refreshIcloudAliases({ silent: true }); + } catch (err) { + if (icloudSummary) icloudSummary.textContent = err.message; + showToast(`删除已用 iCloud 别名失败:${err.message}`, 'error'); + } finally { + setIcloudLoadingState(false); + } +} + async function fetchGeneratedEmail(options = {}) { const { showFailureToast = true } = options; const uiCopy = getEmailGeneratorUiCopy(); @@ -2649,6 +3270,9 @@ async function fetchGeneratedEmail(options = {}) { } inputEmail.value = response.email; + if (getSelectedEmailGenerator() === 'icloud') { + queueIcloudAliasRefresh(); + } showToast(`已${uiCopy.successVerb} ${uiCopy.label}:${response.email}`, 'success', 2500); return response.email; } catch (err) { @@ -2961,6 +3585,75 @@ btnFetchEmail.addEventListener('click', async () => { await fetchGeneratedEmail().catch(() => { }); }); +btnIcloudRefresh?.addEventListener('click', async () => { + await refreshIcloudAliases(); +}); + +btnIcloudDeleteUsed?.addEventListener('click', async () => { + await deleteUsedIcloudAliases(); +}); + +inputIcloudSearch?.addEventListener('input', () => { + icloudSearchTerm = inputIcloudSearch.value || ''; + renderIcloudAliases(lastRenderedIcloudAliases); +}); + +selectIcloudFilter?.addEventListener('change', () => { + icloudFilterMode = selectIcloudFilter.value || 'all'; + renderIcloudAliases(lastRenderedIcloudAliases); +}); + +checkboxIcloudSelectAll?.addEventListener('change', () => { + const visibleAliases = getFilteredIcloudAliases(); + if (checkboxIcloudSelectAll.checked) { + visibleAliases.forEach((alias) => icloudSelectedEmails.add(alias.email)); + } else { + visibleAliases.forEach((alias) => icloudSelectedEmails.delete(alias.email)); + } + renderIcloudAliases(lastRenderedIcloudAliases); +}); + +btnIcloudBulkUsed?.addEventListener('click', async () => { + await runBulkIcloudAction('used'); +}); + +btnIcloudBulkUnused?.addEventListener('click', async () => { + await runBulkIcloudAction('unused'); +}); + +btnIcloudBulkPreserve?.addEventListener('click', async () => { + await runBulkIcloudAction('preserve'); +}); + +btnIcloudBulkUnpreserve?.addEventListener('click', async () => { + await runBulkIcloudAction('unpreserve'); +}); + +btnIcloudBulkDelete?.addEventListener('click', async () => { + await runBulkIcloudAction('delete'); +}); + +btnIcloudLoginDone?.addEventListener('click', async () => { + btnIcloudLoginDone.disabled = true; + try { + const response = await chrome.runtime.sendMessage({ + type: 'CHECK_ICLOUD_SESSION', + source: 'sidepanel', + payload: {}, + }); + if (response?.error) { + throw new Error(response.error); + } + hideIcloudLoginHelp(); + showToast('iCloud 会话已恢复,别名列表已刷新。', 'success', 2600); + await refreshIcloudAliases({ silent: true }); + } catch (err) { + showToast(`看起来还没有登录完成:${err.message}`, 'warn', 4200); + } finally { + btnIcloudLoginDone.disabled = false; + } +}); + btnToggleHotmailList?.addEventListener('click', () => { setHotmailListExpanded(!hotmailListExpanded); }); @@ -3445,6 +4138,15 @@ btnReset.addEventListener('click', async () => { displayStatus.textContent = '就绪'; statusBar.className = 'status-bar'; logArea.innerHTML = ''; + icloudSelectedEmails.clear(); + lastRenderedIcloudAliases = []; + if (icloudList) { + icloudList.innerHTML = ''; + } + if (icloudSummary) { + icloudSummary.textContent = '加载你的 iCloud Hide My Email 别名以便在这里管理。'; + } + updateIcloudBulkUI([]); document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row'); document.querySelectorAll('.step-status').forEach(el => el.textContent = ''); setDefaultAutoRunButton(); @@ -3618,6 +4320,19 @@ selectEmailGenerator.addEventListener('change', () => { saveSettings({ silent: true }).catch(() => { }); }); +selectIcloudHostPreference?.addEventListener('change', () => { + markSettingsDirty(true); + saveSettings({ silent: true }).catch(() => { }); + if (getSelectedEmailGenerator() === 'icloud') { + queueIcloudAliasRefresh(); + } +}); + +checkboxAutoDeleteIcloud?.addEventListener('change', () => { + markSettingsDirty(true); + saveSettings({ silent: true }).catch(() => { }); +}); + selectPanelMode.addEventListener('change', () => { updatePanelModeUI(); markSettingsDirty(true); @@ -3632,6 +4347,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) { @@ -3653,6 +4376,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(); @@ -3660,6 +4404,13 @@ inputCfDomain.addEventListener('keydown', (event) => { } }); +inputTempEmailDomain.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + btnTempEmailDomainMode.click(); + } +}); + inputSub2ApiUrl.addEventListener('input', () => { markSettingsDirty(true); scheduleSettingsAutoSave(); @@ -3741,6 +4492,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(); @@ -3857,6 +4633,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { displayStatus.textContent = '就绪'; statusBar.className = 'status-bar'; logArea.innerHTML = ''; + icloudSelectedEmails.clear(); + lastRenderedIcloudAliases = []; + if (icloudList) icloudList.innerHTML = ''; + if (icloudSummary) icloudSummary.textContent = '加载你的 iCloud Hide My Email 别名以便在这里管理。'; + updateIcloudBulkUI([]); document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row'); document.querySelectorAll('.step-status').forEach(el => el.textContent = ''); syncAutoRunState({ @@ -3896,6 +4677,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') { @@ -3921,6 +4714,15 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { inputEmail.value = getCurrentLuckmailEmail(); queueLuckmailPurchaseRefresh(); } + if (message.payload.autoDeleteUsedIcloudAlias !== undefined && checkboxAutoDeleteIcloud) { + checkboxAutoDeleteIcloud.checked = Boolean(message.payload.autoDeleteUsedIcloudAlias); + } + if (message.payload.icloudHostPreference !== undefined && selectIcloudHostPreference) { + const hostPreference = String(message.payload.icloudHostPreference || '').trim().toLowerCase(); + selectIcloudHostPreference.value = hostPreference === 'icloud.com' + ? 'icloud.com' + : (hostPreference === 'icloud.com.cn' ? 'icloud.com.cn' : 'auto'); + } if (message.payload.autoRunSkipFailures !== undefined) { inputAutoSkipFailures.checked = Boolean(message.payload.autoRunSkipFailures); updateFallbackThreadIntervalInputState(); @@ -3944,6 +4746,21 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { break; } + case 'ICLOUD_LOGIN_REQUIRED': { + const loginMessage = '需要登录 iCloud,我已经为你打开登录页。'; + showToast(loginMessage, 'warn', 5000); + if (icloudSummary) { + icloudSummary.textContent = loginMessage; + } + showIcloudLoginHelp(message.payload || {}); + break; + } + + case 'ICLOUD_ALIASES_CHANGED': { + queueIcloudAliasRefresh(); + break; + } + case 'AUTO_RUN_STATUS': { syncLatestState({ autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(message.payload.phase), diff --git a/tests/auto-run-fresh-attempt-reset.test.js b/tests/auto-run-fresh-attempt-reset.test.js index f7078af..91533ea 100644 --- a/tests/auto-run-fresh-attempt-reset.test.js +++ b/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', @@ -180,6 +189,20 @@ function cancelPendingCommands() {} function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Math.max(0, Math.floor(Number(value) || 0)); } +function buildAutoRunRoundSummaries(totalRuns, rawSummaries = []) { + return Array.from({ length: totalRuns }, (_, index) => ({ + round: index + 1, + status: rawSummaries[index]?.status || 'pending', + attempts: rawSummaries[index]?.attempts || 0, + failureReasons: [...(rawSummaries[index]?.failureReasons || [])], + finalFailureReason: rawSummaries[index]?.finalFailureReason || '', + })); +} +function serializeAutoRunRoundSummaries(totalRuns, roundSummaries = []) { + return buildAutoRunRoundSummaries(totalRuns, roundSummaries); +} +async function logAutoRunFinalSummary() {} +async function waitBetweenAutoRunRounds() {} const chrome = { runtime: { diff --git a/tests/background-icloud.test.js b/tests/background-icloud.test.js new file mode 100644 index 0000000..c7f26db --- /dev/null +++ b/tests/background-icloud.test.js @@ -0,0 +1,265 @@ +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); +} + +const bundle = [ + extractFunction('normalizeEmailGenerator'), + extractFunction('getEmailGeneratorLabel'), + extractFunction('normalizePersistentSettingValue'), + extractFunction('finalizeIcloudAliasAfterSuccessfulFlow'), +].join('\n'); + +function createApi(overrides = {}) { + return new Function('overrides', ` +const HOTMAIL_PROVIDER = 'hotmail-api'; +const HOTMAIL_SERVICE_MODE_LOCAL = 'local'; +const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email'; +const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit'; +const DEFAULT_HOTMAIL_REMOTE_BASE_URL = ''; +const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373'; +const PERSISTED_SETTING_DEFAULTS = { + mailProvider: '163', + autoStepDelaySeconds: null, +}; + +const calls = { + setUsed: [], + logs: [], + deletes: [], + listCalls: 0, +}; + +function normalizeIcloudHost(value) { + const normalized = String(value || '').trim().toLowerCase(); + return ['icloud.com', 'icloud.com.cn'].includes(normalized) ? normalized : ''; +} +function normalizePanelMode(value = '') { + return String(value || '').trim().toLowerCase() === 'sub2api' ? 'sub2api' : 'cpa'; +} +function normalizeMailProvider(value = '') { + return String(value || '').trim().toLowerCase() || '163'; +} +function normalizeAutoRunFallbackThreadIntervalMinutes(value) { + return Math.max(0, Math.floor(Number(value) || 0)); +} +function normalizeAutoRunDelayMinutes(value) { + return Math.max(1, Math.floor(Number(value) || 30)); +} +function normalizeAutoStepDelaySeconds(value, fallback = null) { + const numeric = Number(value); + return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : fallback; +} +function normalizeHotmailServiceMode() { + return HOTMAIL_SERVICE_MODE_LOCAL; +} +function normalizeHotmailRemoteBaseUrl(value = '') { + return String(value || '').trim() || DEFAULT_HOTMAIL_REMOTE_BASE_URL; +} +function normalizeHotmailLocalBaseUrl(value = '') { + return String(value || '').trim() || DEFAULT_HOTMAIL_LOCAL_BASE_URL; +} +function normalizeCloudflareDomain(value = '') { + return String(value || '').trim().toLowerCase(); +} +function normalizeCloudflareDomains(values = []) { + return Array.isArray(values) ? values : []; +} +function normalizeHotmailAccounts(values = []) { + return Array.isArray(values) ? values : []; +} +function getManualAliasUsageMap(state) { + return { ...(state?.manualAliasUsage || {}) }; +} +function getPreservedAliasMap(state) { + return { ...(state?.preservedAliases || {}) }; +} +function isAliasPreserved(state, email) { + return Boolean(getPreservedAliasMap(state)[String(email || '').trim().toLowerCase()]); +} +async function setIcloudAliasUsedState(payload, options = {}) { + calls.setUsed.push({ payload, options }); +} +async function addLog(message, level = 'info') { + calls.logs.push({ message, level }); +} +async function deleteIcloudAlias(alias) { + calls.deletes.push(alias); +} +async function listIcloudAliases() { + calls.listCalls += 1; + return overrides.listIcloudAliases ? overrides.listIcloudAliases() : []; +} +function findIcloudAliasByEmail(aliases, email) { + return (aliases || []).find((alias) => String(alias.email || '').toLowerCase() === String(email || '').toLowerCase()) || null; +} +function getErrorMessage(error) { + return String(typeof error === 'string' ? error : error?.message || ''); +} + +${bundle} + +return { + calls, + normalizeEmailGenerator, + getEmailGeneratorLabel, + normalizePersistentSettingValue, + finalizeIcloudAliasAfterSuccessfulFlow, +}; +`)(overrides); +} + +test('normalizeEmailGenerator and label support icloud', () => { + const api = createApi(); + assert.equal(api.normalizeEmailGenerator('icloud'), 'icloud'); + assert.equal(api.getEmailGeneratorLabel('icloud'), 'iCloud 隐私邮箱'); +}); + +test('normalizePersistentSettingValue handles icloud settings', () => { + const api = createApi(); + assert.equal(api.normalizePersistentSettingValue('icloudHostPreference', 'icloud.com'), 'icloud.com'); + assert.equal(api.normalizePersistentSettingValue('icloudHostPreference', 'bad-host'), 'auto'); + assert.equal(api.normalizePersistentSettingValue('autoDeleteUsedIcloudAlias', 1), true); +}); + +test('finalizeIcloudAliasAfterSuccessfulFlow marks icloud aliases as used without deleting when auto-delete is off', async () => { + const api = createApi(); + const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({ + email: 'alias@icloud.com', + emailGenerator: 'icloud', + autoDeleteUsedIcloudAlias: false, + manualAliasUsage: {}, + preservedAliases: {}, + }); + + assert.deepEqual(result, { handled: true, deleted: false }); + assert.equal(api.calls.setUsed.length, 1); + assert.equal(api.calls.listCalls, 0); + assert.equal(api.calls.deletes.length, 0); +}); + +test('finalizeIcloudAliasAfterSuccessfulFlow skips deleting preserved aliases', async () => { + const api = createApi(); + const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({ + email: 'alias@icloud.com', + emailGenerator: 'icloud', + autoDeleteUsedIcloudAlias: true, + manualAliasUsage: {}, + preservedAliases: { 'alias@icloud.com': true }, + }); + + assert.deepEqual(result, { handled: true, deleted: false }); + assert.equal(api.calls.setUsed.length, 1); + assert.equal(api.calls.listCalls, 0); + assert.equal(api.calls.deletes.length, 0); +}); + +test('finalizeIcloudAliasAfterSuccessfulFlow skips deleting aliases that are preserved in the latest alias list', async () => { + const api = createApi({ + listIcloudAliases() { + return [ + { email: 'alias@icloud.com', anonymousId: 'anon-1', preserved: true }, + ]; + }, + }); + + const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({ + email: 'alias@icloud.com', + emailGenerator: 'icloud', + autoDeleteUsedIcloudAlias: true, + manualAliasUsage: {}, + preservedAliases: {}, + }); + + assert.deepEqual(result, { handled: true, deleted: false }); + assert.equal(api.calls.setUsed.length, 1); + assert.equal(api.calls.listCalls, 1); + assert.equal(api.calls.deletes.length, 0); +}); + +test('finalizeIcloudAliasAfterSuccessfulFlow deletes alias when auto-delete is enabled and alias exists', async () => { + const api = createApi({ + listIcloudAliases() { + return [ + { email: 'alias@icloud.com', anonymousId: 'anon-1', preserved: false }, + ]; + }, + }); + + const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({ + email: 'alias@icloud.com', + emailGenerator: 'icloud', + autoDeleteUsedIcloudAlias: true, + manualAliasUsage: {}, + preservedAliases: {}, + }); + + assert.deepEqual(result, { handled: true, deleted: true }); + assert.equal(api.calls.setUsed.length, 1); + assert.equal(api.calls.listCalls, 1); + assert.deepEqual(api.calls.deletes, [ + { email: 'alias@icloud.com', anonymousId: 'anon-1', preserved: false }, + ]); +}); + +test('finalizeIcloudAliasAfterSuccessfulFlow ignores non-icloud flows', async () => { + const api = createApi(); + const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({ + email: 'plain@example.com', + emailGenerator: 'duck', + autoDeleteUsedIcloudAlias: true, + manualAliasUsage: {}, + preservedAliases: {}, + }); + + assert.deepEqual(result, { handled: false, deleted: false }); + assert.equal(api.calls.setUsed.length, 0); +}); diff --git a/tests/background-luckmail.test.js b/tests/background-luckmail.test.js index d67942e..ff46891 100644 --- a/tests/background-luckmail.test.js +++ b/tests/background-luckmail.test.js @@ -440,6 +440,9 @@ test('resetState preserves LuckMail session config, used map, and preserve tag c 'async function getPersistedSettings() {', " return { mailProvider: '163' };", '}', + 'async function getPersistedAliasState() {', + ' return {};', + '}', 'const chrome = {', ' storage: {', ' session: {', @@ -545,6 +548,7 @@ function broadcastDataUpdate() {} function isLocalhostOAuthCallbackUrl() { return true; } +async function finalizeIcloudAliasAfterSuccessfulFlow() {} ${bundle} diff --git a/tests/cloudflare-temp-email-provider.test.js b/tests/cloudflare-temp-email-provider.test.js new file mode 100644 index 0000000..cc8a473 --- /dev/null +++ b/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, {}, {}), + /缺少目标邮箱地址/ + ); +}); diff --git a/tests/cloudflare-temp-email-utils.test.js b/tests/cloudflare-temp-email-utils.test.js new file mode 100644 index 0000000..cbf08a8 --- /dev/null +++ b/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 ', + '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 '); + 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 ', + 'Subject: Login code', + 'Content-Type: multipart/alternative; boundary="abc123"', + '', + '--abc123', + 'Content-Type: text/html; charset=UTF-8', + 'Content-Transfer-Encoding: quoted-printable', + '', + '

Your login code is 112233.

', + '--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'); +}); diff --git a/tests/icloud-utils.test.js b/tests/icloud-utils.test.js new file mode 100644 index 0000000..5d8fa68 --- /dev/null +++ b/tests/icloud-utils.test.js @@ -0,0 +1,121 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { + findIcloudAliasArray, + findIcloudAliasByEmail, + getConfiguredIcloudHostPreference, + getIcloudHostHintFromMessage, + getIcloudLoginUrlForHost, + getIcloudSetupUrlForHost, + normalizeBooleanMap, + normalizeIcloudAliasList, + normalizeIcloudAliasRecord, + normalizeIcloudHost, + pickReusableIcloudAlias, + toNormalizedEmailSet, +} = require('../icloud-utils.js'); + +test('normalizeIcloudHost and host preference helpers resolve supported hosts', () => { + assert.equal(normalizeIcloudHost('www.icloud.com'), 'icloud.com'); + assert.equal(normalizeIcloudHost('setup.icloud.com.cn'), 'icloud.com.cn'); + assert.equal(normalizeIcloudHost('example.com'), ''); + + assert.equal(getConfiguredIcloudHostPreference({ icloudHostPreference: 'icloud.com' }), 'icloud.com'); + assert.equal(getConfiguredIcloudHostPreference({ icloudHostPreference: 'auto' }), ''); + assert.equal(getIcloudLoginUrlForHost('icloud.com.cn'), 'https://www.icloud.com.cn/'); + assert.equal(getIcloudSetupUrlForHost('icloud.com'), 'https://setup.icloud.com/setup/ws/1'); +}); + +test('getIcloudHostHintFromMessage can infer host from error text', () => { + assert.equal(getIcloudHostHintFromMessage('status 401 from setup.icloud.com.cn/setup/ws/1'), 'icloud.com.cn'); + assert.equal(getIcloudHostHintFromMessage('request failed at https://www.icloud.com/'), 'icloud.com'); + assert.equal(getIcloudHostHintFromMessage('unknown host'), ''); +}); + +test('findIcloudAliasArray finds nested alias collections', () => { + const payload = { + result: { + data: { + items: [ + { hme: 'first@icloud.com', anonymousId: 'a1' }, + { hme: 'second@icloud.com', anonymousId: 'a2' }, + ], + }, + }, + }; + + assert.deepEqual(findIcloudAliasArray(payload), payload.result.data.items); +}); + +test('normalizeIcloudAliasRecord merges used and preserved state', () => { + const alias = normalizeIcloudAliasRecord({ + anonymousId: 'alias-1', + hme: 'Demo@iCloud.com', + label: 'Test', + note: 'Created by test', + state: 'active', + }, { + usedEmails: ['demo@icloud.com'], + preservedEmails: ['demo@icloud.com'], + }); + + assert.deepEqual(alias, { + anonymousId: 'alias-1', + email: 'demo@icloud.com', + label: 'Test', + note: 'Created by test', + state: 'active', + active: true, + used: true, + preserved: true, + createdAt: null, + }); +}); + +test('normalizeIcloudAliasList orders active unused aliases before used aliases', () => { + const aliases = normalizeIcloudAliasList({ + hmeEmails: [ + { hme: 'used@icloud.com', anonymousId: 'u1', active: true }, + { hme: 'fresh@icloud.com', anonymousId: 'f1', active: true }, + { hme: 'inactive@icloud.com', anonymousId: 'i1', active: false }, + ], + }, { + usedEmails: ['used@icloud.com'], + preservedEmails: ['inactive@icloud.com'], + }); + + assert.deepEqual(aliases.map((alias) => alias.email), [ + 'fresh@icloud.com', + 'used@icloud.com', + 'inactive@icloud.com', + ]); + assert.equal(aliases[2].preserved, true); +}); + +test('pickReusableIcloudAlias and findIcloudAliasByEmail select expected aliases', () => { + const aliases = normalizeIcloudAliasList({ + hmeEmails: [ + { hme: 'used@icloud.com', anonymousId: 'u1', active: true }, + { hme: 'fresh@icloud.com', anonymousId: 'f1', active: true }, + ], + }, { + usedEmails: ['used@icloud.com'], + }); + + assert.equal(pickReusableIcloudAlias(aliases)?.email, 'fresh@icloud.com'); + assert.equal(findIcloudAliasByEmail(aliases, 'FRESH@ICLOUD.COM')?.anonymousId, 'f1'); +}); + +test('normalizeBooleanMap and toNormalizedEmailSet normalize keys and truthy entries', () => { + const normalized = normalizeBooleanMap({ + ' Demo@icloud.com ': 1, + 'skip@icloud.com': 0, + }); + + assert.deepEqual(normalized, { + 'demo@icloud.com': true, + 'skip@icloud.com': false, + }); + assert.deepEqual([...toNormalizedEmailSet(normalized)], ['demo@icloud.com']); +}); diff --git a/tests/step9-localhost-cleanup-scope.test.js b/tests/step9-localhost-cleanup-scope.test.js index 788545d..9587c8c 100644 --- a/tests/step9-localhost-cleanup-scope.test.js +++ b/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 }, @@ -122,6 +130,11 @@ async function addLog(message) { logMessages.push(message); } +async function finalizeIcloudAliasAfterSuccessfulFlow() {} +function shouldUseCustomRegistrationEmail() { + return false; +} + ${bundle} return { @@ -183,8 +196,8 @@ return { let snapshot = api.snapshot(); assert.deepStrictEqual( snapshot.removedBatches, - [[1], [2, 3]], - 'handleStepData(9) 应先关闭当前 callback 页,再按同前缀路径清理残留页' + [[1], [2]], + 'handleStepData(9) 应先关闭当前 callback 页,再按同源首段路径清理残留页' ); assert.strictEqual( snapshot.currentState.tabRegistry['signup-page'], diff --git a/tests/verification-stop-propagation.test.js b/tests/verification-stop-propagation.test.js index 291f3e9..dba7532 100644 --- a/tests/verification-stop-propagation.test.js +++ b/tests/verification-stop-propagation.test.js @@ -57,11 +57,12 @@ 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 LUCKMAIL_PROVIDER = 'luckmail-api'; +const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email'; const VERIFICATION_POLL_MAX_ROUNDS = 5; const logs = []; let resendCalls = 0; @@ -123,10 +124,11 @@ 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 LUCKMAIL_PROVIDER = 'luckmail-api'; +const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email'; const logs = []; let pollCalls = 0;