470 lines
16 KiB
JavaScript
470 lines
16 KiB
JavaScript
(function attachBackgroundMail2925Session(root, factory) {
|
||
root.MultiPageBackgroundMail2925Session = factory();
|
||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundMail2925SessionModule() {
|
||
function createMail2925SessionManager(deps = {}) {
|
||
const {
|
||
addLog,
|
||
broadcastDataUpdate,
|
||
chrome,
|
||
findMail2925Account,
|
||
getMail2925AccountStatus,
|
||
isMail2925AccountAvailable,
|
||
MAIL2925_LIMIT_COOLDOWN_MS,
|
||
normalizeMail2925Account,
|
||
normalizeMail2925Accounts,
|
||
pickMail2925AccountForRun,
|
||
getState,
|
||
reuseOrCreateTab,
|
||
sendToMailContentScriptResilient,
|
||
setPersistentSettings,
|
||
setState,
|
||
throwIfStopped,
|
||
upsertMail2925AccountInList,
|
||
} = deps;
|
||
|
||
const MAIL2925_SOURCE = 'mail-2925';
|
||
const MAIL2925_URL = 'https://2925.com/#/mailList';
|
||
const MAIL2925_LOGIN_URL = 'https://2925.com/';
|
||
const MAIL2925_INJECT = ['content/utils.js', 'content/mail-2925.js'];
|
||
const MAIL2925_INJECT_SOURCE = 'mail-2925';
|
||
const MAIL2925_COOKIE_DOMAINS = [
|
||
'2925.com',
|
||
'www.2925.com',
|
||
'mail2.xiyouji.com',
|
||
];
|
||
const MAIL2925_COOKIE_ORIGINS = [
|
||
'https://2925.com',
|
||
'https://www.2925.com',
|
||
'https://mail2.xiyouji.com',
|
||
];
|
||
const MAIL2925_LIMIT_ERROR_PREFIX = 'MAIL2925_LIMIT_REACHED::';
|
||
const MAIL2925_THREAD_TERMINATED_ERROR_PREFIX = 'MAIL2925_THREAD_TERMINATED::';
|
||
|
||
function getMail2925MailConfig() {
|
||
return {
|
||
provider: '2925',
|
||
source: MAIL2925_SOURCE,
|
||
url: MAIL2925_URL,
|
||
label: '2925 邮箱',
|
||
inject: MAIL2925_INJECT,
|
||
injectSource: MAIL2925_INJECT_SOURCE,
|
||
};
|
||
}
|
||
|
||
function getErrorMessage(error) {
|
||
return String(typeof error === 'string' ? error : error?.message || '');
|
||
}
|
||
|
||
function buildMail2925ThreadTerminatedError(message) {
|
||
return new Error(`${MAIL2925_THREAD_TERMINATED_ERROR_PREFIX}${String(message || '').trim()}`);
|
||
}
|
||
|
||
function isMail2925LimitReachedError(error) {
|
||
const message = getErrorMessage(error);
|
||
return message.startsWith(MAIL2925_LIMIT_ERROR_PREFIX)
|
||
|| /子邮箱.{0,12}已达上限|已达上限邮箱|子邮箱上限|邮箱已达上限/i.test(message);
|
||
}
|
||
|
||
function isMail2925ThreadTerminatedError(error) {
|
||
return getErrorMessage(error).startsWith(MAIL2925_THREAD_TERMINATED_ERROR_PREFIX);
|
||
}
|
||
|
||
async function syncMail2925Accounts(accounts) {
|
||
const normalized = normalizeMail2925Accounts(accounts);
|
||
await setPersistentSettings({ mail2925Accounts: normalized });
|
||
await setState({ mail2925Accounts: normalized });
|
||
broadcastDataUpdate({ mail2925Accounts: normalized });
|
||
return normalized;
|
||
}
|
||
|
||
async function upsertMail2925Account(input = {}) {
|
||
const state = await getState();
|
||
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
|
||
const normalizedEmail = String(input?.email || '').trim().toLowerCase();
|
||
const existing = input?.id
|
||
? findMail2925Account(accounts, input.id)
|
||
: accounts.find((account) => account.email === normalizedEmail) || null;
|
||
const credentialsChanged = !existing
|
||
|| (input?.email !== undefined && normalizedEmail !== existing.email)
|
||
|| (input?.password !== undefined && String(input.password || '') !== existing.password);
|
||
const normalized = normalizeMail2925Account({
|
||
...(existing || {}),
|
||
...(credentialsChanged ? { lastError: '' } : {}),
|
||
...input,
|
||
id: input?.id || existing?.id || crypto.randomUUID(),
|
||
});
|
||
|
||
const nextAccounts = existing
|
||
? accounts.map((account) => (account.id === normalized.id ? normalized : account))
|
||
: [...accounts, normalized];
|
||
|
||
await syncMail2925Accounts(nextAccounts);
|
||
return normalized;
|
||
}
|
||
|
||
function getCurrentMail2925Account(state = {}) {
|
||
return findMail2925Account(state.mail2925Accounts, state.currentMail2925AccountId) || null;
|
||
}
|
||
|
||
async function setCurrentMail2925Account(accountId, options = {}) {
|
||
const { logMessage = '', updateLastUsedAt = false } = options;
|
||
const state = await getState();
|
||
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
|
||
const account = findMail2925Account(accounts, accountId);
|
||
if (!account) {
|
||
throw new Error('未找到对应的 2925 账号。');
|
||
}
|
||
|
||
let nextAccount = account;
|
||
if (updateLastUsedAt) {
|
||
nextAccount = normalizeMail2925Account({
|
||
...account,
|
||
lastUsedAt: Date.now(),
|
||
});
|
||
await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item)));
|
||
}
|
||
|
||
await setState({ currentMail2925AccountId: nextAccount.id });
|
||
broadcastDataUpdate({ currentMail2925AccountId: nextAccount.id });
|
||
if (logMessage) {
|
||
await addLog(logMessage, 'ok');
|
||
}
|
||
return nextAccount;
|
||
}
|
||
|
||
async function patchMail2925Account(accountId, updates = {}) {
|
||
const state = await getState();
|
||
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
|
||
const account = findMail2925Account(accounts, accountId);
|
||
if (!account) {
|
||
throw new Error('未找到对应的 2925 账号。');
|
||
}
|
||
|
||
const nextAccount = normalizeMail2925Account({
|
||
...account,
|
||
...updates,
|
||
id: account.id,
|
||
});
|
||
await syncMail2925Accounts(accounts.map((item) => (item.id === account.id ? nextAccount : item)));
|
||
|
||
if (state.currentMail2925AccountId === account.id && nextAccount.enabled === false) {
|
||
await setState({ currentMail2925AccountId: null });
|
||
broadcastDataUpdate({ currentMail2925AccountId: null });
|
||
}
|
||
|
||
return nextAccount;
|
||
}
|
||
|
||
async function deleteMail2925Account(accountId) {
|
||
const state = await getState();
|
||
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
|
||
const nextAccounts = accounts.filter((account) => account.id !== accountId);
|
||
await syncMail2925Accounts(nextAccounts);
|
||
|
||
if (state.currentMail2925AccountId === accountId) {
|
||
await setState({ currentMail2925AccountId: null });
|
||
broadcastDataUpdate({ currentMail2925AccountId: null });
|
||
}
|
||
}
|
||
|
||
async function deleteMail2925Accounts(mode = 'all') {
|
||
const state = await getState();
|
||
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
|
||
const nextAccounts = mode === 'all'
|
||
? []
|
||
: accounts.filter((account) => getMail2925AccountStatus(account) !== String(mode || '').trim());
|
||
const deletedCount = Math.max(0, accounts.length - nextAccounts.length);
|
||
await syncMail2925Accounts(nextAccounts);
|
||
|
||
if (state.currentMail2925AccountId && !findMail2925Account(nextAccounts, state.currentMail2925AccountId)) {
|
||
await setState({ currentMail2925AccountId: null });
|
||
broadcastDataUpdate({ currentMail2925AccountId: null });
|
||
}
|
||
|
||
return {
|
||
deletedCount,
|
||
remainingCount: nextAccounts.length,
|
||
};
|
||
}
|
||
|
||
async function ensureMail2925AccountForFlow(options = {}) {
|
||
const {
|
||
allowAllocate = true,
|
||
preferredAccountId = null,
|
||
excludeIds = [],
|
||
markUsed = false,
|
||
} = options;
|
||
const state = await getState();
|
||
const accounts = normalizeMail2925Accounts(state.mail2925Accounts);
|
||
const now = Date.now();
|
||
|
||
let account = null;
|
||
if (preferredAccountId) {
|
||
account = findMail2925Account(accounts, preferredAccountId);
|
||
}
|
||
if (!account && state.currentMail2925AccountId) {
|
||
account = findMail2925Account(accounts, state.currentMail2925AccountId);
|
||
}
|
||
if ((!account || !isMail2925AccountAvailable(account, now)) && allowAllocate) {
|
||
account = pickMail2925AccountForRun(accounts, {
|
||
excludeIds,
|
||
now,
|
||
});
|
||
}
|
||
|
||
if (!account) {
|
||
throw new Error('没有可用的 2925 账号。请先在侧边栏添加至少一个带密码的 2925 账号。');
|
||
}
|
||
if (!account.password) {
|
||
throw new Error(`2925 账号 ${account.email || account.id} 缺少密码,无法自动登录。`);
|
||
}
|
||
if (!isMail2925AccountAvailable(account, now)) {
|
||
const disabledUntil = Number(account.disabledUntil || 0);
|
||
if (disabledUntil > now) {
|
||
throw new Error(`2925 账号 ${account.email || account.id} 当前处于冷却期,将在 ${new Date(disabledUntil).toLocaleString('zh-CN', { hour12: false })} 后恢复。`);
|
||
}
|
||
throw new Error(`2925 账号 ${account.email || account.id} 当前不可用。`);
|
||
}
|
||
|
||
return setCurrentMail2925Account(account.id, { updateLastUsedAt: markUsed });
|
||
}
|
||
|
||
function normalizeCookieDomainForMatch(domain) {
|
||
return String(domain || '').trim().replace(/^\.+/, '').toLowerCase();
|
||
}
|
||
|
||
function shouldClearMail2925Cookie(cookie) {
|
||
const domain = normalizeCookieDomainForMatch(cookie?.domain);
|
||
if (!domain) return false;
|
||
return MAIL2925_COOKIE_DOMAINS.some((target) => (
|
||
domain === target || domain.endsWith(`.${target}`)
|
||
));
|
||
}
|
||
|
||
function buildCookieRemovalUrl(cookie) {
|
||
const host = normalizeCookieDomainForMatch(cookie?.domain);
|
||
const path = String(cookie?.path || '/').startsWith('/')
|
||
? String(cookie?.path || '/')
|
||
: `/${String(cookie?.path || '')}`;
|
||
return `https://${host}${path}`;
|
||
}
|
||
|
||
async function collectMail2925Cookies() {
|
||
if (!chrome.cookies?.getAll) {
|
||
return [];
|
||
}
|
||
|
||
const stores = chrome.cookies.getAllCookieStores
|
||
? await chrome.cookies.getAllCookieStores()
|
||
: [{ id: undefined }];
|
||
const cookies = [];
|
||
const seen = new Set();
|
||
|
||
for (const store of stores) {
|
||
const storeId = store?.id;
|
||
const batch = await chrome.cookies.getAll(storeId ? { storeId } : {});
|
||
for (const cookie of batch || []) {
|
||
if (!shouldClearMail2925Cookie(cookie)) continue;
|
||
const key = [
|
||
cookie.storeId || storeId || '',
|
||
cookie.domain || '',
|
||
cookie.path || '',
|
||
cookie.name || '',
|
||
cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
|
||
].join('|');
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
cookies.push(cookie);
|
||
}
|
||
}
|
||
|
||
return cookies;
|
||
}
|
||
|
||
async function removeMail2925Cookie(cookie) {
|
||
const details = {
|
||
url: buildCookieRemovalUrl(cookie),
|
||
name: cookie.name,
|
||
};
|
||
|
||
if (cookie.storeId) {
|
||
details.storeId = cookie.storeId;
|
||
}
|
||
if (cookie.partitionKey) {
|
||
details.partitionKey = cookie.partitionKey;
|
||
}
|
||
|
||
try {
|
||
return Boolean(await chrome.cookies.remove(details));
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function clearMail2925SessionCookies() {
|
||
if (!chrome.cookies?.getAll || !chrome.cookies?.remove) {
|
||
return 0;
|
||
}
|
||
|
||
const cookies = await collectMail2925Cookies();
|
||
let removedCount = 0;
|
||
for (const cookie of cookies) {
|
||
throwIfStopped();
|
||
if (await removeMail2925Cookie(cookie)) {
|
||
removedCount += 1;
|
||
}
|
||
}
|
||
|
||
if (chrome.browsingData?.removeCookies) {
|
||
try {
|
||
await chrome.browsingData.removeCookies({
|
||
since: 0,
|
||
origins: MAIL2925_COOKIE_ORIGINS,
|
||
});
|
||
} catch (_) {
|
||
// Best-effort cleanup only.
|
||
}
|
||
}
|
||
|
||
return removedCount;
|
||
}
|
||
|
||
async function ensureMail2925MailboxSession(options = {}) {
|
||
const {
|
||
accountId = null,
|
||
forceRelogin = false,
|
||
actionLabel = '确保 2925 邮箱登录态',
|
||
} = options;
|
||
const account = await ensureMail2925AccountForFlow({
|
||
allowAllocate: true,
|
||
preferredAccountId: accountId,
|
||
});
|
||
|
||
if (forceRelogin) {
|
||
const removedCount = await clearMail2925SessionCookies();
|
||
await addLog(`2925:已清理 ${removedCount} 个登录相关 cookie,准备使用 ${account.email} 重新登录。`, 'info');
|
||
}
|
||
|
||
throwIfStopped();
|
||
await reuseOrCreateTab(MAIL2925_SOURCE, forceRelogin ? MAIL2925_LOGIN_URL : MAIL2925_URL, {
|
||
inject: MAIL2925_INJECT,
|
||
injectSource: MAIL2925_INJECT_SOURCE,
|
||
});
|
||
|
||
const result = await sendToMailContentScriptResilient(
|
||
getMail2925MailConfig(),
|
||
{
|
||
type: 'ENSURE_MAIL2925_SESSION',
|
||
step: 0,
|
||
source: 'background',
|
||
payload: {
|
||
email: account.email,
|
||
password: account.password,
|
||
forceLogin: forceRelogin,
|
||
},
|
||
},
|
||
{
|
||
timeoutMs: forceRelogin ? 90000 : 45000,
|
||
responseTimeoutMs: forceRelogin ? 90000 : 45000,
|
||
maxRecoveryAttempts: 2,
|
||
}
|
||
);
|
||
|
||
if (result?.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
if (result?.limitReached) {
|
||
throw new Error(`${MAIL2925_LIMIT_ERROR_PREFIX}${result.limitMessage || '2925 子邮箱已达上限邮箱'}`);
|
||
}
|
||
if (!result?.loggedIn) {
|
||
throw new Error(`2925:${actionLabel}失败,当前页面仍未进入收件箱。`);
|
||
}
|
||
|
||
await patchMail2925Account(account.id, {
|
||
lastLoginAt: Date.now(),
|
||
lastError: '',
|
||
});
|
||
await setState({ currentMail2925AccountId: account.id });
|
||
broadcastDataUpdate({ currentMail2925AccountId: account.id });
|
||
|
||
return {
|
||
account: await ensureMail2925AccountForFlow({
|
||
allowAllocate: false,
|
||
preferredAccountId: account.id,
|
||
}),
|
||
mail: getMail2925MailConfig(),
|
||
result,
|
||
};
|
||
}
|
||
|
||
async function handleMail2925LimitReachedError(step, error) {
|
||
const reason = getErrorMessage(error).replace(MAIL2925_LIMIT_ERROR_PREFIX, '').trim()
|
||
|| '子邮箱已达上限邮箱';
|
||
const state = await getState();
|
||
const currentAccount = getCurrentMail2925Account(state);
|
||
if (!currentAccount) {
|
||
return buildMail2925ThreadTerminatedError(`步骤 ${step}:2925 检测到“${reason}”,但当前没有可识别的账号,已结束本次尝试。`);
|
||
}
|
||
|
||
const disabledUntil = Date.now() + Math.max(1, Number(MAIL2925_LIMIT_COOLDOWN_MS) || (24 * 60 * 60 * 1000));
|
||
await patchMail2925Account(currentAccount.id, {
|
||
lastLimitAt: Date.now(),
|
||
disabledUntil,
|
||
lastError: reason,
|
||
});
|
||
await addLog(
|
||
`步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”,已禁用 24 小时,恢复时间 ${new Date(disabledUntil).toLocaleString('zh-CN', { hour12: false })}。`,
|
||
'warn'
|
||
);
|
||
|
||
const nextState = await getState();
|
||
const nextAccounts = normalizeMail2925Accounts(nextState.mail2925Accounts);
|
||
const nextAccount = pickMail2925AccountForRun(nextAccounts, {
|
||
excludeIds: [currentAccount.id],
|
||
});
|
||
|
||
if (!nextAccount) {
|
||
await setState({ currentMail2925AccountId: null });
|
||
broadcastDataUpdate({ currentMail2925AccountId: null });
|
||
return buildMail2925ThreadTerminatedError(
|
||
`步骤 ${step}:2925 账号 ${currentAccount.email} 已因“${reason}”禁用 24 小时,且当前没有可切换的下一个账号,本次尝试结束。`
|
||
);
|
||
}
|
||
|
||
await setCurrentMail2925Account(nextAccount.id);
|
||
await ensureMail2925MailboxSession({
|
||
accountId: nextAccount.id,
|
||
forceRelogin: true,
|
||
actionLabel: `步骤 ${step}:切换 2925 账号`,
|
||
});
|
||
await addLog(`步骤 ${step}:2925 已自动切换到下一个账号 ${nextAccount.email} 并完成登录,当前尝试将直接结束。`, 'warn');
|
||
return buildMail2925ThreadTerminatedError(
|
||
`步骤 ${step}:2925 账号 ${currentAccount.email} 命中“${reason}”并已禁用 24 小时,已切换到 ${nextAccount.email},当前尝试结束,等待自动重试进入下一次尝试。`
|
||
);
|
||
}
|
||
|
||
return {
|
||
MAIL2925_LIMIT_ERROR_PREFIX,
|
||
MAIL2925_THREAD_TERMINATED_ERROR_PREFIX,
|
||
clearMail2925SessionCookies,
|
||
deleteMail2925Account,
|
||
deleteMail2925Accounts,
|
||
ensureMail2925AccountForFlow,
|
||
ensureMail2925MailboxSession,
|
||
getCurrentMail2925Account,
|
||
getMail2925MailConfig,
|
||
handleMail2925LimitReachedError,
|
||
isMail2925LimitReachedError,
|
||
isMail2925ThreadTerminatedError,
|
||
patchMail2925Account,
|
||
setCurrentMail2925Account,
|
||
syncMail2925Accounts,
|
||
upsertMail2925Account,
|
||
};
|
||
}
|
||
|
||
return {
|
||
createMail2925SessionManager,
|
||
};
|
||
});
|