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:
+572
-4
@@ -1,6 +1,6 @@
|
||||
// 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 {
|
||||
buildHotmailMailApiLatestUrl,
|
||||
@@ -28,12 +28,32 @@ const {
|
||||
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 CLOUDFLARE_TEMP_EMAIL_PROVIDER = '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 HOTMAIL_LOCAL_HELPER_TIMEOUT_MS = 45000;
|
||||
const DISPLAY_TIMEZONE = 'Asia/Shanghai';
|
||||
const PERSISTENT_ALIAS_STATE_KEYS = ['manualAliasUsage', 'preservedAliases'];
|
||||
|
||||
initializeSessionStorageAccess();
|
||||
|
||||
@@ -86,6 +107,8 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
autoStepDelaySeconds: null,
|
||||
mailProvider: '163',
|
||||
emailGenerator: 'duck',
|
||||
autoDeleteUsedIcloudAlias: false,
|
||||
icloudHostPreference: 'auto',
|
||||
emailPrefix: '',
|
||||
inbucketHost: '',
|
||||
inbucketMailbox: '',
|
||||
@@ -116,6 +139,8 @@ const DEFAULT_STATE = {
|
||||
email: null, // 运行时邮箱,由程序自动获取并写入,不能手动预填。
|
||||
password: null, // 运行时实际密码,由 customPassword 或程序自动生成后写入。
|
||||
accounts: [], // 已生成账号记录:{ email, password, createdAt }。
|
||||
manualAliasUsage: {},
|
||||
preservedAliases: {},
|
||||
lastEmailTimestamp: null, // 最近一次获取到邮箱数据的运行时时间戳。
|
||||
lastSignupCode: null, // 注册验证码,运行时由程序自动读取并写入。
|
||||
lastLoginCode: null, // 登录验证码,运行时由程序自动读取并写入。
|
||||
@@ -143,6 +168,7 @@ const DEFAULT_STATE = {
|
||||
signupVerificationRequestedAt: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
currentHotmailAccountId: null,
|
||||
preferredIcloudHost: '',
|
||||
};
|
||||
|
||||
function normalizeAutoRunDelayMinutes(value) {
|
||||
@@ -236,6 +262,9 @@ function normalizeEmailGenerator(value = '') {
|
||||
if (normalized === 'custom' || normalized === 'manual') {
|
||||
return 'custom';
|
||||
}
|
||||
if (normalized === 'icloud') {
|
||||
return 'icloud';
|
||||
}
|
||||
if (normalized === 'cloudflare') return 'cloudflare';
|
||||
if (normalized === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return CLOUDFLARE_TEMP_EMAIL_GENERATOR;
|
||||
return 'duck';
|
||||
@@ -391,6 +420,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':
|
||||
@@ -475,12 +508,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() {
|
||||
@@ -500,6 +550,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,6 +638,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 });
|
||||
@@ -595,28 +662,84 @@ async function setPasswordState(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() {
|
||||
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',
|
||||
'accounts',
|
||||
'tabRegistry',
|
||||
'sourceLastUrls',
|
||||
'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 || [],
|
||||
tabRegistry: prev.tabRegistry || {},
|
||||
sourceLastUrls: prev.sourceLastUrls || {},
|
||||
preferredIcloudHost: prev.preferredIcloudHost || '',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1460,6 +1583,408 @@ async function pollCloudflareTempEmailVerificationCode(step, state, pollPayload
|
||||
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
|
||||
// ============================================================
|
||||
@@ -1699,6 +2224,7 @@ async function closeTabsByUrlPrefix(prefix, options = {}) {
|
||||
.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;
|
||||
@@ -3222,6 +3748,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 };
|
||||
@@ -3308,6 +3869,7 @@ async function handleStepData(step, payload) {
|
||||
excludeLocalhostCallbacks: true,
|
||||
});
|
||||
}
|
||||
await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
|
||||
if (shouldUseCustomRegistrationEmail(latestState) && latestState.email) {
|
||||
await setEmailStateSilently(null);
|
||||
}
|
||||
@@ -3599,6 +4161,9 @@ function getEmailGeneratorLabel(generator) {
|
||||
if (generator === 'custom') {
|
||||
return '自定义邮箱';
|
||||
}
|
||||
if (generator === 'icloud') {
|
||||
return 'iCloud 隐私邮箱';
|
||||
}
|
||||
if (generator === 'cloudflare') return 'Cloudflare 邮箱';
|
||||
if (generator === CLOUDFLARE_TEMP_EMAIL_GENERATOR) return 'Cloudflare Temp Email';
|
||||
return 'Duck 邮箱';
|
||||
@@ -3773,6 +4338,9 @@ async function fetchGeneratedEmail(state, options = {}) {
|
||||
if (generator === 'custom') {
|
||||
throw new Error('当前邮箱生成方式为自定义邮箱,请直接填写注册邮箱。');
|
||||
}
|
||||
if (generator === 'icloud') {
|
||||
return fetchIcloudHideMyEmail();
|
||||
}
|
||||
if (generator === 'cloudflare') {
|
||||
return fetchCloudflareEmail(currentState, options);
|
||||
}
|
||||
|
||||
+177
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -8,17 +8,29 @@
|
||||
"alarms",
|
||||
"tabs",
|
||||
"webNavigation",
|
||||
"declarativeNetRequest",
|
||||
"debugger",
|
||||
"storage",
|
||||
"scripting",
|
||||
"activeTab"
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://*.icloud.com/*",
|
||||
"https://*.icloud.com.cn/*",
|
||||
"<all_urls>"
|
||||
],
|
||||
"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"
|
||||
},
|
||||
|
||||
+50
@@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -877,6 +877,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;
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
<select id="select-email-generator" class="data-select">
|
||||
<option value="duck">DuckDuckGo</option>
|
||||
<option value="cloudflare">Cloudflare</option>
|
||||
<option value="icloud">iCloud 隐私邮箱</option>
|
||||
<option value="cloudflare-temp-email">Cloudflare Temp Email</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -339,6 +340,66 @@
|
||||
<div id="hotmail-accounts-list" class="hotmail-accounts-list"></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 class="status-dot"></div>
|
||||
<span id="display-status">就绪</span>
|
||||
|
||||
+573
-7
@@ -79,6 +79,26 @@ 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 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');
|
||||
@@ -174,6 +194,11 @@ 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;
|
||||
@@ -1032,6 +1057,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(),
|
||||
@@ -1323,10 +1350,26 @@ function applySettingsState(state) {
|
||||
? 'custom'
|
||||
: '163');
|
||||
selectMailProvider.value = restoredMailProvider;
|
||||
const restoredEmailGenerator = String(state?.emailGenerator || '').trim().toLowerCase();
|
||||
selectEmailGenerator.value = ['cloudflare', 'cloudflare-temp-email'].includes(restoredEmailGenerator)
|
||||
? restoredEmailGenerator
|
||||
: '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 || '';
|
||||
@@ -1362,6 +1405,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;
|
||||
@@ -1609,6 +1655,9 @@ function getSelectedEmailGenerator() {
|
||||
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';
|
||||
@@ -1618,6 +1667,14 @@ function getEmailGeneratorUiCopy() {
|
||||
if (getSelectedEmailGenerator() === 'custom') {
|
||||
return getCustomMailProviderUiCopy();
|
||||
}
|
||||
if (getSelectedEmailGenerator() === 'icloud') {
|
||||
return {
|
||||
buttonLabel: '获取',
|
||||
placeholder: '点击获取 iCloud 隐私邮箱,或手动粘贴邮箱',
|
||||
successVerb: '获取',
|
||||
label: 'iCloud 隐私邮箱',
|
||||
};
|
||||
}
|
||||
if (getSelectedEmailGenerator() === 'cloudflare') {
|
||||
return {
|
||||
buttonLabel: '生成',
|
||||
@@ -1946,14 +2003,26 @@ function updateMailProviderUI() {
|
||||
const hotmailServiceMode = getSelectedHotmailServiceMode();
|
||||
rowInbucketHost.style.display = useInbucket ? '' : 'none';
|
||||
rowInbucketMailbox.style.display = useInbucket ? '' : 'none';
|
||||
const useCloudflare = selectEmailGenerator.value === 'cloudflare';
|
||||
const useCloudflareTempEmailGenerator = selectEmailGenerator.value === 'cloudflare-temp-email';
|
||||
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) {
|
||||
@@ -2002,7 +2071,7 @@ function updateMailProviderUI() {
|
||||
? '请先校验并选择一个 Hotmail 账号'
|
||||
: (useGeneratedAlias
|
||||
? '步骤 3 会自动生成邮箱,无需手动获取'
|
||||
: (useCustomEmail ? '请先填写自定义注册邮箱,成功一轮后会自动清空' : '先自动获取邮箱,或手动粘贴邮箱后再继续'));
|
||||
: (useCustomEmail ? '请先填写自定义注册邮箱,成功一轮后会自动清空' : `先自动获取${uiCopy.label},或手动粘贴邮箱后再继续`));
|
||||
}
|
||||
if (useHotmail) {
|
||||
inputEmail.value = getCurrentHotmailEmail();
|
||||
@@ -2166,6 +2235,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);
|
||||
}
|
||||
|
||||
@@ -2288,6 +2362,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 = '<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 = {}) {
|
||||
const { showFailureToast = true } = options;
|
||||
const uiCopy = getEmailGeneratorUiCopy();
|
||||
@@ -2316,6 +2759,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) {
|
||||
@@ -2628,6 +3074,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);
|
||||
});
|
||||
@@ -3106,6 +3621,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();
|
||||
@@ -3202,6 +3726,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);
|
||||
@@ -3502,6 +4039,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({
|
||||
@@ -3559,6 +4101,15 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
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) {
|
||||
inputAutoSkipFailures.checked = Boolean(message.payload.autoRunSkipFailures);
|
||||
updateFallbackThreadIntervalInputState();
|
||||
@@ -3582,6 +4133,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),
|
||||
|
||||
@@ -189,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: {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
async function finalizeIcloudAliasAfterSuccessfulFlow() {}
|
||||
function shouldUseCustomRegistrationEmail() {
|
||||
return false;
|
||||
}
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user