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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user