feat: integrate iCloud hide my email generation into OAuth flow

- 合并 PR #72 的核心改动:新增 iCloud Hide My Email 生成、alias 管理与成功后自动标记/删除流程
- 本地补充修复:解决与 Cloudflare Temp Email 分支差异的冲突,并补齐合并后的测试常量
- 影响范围:background 邮箱生成与 Step 9 收尾、sidepanel alias 管理 UI、manifest/rules 与新增 iCloud 测试
This commit is contained in:
Q3CC
2026-04-14 23:16:59 +08:00
parent d5a63f9e55
commit 14e3d6271d
11 changed files with 2007 additions and 11 deletions
+572 -4
View File
@@ -1,6 +1,6 @@
// background.js — Service Worker: orchestration, state, tab management, message routing // background.js — Service Worker: orchestration, state, tab management, message routing
importScripts('data/names.js', 'hotmail-utils.js', 'cloudflare-temp-email-utils.js', 'content/activation-utils.js'); importScripts('data/names.js', 'hotmail-utils.js', 'cloudflare-temp-email-utils.js', 'icloud-utils.js', 'content/activation-utils.js');
const { const {
buildHotmailMailApiLatestUrl, buildHotmailMailApiLatestUrl,
@@ -28,12 +28,32 @@ const {
normalizeCloudflareTempEmailDomains, normalizeCloudflareTempEmailDomains,
normalizeCloudflareTempEmailMailApiMessages, normalizeCloudflareTempEmailMailApiMessages,
} = self.CloudflareTempEmailUtils; } = self.CloudflareTempEmailUtils;
const {
findIcloudAliasByEmail,
getConfiguredIcloudHostPreference,
getIcloudHostHintFromMessage,
getIcloudLoginUrlForHost,
getIcloudSetupUrlForHost,
normalizeBooleanMap,
normalizeIcloudAliasList,
normalizeIcloudHost,
pickReusableIcloudAlias,
toNormalizedEmailSet,
} = self.IcloudUtils;
const { const {
isRecoverableStep9AuthFailure, isRecoverableStep9AuthFailure,
} = self.MultiPageActivationUtils; } = self.MultiPageActivationUtils;
const LOG_PREFIX = '[MultiPage:bg]'; const LOG_PREFIX = '[MultiPage:bg]';
const DUCK_AUTOFILL_URL = 'https://duckduckgo.com/email/settings/autofill'; 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 HOTMAIL_PROVIDER = 'hotmail-api';
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email'; const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email'; const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
@@ -62,6 +82,7 @@ const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373'; const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373';
const HOTMAIL_LOCAL_HELPER_TIMEOUT_MS = 45000; const HOTMAIL_LOCAL_HELPER_TIMEOUT_MS = 45000;
const DISPLAY_TIMEZONE = 'Asia/Shanghai'; const DISPLAY_TIMEZONE = 'Asia/Shanghai';
const PERSISTENT_ALIAS_STATE_KEYS = ['manualAliasUsage', 'preservedAliases'];
initializeSessionStorageAccess(); initializeSessionStorageAccess();
@@ -86,6 +107,8 @@ const PERSISTED_SETTING_DEFAULTS = {
autoStepDelaySeconds: null, autoStepDelaySeconds: null,
mailProvider: '163', mailProvider: '163',
emailGenerator: 'duck', emailGenerator: 'duck',
autoDeleteUsedIcloudAlias: false,
icloudHostPreference: 'auto',
emailPrefix: '', emailPrefix: '',
inbucketHost: '', inbucketHost: '',
inbucketMailbox: '', inbucketMailbox: '',
@@ -116,6 +139,8 @@ const DEFAULT_STATE = {
email: null, // 运行时邮箱,由程序自动获取并写入,不能手动预填。 email: null, // 运行时邮箱,由程序自动获取并写入,不能手动预填。
password: null, // 运行时实际密码,由 customPassword 或程序自动生成后写入。 password: null, // 运行时实际密码,由 customPassword 或程序自动生成后写入。
accounts: [], // 已生成账号记录:{ email, password, createdAt }。 accounts: [], // 已生成账号记录:{ email, password, createdAt }。
manualAliasUsage: {},
preservedAliases: {},
lastEmailTimestamp: null, // 最近一次获取到邮箱数据的运行时时间戳。 lastEmailTimestamp: null, // 最近一次获取到邮箱数据的运行时时间戳。
lastSignupCode: null, // 注册验证码,运行时由程序自动读取并写入。 lastSignupCode: null, // 注册验证码,运行时由程序自动读取并写入。
lastLoginCode: null, // 登录验证码,运行时由程序自动读取并写入。 lastLoginCode: null, // 登录验证码,运行时由程序自动读取并写入。
@@ -143,6 +168,7 @@ const DEFAULT_STATE = {
signupVerificationRequestedAt: null, signupVerificationRequestedAt: null,
loginVerificationRequestedAt: null, loginVerificationRequestedAt: null,
currentHotmailAccountId: null, currentHotmailAccountId: null,
preferredIcloudHost: '',
}; };
function normalizeAutoRunDelayMinutes(value) { function normalizeAutoRunDelayMinutes(value) {
@@ -236,6 +262,9 @@ function normalizeEmailGenerator(value = '') {
if (normalized === 'custom' || normalized === 'manual') { if (normalized === 'custom' || normalized === 'manual') {
return 'custom'; return 'custom';
} }
if (normalized === 'icloud') {
return 'icloud';
}
if (normalized === 'cloudflare') return 'cloudflare'; if (normalized === 'cloudflare') return 'cloudflare';
if (normalized === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return CLOUDFLARE_TEMP_EMAIL_GENERATOR; if (normalized === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return CLOUDFLARE_TEMP_EMAIL_GENERATOR;
return 'duck'; return 'duck';
@@ -391,6 +420,10 @@ function normalizePersistentSettingValue(key, value) {
return normalizeMailProvider(value); return normalizeMailProvider(value);
case 'emailGenerator': case 'emailGenerator':
return normalizeEmailGenerator(value); return normalizeEmailGenerator(value);
case 'autoDeleteUsedIcloudAlias':
return Boolean(value);
case 'icloudHostPreference':
return normalizeIcloudHost(value) || 'auto';
case 'emailPrefix': case 'emailPrefix':
return String(value || '').trim(); return String(value || '').trim();
case 'inbucketHost': case 'inbucketHost':
@@ -475,12 +508,29 @@ async function getPersistedSettings() {
return buildPersistentSettingsPayload(stored, { fillDefaults: true }); 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() { async function getState() {
const [state, persistedSettings] = await Promise.all([ const [state, persistedSettings, persistedAliasState] = await Promise.all([
chrome.storage.session.get(null), chrome.storage.session.get(null),
getPersistedSettings(), getPersistedSettings(),
getPersistedAliasState(),
]); ]);
return { ...DEFAULT_STATE, ...persistedSettings, ...state }; return { ...DEFAULT_STATE, ...persistedSettings, ...persistedAliasState, ...state };
} }
async function initializeSessionStorageAccess() { async function initializeSessionStorageAccess() {
@@ -500,6 +550,16 @@ async function setState(updates) {
console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200)); console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
if (Object.keys(updates || {}).length > 0) { if (Object.keys(updates || {}).length > 0) {
await chrome.storage.session.set(updates); 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);
}
} }
} }
@@ -578,6 +638,13 @@ function broadcastDataUpdate(payload) {
}).catch(() => { }); }).catch(() => { });
} }
function broadcastIcloudAliasesChanged(payload = {}) {
chrome.runtime.sendMessage({
type: 'ICLOUD_ALIASES_CHANGED',
payload,
}).catch(() => { });
}
async function setEmailStateSilently(email) { async function setEmailStateSilently(email) {
await setState({ email }); await setState({ email });
broadcastDataUpdate({ email }); broadcastDataUpdate({ email });
@@ -595,28 +662,84 @@ async function setPasswordState(password) {
broadcastDataUpdate({ password }); broadcastDataUpdate({ password });
} }
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() { async function resetState() {
console.log(LOG_PREFIX, 'Resetting all state'); console.log(LOG_PREFIX, 'Resetting all state');
// Preserve settings and persistent data across resets // Preserve settings and persistent data across resets
const [prev, persistedSettings] = await Promise.all([ const [prev, persistedSettings, persistedAliasState] = await Promise.all([
chrome.storage.session.get([ chrome.storage.session.get([
'seenCodes', 'seenCodes',
'seenInbucketMailIds', 'seenInbucketMailIds',
'accounts', 'accounts',
'tabRegistry', 'tabRegistry',
'sourceLastUrls', 'sourceLastUrls',
'preferredIcloudHost',
]), ]),
getPersistedSettings(), getPersistedSettings(),
getPersistedAliasState(),
]); ]);
await chrome.storage.session.clear(); await chrome.storage.session.clear();
await chrome.storage.session.set({ await chrome.storage.session.set({
...DEFAULT_STATE, ...DEFAULT_STATE,
...persistedSettings, ...persistedSettings,
...persistedAliasState,
seenCodes: prev.seenCodes || [], seenCodes: prev.seenCodes || [],
seenInbucketMailIds: prev.seenInbucketMailIds || [], seenInbucketMailIds: prev.seenInbucketMailIds || [],
accounts: prev.accounts || [], accounts: prev.accounts || [],
tabRegistry: prev.tabRegistry || {}, tabRegistry: prev.tabRegistry || {},
sourceLastUrls: prev.sourceLastUrls || {}, sourceLastUrls: prev.sourceLastUrls || {},
preferredIcloudHost: prev.preferredIcloudHost || '',
}); });
} }
@@ -1460,6 +1583,408 @@ async function pollCloudflareTempEmailVerificationCode(step, state, pollPayload
throw lastError || new Error(`步骤 ${step}:未在 Cloudflare Temp Email 中找到新的匹配验证码。`); 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 // Tab Registry
// ============================================================ // ============================================================
@@ -1699,6 +2224,7 @@ async function closeTabsByUrlPrefix(prefix, options = {}) {
.filter((tab) => typeof tab.url === 'string' && !excludedUrls.has(tab.url)) .filter((tab) => typeof tab.url === 'string' && !excludedUrls.has(tab.url))
.filter((tab) => !(excludeLocalhostCallbacks && isLocalhostOAuthCallbackUrl(tab.url))) .filter((tab) => !(excludeLocalhostCallbacks && isLocalhostOAuthCallbackUrl(tab.url)))
.filter((tab) => typeof tab.url === 'string' && tab.url.startsWith(prefix)) .filter((tab) => typeof tab.url === 'string' && tab.url.startsWith(prefix))
.filter((tab) => !isLocalhostOAuthCallbackUrl(tab.url))
.map((tab) => tab.id); .map((tab) => tab.id);
if (!matchedIds.length) return 0; if (!matchedIds.length) return 0;
@@ -3222,6 +3748,41 @@ async function handleMessage(message, sender) {
return { ok: true, email }; 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': { case 'STOP_FLOW': {
await requestStop(); await requestStop();
return { ok: true }; return { ok: true };
@@ -3308,6 +3869,7 @@ async function handleStepData(step, payload) {
excludeLocalhostCallbacks: true, excludeLocalhostCallbacks: true,
}); });
} }
await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
if (shouldUseCustomRegistrationEmail(latestState) && latestState.email) { if (shouldUseCustomRegistrationEmail(latestState) && latestState.email) {
await setEmailStateSilently(null); await setEmailStateSilently(null);
} }
@@ -3599,6 +4161,9 @@ function getEmailGeneratorLabel(generator) {
if (generator === 'custom') { if (generator === 'custom') {
return '自定义邮箱'; return '自定义邮箱';
} }
if (generator === 'icloud') {
return 'iCloud 隐私邮箱';
}
if (generator === 'cloudflare') return 'Cloudflare 邮箱'; if (generator === 'cloudflare') return 'Cloudflare 邮箱';
if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email'; if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email';
return 'Duck 邮箱'; return 'Duck 邮箱';
@@ -3773,6 +4338,9 @@ async function fetchGeneratedEmail(state, options = {}) {
if (generator === 'custom') { if (generator === 'custom') {
throw new Error('当前邮箱生成方式为自定义邮箱,请直接填写注册邮箱。'); throw new Error('当前邮箱生成方式为自定义邮箱,请直接填写注册邮箱。');
} }
if (generator === 'icloud') {
return fetchIcloudHideMyEmail();
}
if (generator === 'cloudflare') { if (generator === 'cloudflare') {
return fetchCloudflareEmail(currentState, options); return fetchCloudflareEmail(currentState, options);
} }
+177
View File
@@ -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,
};
});
+12
View File
@@ -8,17 +8,29 @@
"alarms", "alarms",
"tabs", "tabs",
"webNavigation", "webNavigation",
"declarativeNetRequest",
"debugger", "debugger",
"storage", "storage",
"scripting", "scripting",
"activeTab" "activeTab"
], ],
"host_permissions": [ "host_permissions": [
"https://*.icloud.com/*",
"https://*.icloud.com.cn/*",
"<all_urls>" "<all_urls>"
], ],
"background": { "background": {
"service_worker": "background.js" "service_worker": "background.js"
}, },
"declarative_net_request": {
"rule_resources": [
{
"id": "icloud_headers",
"enabled": true,
"path": "rules.json"
}
]
},
"side_panel": { "side_panel": {
"default_path": "sidepanel/sidepanel.html" "default_path": "sidepanel/sidepanel.html"
}, },
+50
View File
@@ -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"]
}
}
]
+157
View File
@@ -877,6 +877,163 @@ header {
text-align: center; 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 { .data-select {
flex: 1; flex: 1;
padding: 7px 10px; padding: 7px 10px;
+61
View File
@@ -169,6 +169,7 @@
<select id="select-email-generator" class="data-select"> <select id="select-email-generator" class="data-select">
<option value="duck">DuckDuckGo</option> <option value="duck">DuckDuckGo</option>
<option value="cloudflare">Cloudflare</option> <option value="cloudflare">Cloudflare</option>
<option value="icloud">iCloud 隐私邮箱</option>
<option value="cloudflare-temp-email">Cloudflare Temp Email</option> <option value="cloudflare-temp-email">Cloudflare Temp Email</option>
</select> </select>
</div> </div>
@@ -339,6 +340,66 @@
<div id="hotmail-accounts-list" class="hotmail-accounts-list"></div> <div id="hotmail-accounts-list" class="hotmail-accounts-list"></div>
</div> </div>
</div> </div>
<div id="icloud-section" class="data-card hotmail-card" style="display:none;">
<div class="section-mini-header">
<div class="section-mini-copy">
<span class="section-label">iCloud 隐私邮箱</span>
</div>
<div class="section-mini-actions">
<button id="btn-icloud-refresh" class="btn btn-ghost btn-xs" type="button">刷新</button>
<button id="btn-icloud-delete-used" class="btn btn-ghost btn-xs" type="button">删除已用</button>
</div>
</div>
<div class="data-row">
<span class="data-label">iCloud</span>
<select id="select-icloud-host-preference" class="data-select">
<option value="auto">自动</option>
<option value="icloud.com">iCloud.com</option>
<option value="icloud.com.cn">iCloud.com.cn</option>
</select>
</div>
<div class="data-row">
<span class="data-label">自动删除</span>
<label class="option-toggle" for="checkbox-auto-delete-icloud">
<input type="checkbox" id="checkbox-auto-delete-icloud" />
<span>成功使用后自动删除 iCloud 别名</span>
</label>
</div>
<div class="icloud-card">
<div id="icloud-summary" class="icloud-summary">加载你的 iCloud Hide My Email 别名以便在这里管理。</div>
<div id="icloud-login-help" class="icloud-login-help" style="display:none;">
<div class="icloud-login-help-main">
<div id="icloud-login-help-title" class="icloud-login-help-title">需要登录 iCloud</div>
<div id="icloud-login-help-text" class="icloud-login-help-text">我已经为你打开 iCloud 登录页。请在那个页面完成登录,然后回到这里点击“我已登录”。</div>
</div>
<button id="btn-icloud-login-done" class="btn btn-primary btn-xs" type="button">我已登录</button>
</div>
<div class="icloud-toolbar">
<input id="input-icloud-search" class="data-input icloud-search" type="text" placeholder="搜索邮箱 / 标签 / 备注" />
<select id="select-icloud-filter" class="data-select icloud-filter">
<option value="all">全部</option>
<option value="active">可用</option>
<option value="used">已用</option>
<option value="unused">未用</option>
<option value="preserved">保留</option>
</select>
</div>
<div class="icloud-bulkbar">
<label class="option-toggle icloud-select-all" for="checkbox-icloud-select-all">
<input type="checkbox" id="checkbox-icloud-select-all" />
<span id="icloud-selection-summary">已选 0 个</span>
</label>
<div class="icloud-bulk-actions">
<button id="btn-icloud-bulk-used" class="btn btn-outline btn-xs" type="button">标记已用</button>
<button id="btn-icloud-bulk-unused" class="btn btn-outline btn-xs" type="button">标记未用</button>
<button id="btn-icloud-bulk-preserve" class="btn btn-outline btn-xs" type="button">保留</button>
<button id="btn-icloud-bulk-unpreserve" class="btn btn-outline btn-xs" type="button">取消保留</button>
<button id="btn-icloud-bulk-delete" class="btn btn-outline btn-xs" type="button">删除</button>
</div>
</div>
<div id="icloud-list" class="icloud-list"></div>
</div>
</div>
<div id="status-bar" class="status-bar"> <div id="status-bar" class="status-bar">
<div class="status-dot"></div> <div class="status-dot"></div>
<span id="display-status">就绪</span> <span id="display-status">就绪</span>
+573 -7
View File
@@ -79,6 +79,26 @@ const selectTempEmailDomain = document.getElementById('select-temp-email-domain'
const inputTempEmailDomain = document.getElementById('input-temp-email-domain'); const inputTempEmailDomain = document.getElementById('input-temp-email-domain');
const btnTempEmailDomainMode = document.getElementById('btn-temp-email-domain-mode'); const btnTempEmailDomainMode = document.getElementById('btn-temp-email-domain-mode');
const hotmailSection = document.getElementById('hotmail-section'); const hotmailSection = document.getElementById('hotmail-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 rowHotmailServiceMode = document.getElementById('row-hotmail-service-mode');
const hotmailServiceModeButtons = Array.from(document.querySelectorAll('[data-hotmail-service-mode]')); const hotmailServiceModeButtons = Array.from(document.querySelectorAll('[data-hotmail-service-mode]'));
const rowHotmailRemoteBaseUrl = document.getElementById('row-hotmail-remote-base-url'); const rowHotmailRemoteBaseUrl = document.getElementById('row-hotmail-remote-base-url');
@@ -174,6 +194,11 @@ let settingsSaveInFlight = false;
let settingsAutoSaveTimer = null; let settingsAutoSaveTimer = null;
let cloudflareDomainEditMode = false; let cloudflareDomainEditMode = false;
let cloudflareTempEmailDomainEditMode = false; let cloudflareTempEmailDomainEditMode = false;
let icloudRefreshQueued = false;
let lastRenderedIcloudAliases = [];
let icloudSelectedEmails = new Set();
let icloudSearchTerm = '';
let icloudFilterMode = 'all';
let modalChoiceResolver = null; let modalChoiceResolver = null;
let currentModalActions = []; let currentModalActions = [];
let modalResultBuilder = null; let modalResultBuilder = null;
@@ -1032,6 +1057,8 @@ function collectSettingsPayload() {
customPassword: inputPassword.value, customPassword: inputPassword.value,
mailProvider: selectMailProvider.value, mailProvider: selectMailProvider.value,
emailGenerator: selectEmailGenerator.value, emailGenerator: selectEmailGenerator.value,
autoDeleteUsedIcloudAlias: checkboxAutoDeleteIcloud?.checked,
icloudHostPreference: selectIcloudHostPreference?.value || 'auto',
emailPrefix: inputEmailPrefix.value.trim(), emailPrefix: inputEmailPrefix.value.trim(),
inbucketHost: inputInbucketHost.value.trim(), inbucketHost: inputInbucketHost.value.trim(),
inbucketMailbox: inputInbucketMailbox.value.trim(), inbucketMailbox: inputInbucketMailbox.value.trim(),
@@ -1323,10 +1350,26 @@ function applySettingsState(state) {
? 'custom' ? 'custom'
: '163'); : '163');
selectMailProvider.value = restoredMailProvider; selectMailProvider.value = restoredMailProvider;
const restoredEmailGenerator = String(state?.emailGenerator || '').trim().toLowerCase(); {
selectEmailGenerator.value = ['cloudflare', 'cloudflare-temp-email'].includes(restoredEmailGenerator) const restoredGenerator = String(state?.emailGenerator || '').trim().toLowerCase();
? restoredEmailGenerator if (restoredGenerator === 'icloud') {
: 'duck'; 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 || ''; inputEmailPrefix.value = state?.emailPrefix || '';
inputInbucketHost.value = state?.inbucketHost || ''; inputInbucketHost.value = state?.inbucketHost || '';
inputInbucketMailbox.value = state?.inbucketMailbox || ''; inputInbucketMailbox.value = state?.inbucketMailbox || '';
@@ -1362,6 +1405,9 @@ async function restoreState() {
try { try {
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }); const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' });
applySettingsState(state); applySettingsState(state);
if (getSelectedEmailGenerator() === 'icloud' && icloudSection?.style.display !== 'none') {
refreshIcloudAliases({ silent: true }).catch(() => { });
}
if (state.oauthUrl) { if (state.oauthUrl) {
displayOauthUrl.textContent = state.oauthUrl; displayOauthUrl.textContent = state.oauthUrl;
@@ -1609,6 +1655,9 @@ function getSelectedEmailGenerator() {
if (generator === 'custom' || generator === 'manual') { if (generator === 'custom' || generator === 'manual') {
return 'custom'; return 'custom';
} }
if (generator === 'icloud') {
return 'icloud';
}
if (generator === 'cloudflare') return 'cloudflare'; if (generator === 'cloudflare') return 'cloudflare';
if (generator === 'cloudflare-temp-email') return 'cloudflare-temp-email'; if (generator === 'cloudflare-temp-email') return 'cloudflare-temp-email';
return 'duck'; return 'duck';
@@ -1618,6 +1667,14 @@ function getEmailGeneratorUiCopy() {
if (getSelectedEmailGenerator() === 'custom') { if (getSelectedEmailGenerator() === 'custom') {
return getCustomMailProviderUiCopy(); return getCustomMailProviderUiCopy();
} }
if (getSelectedEmailGenerator() === 'icloud') {
return {
buttonLabel: '获取',
placeholder: '点击获取 iCloud 隐私邮箱,或手动粘贴邮箱',
successVerb: '获取',
label: 'iCloud 隐私邮箱',
};
}
if (getSelectedEmailGenerator() === 'cloudflare') { if (getSelectedEmailGenerator() === 'cloudflare') {
return { return {
buttonLabel: '生成', buttonLabel: '生成',
@@ -1946,14 +2003,26 @@ function updateMailProviderUI() {
const hotmailServiceMode = getSelectedHotmailServiceMode(); const hotmailServiceMode = getSelectedHotmailServiceMode();
rowInbucketHost.style.display = useInbucket ? '' : 'none'; rowInbucketHost.style.display = useInbucket ? '' : 'none';
rowInbucketMailbox.style.display = useInbucket ? '' : 'none'; rowInbucketMailbox.style.display = useInbucket ? '' : 'none';
const useCloudflare = selectEmailGenerator.value === 'cloudflare'; const selectedGenerator = getSelectedEmailGenerator();
const useCloudflareTempEmailGenerator = selectEmailGenerator.value === 'cloudflare-temp-email'; const useCloudflare = selectedGenerator === 'cloudflare';
const useIcloud = selectedGenerator === 'icloud';
const useCloudflareTempEmailGenerator = selectedGenerator === 'cloudflare-temp-email';
const showCloudflareDomain = useEmailGenerator && useCloudflare; const showCloudflareDomain = useEmailGenerator && useCloudflare;
const showCloudflareTempEmailSettings = useCloudflareTempEmailProvider || (useEmailGenerator && useCloudflareTempEmailGenerator); const showCloudflareTempEmailSettings = useCloudflareTempEmailProvider || (useEmailGenerator && useCloudflareTempEmailGenerator);
const showCloudflareTempEmailDomain = useEmailGenerator && useCloudflareTempEmailGenerator; const showCloudflareTempEmailDomain = useEmailGenerator && useCloudflareTempEmailGenerator;
if (rowEmailGenerator) { if (rowEmailGenerator) {
rowEmailGenerator.style.display = useEmailGenerator ? '' : 'none'; 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'; rowCfDomain.style.display = showCloudflareDomain ? '' : 'none';
const { domains } = getCloudflareDomainsFromState(); const { domains } = getCloudflareDomainsFromState();
if (showCloudflareDomain) { if (showCloudflareDomain) {
@@ -2002,7 +2071,7 @@ function updateMailProviderUI() {
? '请先校验并选择一个 Hotmail 账号' ? '请先校验并选择一个 Hotmail 账号'
: (useGeneratedAlias : (useGeneratedAlias
? '步骤 3 会自动生成邮箱,无需手动获取' ? '步骤 3 会自动生成邮箱,无需手动获取'
: (useCustomEmail ? '请先填写自定义注册邮箱,成功一轮后会自动清空' : '先自动获取邮箱,或手动粘贴邮箱后再继续')); : (useCustomEmail ? '请先填写自定义注册邮箱,成功一轮后会自动清空' : `先自动获取${uiCopy.label},或手动粘贴邮箱后再继续`));
} }
if (useHotmail) { if (useHotmail) {
inputEmail.value = getCurrentHotmailEmail(); inputEmail.value = getCurrentHotmailEmail();
@@ -2166,6 +2235,11 @@ function updateButtonStates() {
}); });
btnReset.disabled = anyRunning || autoScheduled || isAutoRunPausedPhase() || autoLocked; 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); updateStopButtonState(anyRunning || autoScheduled || isAutoRunPausedPhase() || autoLocked);
} }
@@ -2288,6 +2362,375 @@ function escapeHtml(text) {
return div.innerHTML; 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 = '<div class="icloud-empty">未找到 iCloud Hide My Email 别名。</div>';
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 = '<div class="icloud-empty">没有匹配当前筛选条件的别名。</div>';
updateIcloudBulkUI([]);
return;
}
for (const alias of visibleAliases) {
const item = document.createElement('div');
item.className = 'icloud-item';
item.innerHTML = `
<input class="icloud-item-check" type="checkbox" data-action="select" ${icloudSelectedEmails.has(alias.email) ? 'checked' : ''} />
<div class="icloud-item-main">
<div class="icloud-item-email">${escapeHtml(alias.email)}</div>
<div class="icloud-item-meta">
${alias.used ? '<span class="icloud-tag used">已用</span>' : ''}
${!alias.used && alias.active ? '<span class="icloud-tag active">可用</span>' : ''}
${alias.preserved ? '<span class="icloud-tag">保留</span>' : ''}
${alias.label ? `<span class="icloud-tag">${escapeHtml(alias.label)}</span>` : ''}
${alias.note ? `<span class="icloud-tag">${escapeHtml(alias.note)}</span>` : ''}
</div>
</div>
<div class="icloud-item-actions">
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-used">${escapeHtml(alias.used ? '标记未用' : '标记已用')}</button>
<button class="btn btn-outline btn-xs" type="button" data-action="toggle-preserved">${escapeHtml(alias.preserved ? '取消保留' : '保留')}</button>
<button class="btn btn-outline btn-xs" type="button" data-action="delete">删除</button>
</div>
`;
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 = '<div class="icloud-empty">无法加载 iCloud 别名。</div>';
}
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 = {}) { async function fetchGeneratedEmail(options = {}) {
const { showFailureToast = true } = options; const { showFailureToast = true } = options;
const uiCopy = getEmailGeneratorUiCopy(); const uiCopy = getEmailGeneratorUiCopy();
@@ -2316,6 +2759,9 @@ async function fetchGeneratedEmail(options = {}) {
} }
inputEmail.value = response.email; inputEmail.value = response.email;
if (getSelectedEmailGenerator() === 'icloud') {
queueIcloudAliasRefresh();
}
showToast(`${uiCopy.successVerb} ${uiCopy.label}${response.email}`, 'success', 2500); showToast(`${uiCopy.successVerb} ${uiCopy.label}${response.email}`, 'success', 2500);
return response.email; return response.email;
} catch (err) { } catch (err) {
@@ -2628,6 +3074,75 @@ btnFetchEmail.addEventListener('click', async () => {
await fetchGeneratedEmail().catch(() => { }); 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', () => { btnToggleHotmailList?.addEventListener('click', () => {
setHotmailListExpanded(!hotmailListExpanded); setHotmailListExpanded(!hotmailListExpanded);
}); });
@@ -3106,6 +3621,15 @@ btnReset.addEventListener('click', async () => {
displayStatus.textContent = '就绪'; displayStatus.textContent = '就绪';
statusBar.className = 'status-bar'; statusBar.className = 'status-bar';
logArea.innerHTML = ''; 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-row').forEach(row => row.className = 'step-row');
document.querySelectorAll('.step-status').forEach(el => el.textContent = ''); document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
setDefaultAutoRunButton(); setDefaultAutoRunButton();
@@ -3202,6 +3726,19 @@ selectEmailGenerator.addEventListener('change', () => {
saveSettings({ silent: true }).catch(() => { }); 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', () => { selectPanelMode.addEventListener('change', () => {
updatePanelModeUI(); updatePanelModeUI();
markSettingsDirty(true); markSettingsDirty(true);
@@ -3502,6 +4039,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
displayStatus.textContent = '就绪'; displayStatus.textContent = '就绪';
statusBar.className = 'status-bar'; statusBar.className = 'status-bar';
logArea.innerHTML = ''; 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-row').forEach(row => row.className = 'step-row');
document.querySelectorAll('.step-status').forEach(el => el.textContent = ''); document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
syncAutoRunState({ syncAutoRunState({
@@ -3559,6 +4101,15 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
inputEmail.value = getCurrentHotmailEmail(); inputEmail.value = getCurrentHotmailEmail();
} }
} }
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) { if (message.payload.autoRunSkipFailures !== undefined) {
inputAutoSkipFailures.checked = Boolean(message.payload.autoRunSkipFailures); inputAutoSkipFailures.checked = Boolean(message.payload.autoRunSkipFailures);
updateFallbackThreadIntervalInputState(); updateFallbackThreadIntervalInputState();
@@ -3582,6 +4133,21 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
break; 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': { case 'AUTO_RUN_STATUS': {
syncLatestState({ syncLatestState({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(message.payload.phase), autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(message.payload.phase),
@@ -189,6 +189,20 @@ function cancelPendingCommands() {}
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { function normalizeAutoRunFallbackThreadIntervalMinutes(value) {
return Math.max(0, Math.floor(Number(value) || 0)); 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 = { const chrome = {
runtime: { runtime: {
+265
View File
@@ -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);
});
+121
View File
@@ -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']);
});
@@ -110,6 +110,11 @@ async function addLog(message) {
logMessages.push(message); logMessages.push(message);
} }
async function finalizeIcloudAliasAfterSuccessfulFlow() {}
function shouldUseCustomRegistrationEmail() {
return false;
}
${bundle} ${bundle}
return { return {